Java内存模型和虚拟机相关内容
Java线程内存模型
![image-20191217175041005](https://tva1.sinaimg.cn/large/006tNbRwly1g9zv84vidnj30wd0u0dqz.jpg)
线程私有
-
程序计数器--->**为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。**程序计数器是唯一一个不会出现OutOfMemoryError的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
-
虚拟机栈--->其中栈就是现在说的虚拟机栈,或者说是虚拟机栈中局部变量表部分。 (实际上,Java虚拟机栈是由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法出口信息。)
-
本地方法栈--->和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务
线程共有
-
堆:Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。
-
新生代(1/3)
- Eden(8/10)
- From Survivor(1/10)
- To Survivor(1/10)
-
老年代(2/3)
-
永久代--->JDK1.8移除整个永久代,取而代之的是一个叫元空间(Metaspace)的区域(永久代使用的是JVM的堆内存空间,而元空间使用的是物理内存,直接受到本机的物理内存限制)
-
方法区:方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。**
-
运行时常量池:运行时常量池是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池信息(用于存放编译期生成的各种字面量和符号引用)
直接内存
常见垃圾回收算法
-
如何判断对象已经死亡
-
引用计数法---->不能解决循环引用问题
给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被使用的。实现简单,效率高
-
GC Roots(可达性分析算法)
从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。
可作为GC Roots的:线程栈变量、静态变量、常量池、JNI指针
-
-
常见垃圾回收算法
-
标记清除(Mark and sweep)
首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象---->问题:位置不连续产生碎片
-
拷贝算法
它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。没有碎片,效率高,连续------>问题:浪费内存
-
标记-压缩(标记整理)
标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。没有碎片--->问题:浪费内存
-
分代收集
这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将 java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
比如在新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。
-
HotSpot虚拟机对象揭秘
对象创建过程
-
类加载检查
虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
-
申请内存
-
指针碰撞
用于堆内存比较规整
-
空闲列表
用于堆内存比较零散
-
-
成员变量赋予默认值
-
调用初始化方法
-
成员变量按顺序调用
-
初始化成员变量
-
构造方法调用
-
对象内存布局
和虚拟机有关
以HotSpot为例
-
对象头markWord
- 存储对象自身的自身运行时数据(哈希码、GC分代年龄、锁状态标志等等)
- 类型指针---->指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是那个类的实例。
-
实例数据(对象的成员变量)
-
补齐(因为Hotspot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍)
对象的访问定位
- 使用句柄:Java堆中将会划分出一块内存来作为句柄池
![image-20191218134522761](https://tva1.sinaimg.cn/large/006tNbRwly1ga0tr5oz9tj314i0jo46h.jpg)
- 直接指针: Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息
![image-20191218134627510](https://tva1.sinaimg.cn/large/006tNbRwly1ga0tsb9nsdj315i0k8jyq.jpg)
使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。
String类和常量池
String对象的两种创建方式
String str1 = "abcd";
String str2 = new String("abcd");
System.out.println(str1==str2);//false
- 第一种方式是在常量池中拿对象
- 第二种方式是直接在堆内存空间创建一个新的对象。(只要使用了new方法,则一定会在堆内存区域创建新的对象)
![image-20191218135354038](https://tva1.sinaimg.cn/large/006tNbRwly1ga0u00la52j312q0hyq4p.jpg)
String类型的常量池
-
直接使用双引号声明出来的 String 对象会直接存储在常量池中。
-
如果不是用双引号声明的 String 对象,可以使用 String 提供的 intern 方法。String.intern() 是一个 Native 方法,它的作用是:如果运行时常量池中已经包含一个等于此 String 对象内容的字符串,则返回常量池中该字符串的引用;如果没有,则在常量池中创建与此 String 内容相同的字符串,并返回常量池中创建的字符串的引用。
String s1 = new String("计算机"); String s2 = s1.intern(); String s3 = "计算机"; System.out.println(s2);//计算机 System.out.println(s1 == s2);//false,因为一个是堆内存中的String对象一个是常量池中的String对象, System.out.println(s3 == s2);//true,因为两个都是常量池中的String对象字符串拼接
-
字符串拼接
String str1 = "str"; String str2 = "ing"; String str3 = "str" + "ing"; //常量池中的对象 String str4 = str1 + str2; //在堆上创建的新的对象 String str5 = "string"; //常量池中的对象 System.out.println(str3 == str4);//false System.out.println(str3 == str5);//true System.out.println(str4 == str5);//false
String str = new String("abc")创建了几个对象?
2个 先有字符串"abc"放入常量池,然后 new 了一份字符串"abc"放入Java堆(字符串常量"abc"在编译期就已经确定放入常量池,而 Java 堆上的"abc"是在运行期初始化阶段才确定),然后 Java 栈的 str1 指向Java堆上的"abc"。
验证:
String s1 = new String("abc");// 堆内存的地址值
String s2 = "abc";
System.out.println(s1 == s2);// 输出false,因为一个是堆内存,一个是常量池的内存,故两者是不同的。
System.out.println(s1.equals(s2));// 输出true
8种基本类型的包装类和常量池
-
Java 基本类型的包装类的大部分都实现了常量池技术,即Byte,Short,Integer,Long,Character,Boolean;这5种包装类默认创建了数值[-128,127]的相应类型的缓存数据,但是超出此范围仍然会去创建新的对象。
-
两种浮点数类型的包装类 Float,Double 并没有实现常量池技术。
验证:
Integer i1 = 33;
Integer i2 = 33;
System.out.println(i1 == i2);// 输出true
Integer i11 = 333;
Integer i22 = 333;
System.out.println(i11 == i22);// 输出false
Double i3 = 1.2;
Double i4 = 1.2;
System.out.println(i3 == i4);// 输出false
- 深入理解包装类的装箱拆箱
Integer i1 = 40;
Integer i2 = 40;
Integer i3 = 0;
Integer i4 = new Integer(40);
Integer i5 = new Integer(40);
Integer i6 = new Integer(0);
System.out.println("i1=i2 " + (i1 == i2));
System.out.println("i1=i2+i3 " + (i1 == i2 + i3));
System.out.println("i1=i4 " + (i1 == i4));
System.out.println("i4=i5 " + (i4 == i5));
System.out.println("i4=i5+i6 " + (i4 == i5 + i6));
System.out.println("40=i5+i6 " + (40 == i5 + i6));
输出结果:
i1=i2 true
i1=i2+i3 true
i1=i4 false
i4=i5 false
i4=i5+i6 true
40=i5+i6 true
-
Integer i1=40;Java 在编译的时候会直接将代码封装成Integer i1=Integer.valueOf(40);,从而使用常量池中的对象。
-
Integer i4 = new Integer(40);这种情况下会创建新的对象。
-
语句i4 == i5 + i6,因为+这个操作符不适用于Integer对象,首先i5和i6进行自动拆箱操作,进行数值相加,即i4 == 40。然后Integer对象无法与数值进行直接比较,所以i4自动拆箱转为int值40,最终这条语句转为40 == 40进行数值比较。