点击查看更多JVM总结
文章目录
- 点击查看更多JVM总结
- 一、Java虚拟机内存模型相关
- 1.JVM内存区域
- 2.每一块内存区域的作用
- 3. 方法区和永久代之间的关系
- 4. 方法区和元空间的关系
- 5. 为什么要删去方法区而开辟元空间
- 6. 运行时常量池和字符串常量池在不同JDK版本的位置变化
- 7.Java堆空间的基本结构以及参数配置
- 8.堆内存中对象的分配策略是什么
- 9.垃圾收集过程与对象年龄判定
- 补充:==什么是分配担保==
- 10.动态年龄判断
- 11.如何判断一个对象是否已死
- 12.如何判断一个类是无用的类
- 13.如何判断一个常量是无用的
- 14.对象的创建过程是什么
- 15.并发地对象创建过程中,如何保证分配内存不发生冲突
- 16.对象访问定位的两种方式是什么
- 17.new String("Hello")产生了多少个String对象
- 18.包装类常量池是什么
一、Java虚拟机内存模型相关
1.JVM内存区域
JVM内存区域可分为线程私有和线程共享两个大部分,其中线程私有的包括:程序计数器、JVM栈、本地方法栈;而线程共享的包括:Java堆、方法区。
TIP:JDK 1.8中,去除了方法区,以直接内存中的元空间取代。
2.每一块内存区域的作用
1.程序计数器
程序计数器是一块很小的内存区域,其中存储的是当前线程所执行到的子节码行号;子节码指示器通过改变程序计数器的值来定位到子节码,以控制程序的跳转、循环、分支等操作。所以程序计数器是单个线程私有的,每个线程都有自己的一个程序计数器。程序计数器是唯一不会出现内存溢出的内存区域。
2.JVM栈
Java虚拟机栈是线程私有的,它的生命周期和线程的生命周期相同,它是描述Java方法运行的内存模型。
线程调用方法的过程和结束方法的过程,都对应着一个栈帧的入栈和出栈,每个栈帧中含有描述该方法的所有信息,例如含有局部变量表,用来存储方法的基本变量的值或者一个引用(指向Java堆中实例或者句柄);除了局部变量表,还含有操作数栈、动态链接、方法出口信息等。
在JVM栈区域,会出现StackOverflow异常和OutOfMemory异常,分别表示调用方法次数过多,栈帧的数量已经超过了栈深度、JVM栈已没有足够的空间去申请开辟线程。
3.本地方法栈
本地方法栈和JVM栈一样,不同的是本地方法栈执行的不是子节码,而是Native方法。
4.Java堆
Java堆又被称为GC堆,是线程共享的。大部分的数据实例都在这里分配,所以Java堆也是GC发生最频繁的地方。
Java堆可细分为新生代和老年代,其中新生代又可细分为1个Eden区和2个Survivor区。在Java堆中同时也含有字符串常量池,用以存储字符串。
5.方法区
方法区和Java堆一样是线程共享的,它存放的是类加载信息,包括类信息、常量、静态变量、即时编译器编译后的代码等数据,在HotSpot规范中,将方法区和Java堆描述为一个逻辑部分,但是为了区分它们,方法区又被称为"非堆"。
6.直接内存
并不属于Java虚拟机内存规范中定义,是IO速度最快的一块内存空间。
3. 方法区和永久代之间的关系
方法区是Java虚拟机规范中的一个规范,而永久代上hotSpot对方法区规范上的实现。
4. 方法区和元空间的关系
方法区在JDK 1.7开始被逐渐移出,在JDK 1.8中已删去方法区,而在直接内存中开辟了一块称为"元空间"的区域取代了方法区。
5. 为什么要删去方法区而开辟元空间
因为方法区在过去是内存溢出错误的常发地,由于方法区的空间在虚拟机启动之初即确定,所以会存在溢出;而元空间位于直接内存中,直接内存的空间只受本机内存所限制,不易发生内存溢出。
6. 运行时常量池和字符串常量池在不同JDK版本的位置变化
运行时常量池:
- JDK 1.6:位于方法区中
- JDK 1.7:位于方法区中
- JDK 1.8:位于元空间中
字符串常量池:
- JDK 1.6:位于方法区运行时常量池中
- JDK 1.7:位于Java堆中
- JDK 1.8:位于Java堆中
7.Java堆空间的基本结构以及参数配置
Java堆空间可分为新生代和老年代,而新生代中又可分为Eden区和from Survivor区、to Survivor区。可由以下参数进行配置:
- -Xms 20M:Java堆空间最多为20M
- -Xmx 20M:Java堆空间最少为20M
- -Xmn 10M:新生代分配10M
- -XX:SurvivorRatio = 8: Eden区/1个Survivor区 = 8
例题:-Xms 10M,-Xmx 10M, -Xmn 5M,-XX:SurvivorRatio = 3:问Survivor区总空间为多少?
由-Xms 10M/ -Xmx 10M可知,Java堆空间被限制为10M;由-Xmn 5M得知,新生代空间被分配为5M;由-XX:SurvivorRatio = 3 可知,Eden/一个Survivor区 = 3,故可知Eden区为3M,一个Survivor区为1M,故Survivor区总空间为2M,
8.堆内存中对象的分配策略是什么
1.对象优先在新生代Eden区分配
2.大对象直接进入老年代(避免频繁的分配担保)
3.长期存活对象直接进入老年代
9.垃圾收集过程与对象年龄判定
1.第一次分配
- 将对象分配到Eden区,当对象是大对象(超过了
-XX:PretenureSizeThresold
时),直接分配到老年代 - 当Eden区空间不足时,先将Eden区中不需要回收的对象转移到To Survivor中(每次转移到To Survivor的过程都使对象年龄+1)
- 发生Minor GC,将整个Eden区清空
- 互换From Survivor与To Survivor,保证在下一次Minor GC前To Survivor是空的
2.第二次及之后的分配
- 将对象分配到Eden区,当对象是大对象(超过了
-XX:PretenureSizeThresold
时),直接分配到老年代 - 当Eden区空间不足时,先将Eden区与From Survivor区中不需要回收的对象转移到To Survivor中(每次转移到To Survivor的过程都使对象年龄+1)
- 发生Minor GC,将整个Eden区和From Servivor区清空
- 互换From Survivor与To Survivor,保证在下一次Minor GC前To Survivor是空的
补充:什么是分配担保
分配担保指的是在发生Minor GC前,Eden区中的需要转移到To Survivor区的对象过大,无法被From Survivor区容纳,需要把该对象以“分配担保”的形式“担保”给老年代。这个过程会十分耗时,并且由于大对象在Eden与老年代之间的复制,会造成效率的损失,这也是为什么要将大对象直接分配到老年代的原因。
10.动态年龄判断
其实to Survivor中的对象并不一定需到达15岁才会被转移到老年代如果Survivor空间中相同年龄的所有对象大小大于Survivor空间的一半,那么年龄大于等于该年龄的对象将会被转移到老年代,无需达到MaxTenuringThreshold
规定的年龄。
11.如何判断一个对象是否已死
1.引用计数法:无法解决循环引用的问题
2.可达性分析(GC ROOT)
GC ROOT可以是JVM栈中引用的对象、方法区或元空间中的类变量、Java堆中的对象等。
12.如何判断一个类是无用的类
1.该类所有实例都已经被回收,Java堆中不存在该类的任何实例
2.加载该类的类加载器已经被回收
3.该类对应的java.lang.Class对象没有在任何地方被引用,无法通过反射获取该类的变量和方法
13.如何判断一个常量是无用的
没有在任何地方被引用。
14.对象的创建过程是什么
1.类加载检查,在方法区常量池中检查该类的符号引用是否存在,是否被加载过,若没加载过则进行类加载过程
2.为对象在堆中分配内存:
1.指针碰撞分配
2.空闲列表分配
3.初始化零值
4.设置对象头:
1.类的元数据
2.对象的哈希码
3.GC分代年龄
5.执行<init>方法
15.并发地对象创建过程中,如何保证分配内存不发生冲突
1.CAS + 失败重试,直到内存分配成功
2.每一个线程在Java堆中 预先分配一小块内存,被称为本地线程分配缓存(TLAB)。每个线程都在各自的TLAB中分配内存,如果使用完了当前的TLAB,在使用CAS + 失败重试去再分配出一块TLAB。
16.对象访问定位的两种方式是什么
对象访问定位即JVM栈中如何定位到位于Java堆中的实例数据,有两种方式
1.直接指针定位
即JVM栈中存储的是对象的指针,该指针直接指向了Java堆中的实例
2.句柄定位
句柄定位指的是JVM栈中存储的是对象的句柄地址,句柄地址中存储着指向对象实例的指针。
这两种对象访问方式各有优势。使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。
17.new String(“Hello”)产生了多少个String对象
若字符串常量池中不存在Hello这个字符串常量,则会在字符串常量池中创建一个,并且在Java堆中也创建一个字符串实例,所以一共创建了两个对象;若字符串常量池中已经含有了Hello,那么只会在堆中创建一个字符串实例。
String a = "hello";
String b = "hello";
String c = new String("hello");
a == b // true,引用的都是位于字符串常量池的hello
a == c // false,new String引用的是Java堆中(非常量池)的hello
18.包装类常量池是什么
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 缓存源代码:
/**
*此方法将始终缓存-128 到 127(包括端点)范围内的值,并可以缓存此范围之外的其他值。
*/
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
应用场景:
Integer i1=40;Java 在编译的时候会直接将代码封装成 Integer i1=Integer.valueOf(40);,从而使用常量池中的对象。
Integer i1 = new Integer(40);这种情况下会创建新的对象。
Integer i1 = 40;
Integer i2 = new Integer(40);
System.out.println(i1==i2);//输出 false
Integer 比较更丰富的一个例子:
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
解释:
语句 i4 == i5 + i6,因为+这个操作符不适用于 Integer 对象,首先 i5 和 i6 进行自动拆箱操作,进行数值相加,即 i4 == 40。然后 Integer 对象无法与数值进行直接比较,所以 i4 自动拆箱转为 int 值 40,最终这条语句转为 40 == 40 进行数值比较。