
本文共 23064 字,大约阅读时间需要 76 分钟。
JVM之(执行引擎、性能监控、垃圾回收)-总结
如想了解更多更全面的Java必备内容可以阅读:所有JAVA必备知识点面试题文章目录:
注意: 本篇主要以HotSpot虚拟机为主,主要涉及JDK1.8,涵盖(JDK1.6~JDK13)
Java8虚拟机规范官方地址: https://docs.oracle.com/javase/specs/jvms/se8/html/Java8官网参数配置说明地址: https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
HotSpot虚拟机垃圾收集优化指南:https://docs.oracle.com/en/java/javase/14/gctuning/
来…直接进入主题:
JVM入门到精通学习视频:https://study.163.com/course/courseMain.htm?courseId=1209670826文章目录
1、说说Java创建对象都有哪些方式?
- 通过new关键字来创建对象。
- 使用Class的newInstance():此方式在JDK1.9就过时了,此方法是通过反射的方式,但是只能调用空参的构造器,权限也必须是public。
- 使用Constructor的newInstance(形参列表):反射的方式,可以调无参、有参构造器,权限没有要求。
- 使用clone():通过实现Cloneable接口,实现克隆,此方式也称浅克隆。
- 使用序列化:通过序列化和反序列化来创建对象,此方式也称深克隆。
- 使用第三方库:如Objenesis是一个小的Java库,它有一个用途:实例化一个特定类的新对象。
2、结合JVM说说创建一个对象的主要步骤都有哪些?
- 判断对象对应的类是否加载、链接、初始化:检查对应的类是否在运行时常量池中已经被加载、解析、初始化,如果没有通过双亲委派机制进行加载生成对应的class对象,如果没有找到这个类则抛出ClassNotFoundException异常。
- 为对象分配内存:计算对象占用的空间大小,并在堆中划分一块内存给新对象。
- 内存规整:采用指针碰撞为对象分配内存。【指针碰撞:内存规整即所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离】
- 内存不规整:即已使用内存与未使用内存相互交错无序。虚拟机采用空闲列表法来为对象分配内存。【采用空闲列表:维护一个列表,记录哪些内存可用】
- Java堆是否规整是由采用的垃圾收集器是否带有压缩整理功能决定。
- 分配内存带来的并发安全问题:采用CAS比较交换技术保证原子性;为每一个线程预先分配一个TLAB 。
- 初始化分配到的堆空间:为其设置默认值。
- 设置对象的对象头:将对象所属类信息、对象的HashCode值、GC信息、锁信息等数据存储在对象的对象头中。
- 执行显示初始化、代码块中初始化以及构造器中初始化,并把堆内对象的地址复制给引用变量。
3、对象在堆空间的内存布局是什么?
- 对象头(Hander):包含运行时元数据(包含:哈希值、GC分代年龄/阙值、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳)和类型指针(指向方法区元数据的Class信息)。
- 实例数据(Instance Data):对象存储的有效信息,包含程序代码中定义的各种类型的字段以及父类的。(规则:相同宽度的字段总被分配在一起;父类定义的变量会出现在子类之前)。
- 对其填充(Padding):不是必须得,起到占位符的作用。
4、举例说明创建对象时,图示说明虚拟机栈、堆、方法区都分别是怎么存储的?
### CustomerTest类public class CustomerTest { public static void main(String[] args){ Customer cust = new Customer(); }}### Customer类public class Customer { int i = 1001; String name; Account acct; { name = "匿名客户"; } public Customer(){ acct = new Account(); }}class Account{ }####################################图示如下:
5、JVM是如何通过栈帧中对象的引用访问到其内部的对象实例呢?
对象的访问方式主要有两种,分别是句柄访问和直接指针(HotSpot虚拟机采用)。


6、简述机器码、指令、汇编语言、高级语言都分别是什么?
机器码:是由0和1组成的二进制序列,机器码能够被计算机理解和接收,相比其他语言编写的程序它的执行速度最快。
指令:将机器码中特点的0和1序列简化成对应的指令,提高可读性。 汇编语言:由于指令的可读性还是太差,于是引进了汇编语言。在汇编语言中,用助记符代替指令的操作码,用地址符号或标号带起指令的地址。汇编语言还必须翻译成机器码才能被计算机识别和执行。 高级语言:高级语言比机器语言和汇编语言更接近人的语言。经过高级语言编写的程序,任然需要把程序解释和编译成机器的指令码。
7、什么是执行引擎,执行引擎都包含哪些模块?
执行引擎:将字节码指令解释/编译为对应平台的本地机器指令,也就是将高级语言翻译成机器语言。
执行引擎主要包括解释器、及时编译器、垃圾回收器。
8、什么是解释器?
解释器:虚拟机启动时会分局预定义的规范对字节码采用逐行解释的方式执行。解释器分为两种,分别是:
- 字节码解释器:存软件代码模拟字节码执行,效率非常低。
- 模板解释器:发一条字节码与一个模板函数相关联,模板函数直接产生这条字节码执行时的机器码。
缺点:效率低,性能差。
优点:响应时间快,可立即逐行执行。9、什么是及时编译器[JIT(just in time)编译器]?
JIT(just in time)编译器:是虚拟机将源代码直接编译成和本地机器平台相关的机器语言,并使用栈上替换,缓存在方法区中。
是否需要启动JIT编译器需要根据代码被调用执行的频率而定。一个被多次调用的 方法 或者方法体内循环次数较多的 循环体 被称为热点代码。JIT编译器还会对那些热点代码做深度优化。调用多少次才能被认为是热点代码呢?目前HotSpot VM所采用的热点探测方式是基于计数器的热点探测。并且为每一个方法够建立2种不同类型的计数器,分别是:
- 方法调用计数器:统计一段时间内方法被调用的次数,一定时间(半衰周期)内,方法被调用次数还是不足设定值,计数器会被衰减一半。默认阙值:Client模式下1500次,Server模式下10000次。可以通过参数【-XX:CompileThreshold】来设置
- 回边计数器:统计循环体执行的循环次数。
【-Xint】:采用完全解释器模式。
【-Xcomp】:采用完全编译器模式。 【-Xmixed】:采用解释器+编译器的混合模式(默认)。在HotSpot VM中内嵌有两个JIT编译器,分别是Client编译器和Server编译器,简称C1编译器(策略:方法内联、去虚拟化、冗余消除)和C2编译器(策略:栈上分配、同步消除、标量替换)。C2编译器启动时长比C1编译器慢;系统稳定后C2执行熟读快于C1。
缺点:响应时间满,立即编译需要花费一定的时间。
优点:对热点代码效率搞,性能好。
10、为什么是Java是半编译半解释型语言?
HotSpot VM采用解释器与编译器 并存的架构。在Java虚拟机运行时,解释器和编译器能够相互协作、取长补短,通常二者结合起来进行。
当Java虚拟机启动时,解释器可以首先发挥作用,而不必等JIT编译器全部编译完成后执行,这样可以省去许多不必要的编译时间,提高了响应时间。随着时间推移,编译器发挥作用,从而获得更高的效率。

11、什么是静态提前编译器[AOT(Ahead Of Time)编译器]?
AOT编译器:JDK1.9引入。在程序运行之前将字节码转换成机器码的过程。
优点:可以直接执行。 缺点:破坏Java"一次编译到处运行";降低Java的动态性(多态性)等。12、说说Java中String的特性及底层实现?
String标识字符串。申明为final说明不可继承;实现了Serializable接口说明可以支持序列化;实现了Comparable接口表示可以比较大小。
- 在JDK8及之前 底层具体实现为:final char[] value;
- 在JDK9及之后 底层具体实现为:final byte[] value;【为什么要改?官方说明:String该类的当前实现将字符存储在 char数组中,每个字符使用两个字节(十六位)。从许多不同的应用程序收集的数据表明,字符串是堆使用情况的主要组成部分,而且,大多数String对象仅包含Latin-1字符。这样的字符仅需要一个字节的存储空间,因此char此类String对象的内部数组中的 一半空间未使用。】【同时操作字符串的类如 StringBuffer、StringBuilder等等也做了同样的更新】
不可变性,例如以下操作时需要重新指定新的内存区域,不能将原来的值进行修改:
- 对字符串重新赋值时。
- 对现有的字符串进行连接操作时。
- 当调用String的replace()等方法时。
- ……等
13、对字符串常量池(StringTable)的理解?
字符串常量池是一个固定大小的HashTable。可以使用参数 【-XX:StringTableSize】 进行设置。可以使用参数 【-XX:+PrintStringTableStatistics】 打印字符串常量池的信息。如果太小时,就会造成更多的Hash冲突,导致链表会很长,当调用String.intern()时性能会大幅下降。
- JDK1.6之前,字符串常量池放在永久代(方法区)中。大小默认是1009,设置无要求。
- JDK1.7及之后,字符串常量池放在堆中。默认大小是60013,设置无要求。
- JDK1.8之后设置StringTableSize的最小值是1009。
基本特性:字符串常量池中不会存储相同内容的字符串。创建时,如果字符串常量池中有则返回其地址;如果没有,创建字符串并返回其地址。
14、分析在不同的字符串拼接场景下,其结果放在堆还是字符串常量池中?
-
常量与常量的拼接结果放在—>字符串常量池中。
######简单代码如下: s1/s2/s5 都指向字符串常量池中的----->"shishihenlove"public static void main(String[] args){ //经过编译之后,反编译字节码可知,s1被优化成:[s1 = "shishihenlove";],并将"shishihenlove"放入字符串常量池中。 String s1 = "shi"+"shi"+"hen"+"love"; //检查字符串常量池中是否有"shishihenlove",可知上一行代码已经将"shishihenlove"放入,所以直接将"shishihenlove"的地址返回给s2 String s2 = "shishihenlove"; System.out.println(s1 == s2);//----------->true final String s3 = "shishi"; final String s4 = "henlove"; //经过编译之后,反编译字节码可知,s5被优化成:[s5 = "shishihenlove";],并将字符串常量池中"shishihenlove"的地址返回给s5。 String s5 = s3 + s4; System.out.println(s5 == s2);//----------->true}
-
只要其中有一个是变量,拼接结果放在—>堆中。
/*** 说明:如下字符串["shishihenlove"、"henlove"、"shishi"],检查字符串常量池中是否* 如果没有,则在字符串常量池中创建改字符串,并将其地址返回给局部变量* 如果有,则直接将字符串的地址返回给局部变量* @param args*/public static void main(String[] args){ String s1 = "shishihenlove"; final String s2 = "henlove"; String s3 = "shishi"; final String s4 = "shishi"; //说明:s3是一个变量,所以如下s3+s2字节码执行细节 类似如下: // ① StringBuilder sb = new StringBuilder(); //--jdk1.5之前使用的是StringBuffer // ② sb.append("shishi"); // ③ sb.append("henlove"); // ④ sb.toString() ====>其底层实现相当于 new String("shishihenlove") //我们知道通过【new关键字】出来的对象是放在【堆】中的。 //所以 s5(堆中地址) != s1(字符串常量池中地址) String s5 = s3 + s2; System.out.println(s5 == s1);//----------->false //经过编译之后,反编译字节码可知,s6被优化成:[s6 = "shishihenlove";],并将字符串常量池中"shishihenlove"的地址返回给s6。 String s6 = s4 + s2; System.out.println(s6 == s1);//----------->true}
15、如何保证创建的字符串一定存放在字符串常量池中?
- 通过 双引号" " 创建的字符串一定存放在字符串常量池中
String s1 = "shishihenlove";
- 通过将字符串调用String类的intern()方法,intern()会主动将字符串放入字符串常量池中,如果有则返回其地址,如果没有则创建后返回其地址。
String s1 = "shishihenlove";final String s2 = "henlove";String s3 = "shishi";//调用intern()方法String s5 = (s3 + s2).intern();System.out.println(s5 == s1);//----------->true
16、对于使用String中的intern()方法,在不同JDK版本中的差异知道多少?
总结String中的intern()的使用:
-
jdk1.6及之前,调用intern()方法,会将这个字符串对象 尝试 放入 【字符串常量池中】。
- 如果 【字符串常量池中】有,并不会创建,而是将已有的字符串地址返回。
- 如果【字符串常量池中】没有,会把对象的 “值” 复制一份,放入字符串常量池中,并返回其字符串的地址。
-
jdk1.7及之后,调用intern()方法,会将这个字符串对象 尝试 放入 【字符串常量池中】。
- 如果 【字符串常量池中】有,并不会创建,而是将已有的字符串地址返回。
- 如果【字符串常量池中】没有,会把对象的 “引用地址” 复制一份,放入字符串常量池中,并返回其字符串的地址。 在 jdk1.7及之后,如果程序中大量使用字符串,使用intern()方法,可以确保堆中就一份字符串,节省内存空间。
17、你认为new String(“shishihenlove”)会创建几个对象?
答案:两个或者一个。
解析:① 第一个对象:new关键字创建的对象会创建在【堆】中。 ② 第二个对象:如果字符串常量池中没有"shishihenlove"则创建 ,如果有则不创建。18、说说 new String(“shishi”) + new String(“henlove”)会创建几个对象?
【在字符串常量池中会 不会有 “shishihenlove”】
/*** 对象创建说明:* 对象① :拼接操作(+) new StringBuilder()* 对象② :new String("shishi")* 对象③ :"shishi"在字符串常量池中创建* 对象④ :new String("henlove") * 对象⑤ :"henlove"在字符串常量池中创建* * 对象⑥ :StringBuilder()调用toString()方法会创建一个 new String("shishihenlove"),* 特别说明,这时的"shishihenlove"不会在字符串常量池中创建*/String str = new String("shishi") + new String("henlove");
19、说说如下代码的执行结果?
public static void main(String[] args){ //###s指向【堆空间】new String("1")的地址 String s = new String("1"); //切记切记 : s.intern(); 【在字符串常量池中创建"1"】 与 s = s.intern();【s指向字符串常量池"1"的引用】 不一样 s.intern(); //上一行代码已经在字符串常量池中创建了"1",所以此行代码无具体意义。 //###s2指向【字符串常量池】中"1"的地址 String s2 = "1"; System.out.println(s == s2);//jdk1.6(字符串常量池在方法区)==>false jdk1.7及之后(字符串常量池在堆)==>false //###s3指向【堆空间】new String("11")的地址 String s3 = new String("1") + new String("1");//注意:【执行完此行之后,字符串常量池中还没有"11"】 //切记切记 : s3.intern();【在字符串常量池中创建"11"】 与 s3 = s3.intern(); 【s3指向字符串常量池"11"的引用】 不一样 /**在字符串常量池中创建"11"。两种情况: * ① jdk1.6(字符串常量池在方法区)会创建"11"; * ② jdk1.7及之后(字符串常量池在堆)此时堆空间已经有"11"了,为了节约空间并没有字符串常量池中创建“11”,而是在字符串常量池中创建一个指向堆空间中new String("11")的地址。 */ s3.intern(); //###s4指向【字符串常量池】中"11"的地址 String s4 = "11"; System.out.println(s3 == s4);//jdk1.6(字符串常量池在方法区)==>false jdk1.7及之后(字符串常量池在堆)==>true //##################拓展 拓展 拓展 拓展 拓展 拓展 拓展 拓展 拓展 拓展################## //###s5 指向【堆空间】new String("11")的地址 String s5 = new String("1") + new String("1");//注意:【执行完此行之后,字符串常量池中还没有"11"】 //###在【字符串常量池】中创建"11",s6并指向【字符串常量池】中"11"的地址 String s6 = "11"; s5.intern();//上一行代码已经在字符串常量池中创建了"11",所以此行代码无具体意义。 System.out.println(s5 == s6);//=======>s5与s6指向的地址不一样 false //s5指向【字符串常量池】中"11"的地址 s5= s5.intern(); System.out.println(s5 == s6);//=======>s5与s6都指向【字符串常量池】中"11"的地址 ture}##############################图示解析如下:

20、什么是垃圾回收?
垃圾:指运行程序中没有任何指针指向的对象。
GC垃圾回收:首先需要区分内存中哪些是存活对象,哪些是已经死亡的对象。只有被标记为已经死亡的对象,GC才会在执行垃圾回收时,释放其占有的内存空间。 判断对象存活一般有两种算法:引用计数算法 和 可达性分析算法。21、什么是引用计数算法(Referece Counting)?
引用计数算法(Referece Counting):对每一个对象保存一个整形的引用计数器属性,用于记录对象被引用的情况。
举例说明:对于一个对象A,当有任何一个对象引用了A,这A的引用计数器就加1;当引用失效时,A的引用计数器减1。只要对象A的引用计数器的值为0,即A对象没有被引用,就可以被回收。
优点:
- 实现简单,判断率较高,回收没有延迟性。
缺点:
- 每一个对象都有引用计数器,增加内存开销。
- 每一次引用的改变都伴随着引用计数器的变化,增加了时间开销。
- ------> 最严重的问题是:无法处理循环引用的情况,有可能导致内存泄漏和OOM,如下场景:
使用场景:因为循环引用的问题,Java没有采用引用计数器算法,但是Python采用了此算法。
Python如何解决循环引用问题的?
- 手动解除。适当时机手动的解除引用关系
- 使用弱引用weakref
22、什么是可达性分析算法(根搜索算法)?
可达性分析算法:解决了引用计数短发中循环引用的问题,防止了内存泄漏。
基本思路:
- 以 根对象集合(GC Roots) 为起点,从上至下 的方式搜索 被根节点对象集合所连接的目标对象是否可到达。
- 内存中存活的对象都会被跟对象直接或间接的连接着,连接的路径称为引用链(Referece Chain)。
- 如果对象没有被引用链相连,则是不可达对象,意味着对象已经死亡,可以标记为垃圾对象。
可达性分析算法是用来判断内存是否可回收,而且分析工作必须在一个能保证一致性的快照中进行。所以也会导致必须暂停用户线程[STW:Stop The World]。
23、在Java中,什么是GC Roots? GC Roots都包括哪些?
GC Roots:是一组必须活跃的引用根集合。可以通过MAT(Java堆内存分析器) 和 JProfiler等工具查看GC Roots。
在Java中,GC Roots包括以下几类元素的集合集合:- 虚拟机栈中~引用的对象:比如每个线程被调用的方法中使用的参数、局部变量等。
- 本地方法栈中~JNI引用对象。
- 方法区中~类静态属性引用的对象:如Java类的引用类型静态变量。
- 方法区中~常量引用的对象:比如jdk1.7之前,方法区中字符串常量池里的引用。
- 所有被同步锁synchronize 持有的对象。
- Java虚拟机内部的引用:如基本数据类型对应的class对象;一些常驻的异常对象(NullPointerException、OutOfMemoryError等);系统类加载器。
- 反应Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
- 除以上外,根据用户所选用的垃圾收集器以及当前会后的内存区域不同,还可以有其他对象“临时性”的加入,比如G1的Remembered Set记忆集,共同构成了完整的GC Root集合。比如分代收集 和 局部回收。
判断是否为Root的小技巧:由于Root采用栈方式存放变量和指针。所以如果一个指针,它保存了堆内存里面的对象,但是自己又不存放在堆中。那么它就是一个Root。

24、说说对象的finalization机制的理解?
Java语言提供了对象终止(finalization)机制 来允许开发人员提供对象被销毁之前的自定义处理逻辑。
当垃圾回收器发现没有引用指向一个对象,即: 垃圾回收此对象之前,总会先调用这个对象的finalize()方法。 finalize()方法允许在子类中被重写,用于在对象被回收时进行资源释放。通常在这个方法中进行一些资源释放和清理的工作,比如关闭文件、套接字和数据库连接等。永远不要【主动】调用某个对象的finalize()方法,应该交给垃圾回收机制调用。理由包括下面三点:
- 在finalize()时可能会导致对象复活。
- finalize()方法的执行时间是没有保障的,它完全由GC线程决定,极端情况下,若不发生GC,则finalize()方法将没有执行机会。
- 一个糟糕的finalize()会严重影响GC的性能。例如在finalize()中出现死循环等,会一直掉finalize(),当finalize()没有执行结束是不会执行GC操作的。
如果从所有的根节点都无法访问到某个对象,说明对象已经不再使用了。一般来说,此对象需要被回收。但事实上,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段。一个无法触及的对象有可能在某一个条件下"复活"自己,如果这样,那么对它的回收就是不合理的。为此,由于finalize()方法的存在,虚拟机中的对象一般处于三种可能的状态。如下:
- 可触及的:从根节点开始,可以到达这个对象。
- 可复活的:对象的所有引用都被释放,但是对象有可能在finalize()中复活。
- 不可触及的:对象的finalize()被调用,并且没有复活,那么就会进入不可触及状态。不可触及的对象不可能被复活,因为finalize()只会被调用一次。
25、判断一个对象A是否被回收,会经历哪两次标记过程?
- 如果对象A到GCRoots没有引用链,则 第一次被标记。
- 进行筛选,判断此对象是否有必要执行finalize()方法。
- 如果判断对象A 没有重写 finalize()方法,或者finalize()方法已经被虚拟机过。则虚拟机认为 “没有必要执行” finalize()方法,A对象被判定为不可触及的。
- 如果对象A 重写了 finalize()方法,且还未执行过,那么对象A会被插入到F-Queue队列中,由一个虚拟机自动创建低优先级的 Finalizer线程 触发其finalize()方法。
- finalize()方法是对象逃脱死亡的最后机会。稍后GC会对F-Queue队列中的对象进行 第二次标记。如果A对象在finalize()方法中与引用链上的任何一个对象建立了联系,那么在第二次标记时,A对象会被移出"即将回收"集合。 之后,对象可能会再次出现没有引用存在的情况。在这个情况下,finalize()方法不会被再次调用,对象会直接变成不可触及的状态,也就是说,一个对象的finalize()方法只会被调用一次。
26、垃圾清除阶段,常见的三种垃圾收集算法都分别是什么?三种算法之间存在区别?
当成功区分出内存中存活对象和死亡对象后,接下来就是执行垃圾回收,释放无用对象所占的内存空间。
垃圾清除阶段,常见的三种垃圾收集算法都分别:
- 标记-清除算法(mark-Sweep)
- 复制算法(Copying)
- 标记-压缩算法(Mark-Compact)
三种垃圾收集算法的区别:
# | 标记-清除算法(mark-Sweep) | 复制算法(Copying) | 标记-压缩算法(Mark-Compact) |
---|---|---|---|
时间开销 | 中等 | 最快 | 最慢 |
空间开销 | 少 | 通常需要对象的两倍的内存空间 | 少 |
是否存在碎片 | 存在 | 不存在 | 不存在 |
是否移动对象至新的地址 | 否 | 是(需要同步改所用引用该对象的引用地址) | 是(需要同步改所用引用该对象的引用地址) |
27、什么是标记-清除算法(mark-Sweep)?
标记-清除算法(mark-Sweep),1960年被提出,标记-清除算法需要进行两项工作,如下:
- 标记:从引用根节点开始遍历,标记所有被引用的对象,即标记可达的对象。
- 清除:从堆内存从头到尾进行线性遍历。如果发现没有被标记的对象,将其清除。
何为清除? 说明:清除并不是将其内存置空,而是把清除对象的内存地址记录在【空闲地址列表】中,下次有新的对象分配时,判断垃圾的位置空间是否足够,如果足够将原来的垃圾地址覆盖。
缺点:
- 效率不算高。
- 当GC时需要STW,用户体验差。
- 清理出来的空闲内存不是连续的,会产生内存碎片,即内存不规整。需要维护一个“空闲地址列表”。
28、什么是复制算法(Copying)?
复制算法(Copying)核心思想:将内存空间分为两块。每次只使用其中一块,在垃圾回收时,将其存活的对象依次复制到另一块内存中,之后将其全部清除,最后完成垃圾回收。
优点:
- 实现简单,运行高效
- 不会出现【内存碎片】问题
缺点:
- 需要两倍的内存空间,将一块内存平均分为两份,每次只使用其中一块。
- 对象是复制而不是移动,所以对象的移动需要维护其对象的引用,会增加时间的开销。
使用场景:适合使用需要复制的对象不易太多,或者非常少才行。即:存活对象少,垃圾对象多的前提下。如堆区中 新生代中 Survivor区的实现。

29、什么是标记-压缩算法(Mark-Compact)?
背景:复制算法的高效性是建立在存活对象少,垃圾对象多的前提下,适用于新生代。但是老年代中,通常都是存活的对象,所以复制算法不适用。
标记-压缩算法(Mark-Compact),诞生于1970年,可以看做是对标记-清除算法的改进,也是分为两个阶段,如下:
- 第一阶段-标记:与标记-清除算法一样,从根节点开始标记所有可达的对象。
- 第二阶段-压缩:将所有存活的对象依次压缩整理到内存的起始端,按顺序排放。 之后,清理边界外的所有内存空间。
可以看做标记-清除算法执行完后,再执行一次内存碎片整理。
优点:
- 避免了标记-清除算法执行后的【内存碎片】问题。
- 取消了复制算法使用内存减半的代价。
缺点:
- 从效率来说,标记-压缩算法的执行效率低于复制算法。
- 移动对象的同时,需要额外更新该对象地址的引用。比如需要更新局部变量表中某个局部变量指向它地址。
- 移动过程中需要全程暂停用户应用程序,即STW。
30、什么是增量收集算法?
背景:为了避免在垃圾回收过程中,应用软件将处于一种STW状态下,应用程序的所有的线程都会挂起,如果垃圾回收过长会严重影响用户体验或者小系统的稳定性。
基本思想:在标记-清除算法和复制算法的基础之上,如果一次性将所有垃圾进行处理,需要造成系统长时间停顿,那么可以让垃圾收集线程与应用程序线程交替执行。每次收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。
缺点:线程切换和上下文转换的消耗,会造成系统吞吐量下降。
31、什么是分区算法?
核心思想:如果待收集的区域太大,一次完整的GC需要的时间就越长。为了更好的控制GC产生的停顿时间,将一大块内存区域分割成多个小块区域,根据允许停顿的时间值,每次合理的回收若干小区间,而不是整个区域,从而减少STW的时间。例如:

32、说说你对System.gc()的理解?
System.gc()底层调用的是Runtime.getRuntime().gc(),会尝试触发 Full GC,尝试调用但无法保证对垃圾收集器的调用。
33、什么是内存溢出(Out Of Memory)?
内存溢出(OOM:OutOfMemoryError):没有足够的空闲内存,并且垃圾回收集后也无法提供足够的空闲内存。
Java虚拟机的堆内存不够,原因有两点:
- Java虚拟机的堆内存设置不够:比如,可能内存泄漏问题,也有可能堆大小设置不合适,可以通过参数-Xms和参数-Xmx参数来调整。
- 代码中创建大量的大对象,并且长时间不能被垃圾收集器收集。
常见的OOM错误:
- java.lang.OutOfMemoryError: Java heap space
---堆内存溢出。
- java.lang.OutOfMemoryError: PermGen space
---永久代(方法区)溢出。
- java.lang.OutOfMemoryError: Matespace
---元空间(方法区)溢出。
- java.lang.OutOfMemoryError: unable to create new native thread
---程序创建的线程数量已达到上限值。
- java.lang.OutOfMemoryError: Direct buffer memory
---堆外内存分配失败,一般在nio、netty 中都会直接使用对外内存DirectByteBuffer,DirectByteBuffer是通过虚引用(Phantom Reference)来实现堆外内存的释放的。
- java.lang.OutOfMemoryError: reason stack_trace_with_native_method
---本机方法遇到了分配故障:分配失败是在 Java 本地接口(JNI)或本机方法中检测到的,如果抛出此类 OutOfMemoryError 异常,则可能需要使用操作系统的本机实用程序来进一步诊断问题。堆外内存=总内存-堆内存-元空间/持久代内存-线程占用空间。
- java.lang.OutOfMemoryError: GC Overhead limit exceeded
---执行垃圾收集的时间比例太大, 回收内存量太小。默认情况下, 如果GC花费的时间超过 98%, 并且GC回收的内存少于 2%, JVM就会抛出这个错误。
- java.lang.OutOfMemoryError: request size bytes for reason. Out of swap space
---JVM启动参数 -Xmx 指定了最大内存限制。假若JVM使用的内存总量超过可用的物理内存, 操作系统就会用到虚拟内存。异常表明, 交换空间(虚拟内存) 不足,是由于物理内存和交换空间都不足所以导致内存分配失败。
- java.lang.OutOfMemoryError: Required array size too large
---【byte[] data = Files.readAllBytes(path);】Files.readAllBytes 方法最大支持 Integer.MAX_VALUE - 8 大小的文件,也即最大2GB的文件。
- java.lang.OutOfMemoryError: Requested array size exceeds VM limit
---Java平台限制了数组的最大长度。各个版本的具体限制可能稍有不同, 但范围都在1 ~ 21亿之间。异常说明想要创建的数组长度超过限制。
34、什么是内存泄漏(Memory Leak)?
内存泄漏(Memory Leak):严格来说,只有对象不会再被程序用到了,但是GC又不能对其回收的情况。这样刚开始一般不会立刻引起程序崩溃,但是一旦发生内存泄漏内存就会被蚕食而不能回收,最终会导致OOM错误,倒是程序崩溃。
日常开发中可能导致内存泄漏的例子:
- 单例模式下引用外部对象:单例生命周期一般和引用程序是一样长的,所以单例中,如果持有对外部对象的引用的话,引用完后未断开引用,那么外部对象也不会被回收,则会导致内存泄漏。
- 一些提供close()方法的资源未关闭:如文件类、IO流、数据库连接、网络连接在业务结束时必须手动调用close(),否则不会被回收。
35、什么是STW(Stop The World)?
STW(Stop The World):值GC事件发生过程中,会产生应用程序的暂时停顿。即整个应有程序线程都会被暂停。STW中断的线程会在GC完成后恢复运行。
STW事件和采用具体的GC产品无关,所有GC都会有STW事件。这个事件时JVM自动发起和自动完成的。36、什么是垃圾回收的并发与并行?
并行:指多条垃圾收集线程并行工作,但是用户线程任然处于暂停状态。如ParNew、Parallel、Parallel Old等垃圾回收器。
并发:指垃圾收集线程 与 用户线程 CPU交替执行(同时执行)。如:CMS、G1等垃圾回收器。37、什么是安全点?什么是安全区域?
安全点:在程序运行期间,并非所有地方都能立马停下来开始GC,只有特殊的位置才能停顿下来开始GC,这些位置称为“安全点”。
安全点太少:可能会倒是GC等待时间太长, 安全点太多:可能导致运行时的性能问题,GC太多,用户体验不佳。 GC发生时,如何检查所有线程都跑到最近的安全点停下来呢?- 抢先式中断(目前没有虚拟机采用了):抢先中断所有线程,如果还有线程不在安全点,就恢复线程,让线程跑到安全点。【不靠谱】
- 主动式中断:设置一个中断标志,每一个线程跑到安全点后,轮询这个标志。如果中断标志为真,则将线程自己中断挂起。
安全区域:指在一段代码片段中,对象引用关系不会发生变化,在这个区域中任何位置开始GC都是安全的。
38、强引用、软引用、弱引用、虚引用有什么区别?具体都有什么使用场景?
JDK1.2之后,Java对引用分为:强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference) 四种。
引用强度由【强】【软】【弱】【虚】依次减弱。-
强引用(Strong Reference):最常见的引用类型,99%以上都是强引用(如:通过new关键字创建的对象)。【如果对象可达,垃圾收集器是永远不会回收强引用的对象,所以强引用是造成内存泄漏的主要原因之一】
Object obj = new Object();//obj指向Object对象的强引用
-
软引用(Soft Reference):描述一些还要引用,但非必需的对象,只被软应用关联的对象,在系统将要发生内存溢出之前,会把这些对象进行第二次回收,如果这次回收后还没有足够的内存,才会抛出OOM错误。【当内存足够时–不会回收软引用的可达对象;当内存不足够时–会回收软引用的对象】
###场景:高速缓存。有内存就缓存起来加快系统响应速度,没有内存则销毁。//java通过SoftReference操作类来实现---“软引用”SoftReference
-
弱引用(Weak Reference):用来描述非必需的对象。只被弱引用关联的对象只能生存到下一次垃圾收集的发生为止。【当触发垃圾收集时–回收所有弱引用的对象】
###场景:可有可无的缓存数据;WeakHashMap底层使用的是WeakReference< Object>,源码如下public class WeakHashMap
extends AbstractMap implements Map { Entry [] table; private static class Entry extends WeakReference implements Map.Entry { ......} //java为 "弱引用" 提供WeakReference操作类WeakReference
weakReference = new WeakReference (new Object());//通过get()方法可以获取对应的引用对象实例System.out.println(weakReference.get());//====> java.lang.Object@677327b6 -
虚引用(Phantom Reference):是引用最弱的一个。如果一个对象存在虚引用,那么它和没有引用几乎一样。【随时都可能被垃圾收集器回收。为对象设置虚引用关联的唯一目的:跟踪垃圾回收过程。比如当对象被回收时收到一个系统通知】
###场景:由于虚引用可以跟踪对象的回收,因此可以将一些资源释放操作放在虚引用中执行和记录。###虚引用必须和引用队列一起使用,虚引用在创建对象时,必须提供一个引用队列作为参数。###当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象后,将这个虚引用加入引用队列,以通知应用程序对象的回收情况。//引用队列ReferenceQueue referenceQueue = new ReferenceQueue();//java为 "虚引用" 提供PhantomReference操作类PhantomReference phantomReference = new PhantomReference(new Object(), referenceQueue);//"虚引用"通过get()方法获取不到,对应的引用对象实例System.out.println(phantomReference.get());//====> null
39、你知道哪些收集器,及它们之间怎么组合使用的?
HotSpot 虚拟机 GC发展:
- 1999年JDK1.3推出单线程串行的Serial GC;Serial GC的多线程版本:ParNew GC。
- 2001年JDK1.4推出Parallel GC 与 CMS(Concurrent Mark and Sweep) GC。
- 2012年JDK1.7推出G1,Parallel GC成为默认GC。
- 2017年JDK9,G1成为默认的垃圾收集器,
- 2018年JDK11推出ZGC和Epsilon垃圾收集器。
- 2019年OpenJDK12推出Shenandoah GC。
- 2020年3月JDK14 删除CMS,扩展ZGC(实验阶段)在macOs和Windows上的应用。
- 2020年9月JDK15,ZGC转正,之前版本可以使用-XX:+UseZGC命令行参数打开,相信不久的将来它必将成为默认的垃圾回收器。
组合使用关系:
- Serial 与 Serial Old
- Serial 与 CMS
- ParNew 与 Serial Old
- ParNew 与 CMS
- Parallel Scavenge 与 Serial Old
- Parallel Scavenge 与 Parallel Old
- G1
- JDK8(红色虚线),将
Serial与CMS,ParNew与Serial Old组合废弃。 - JDK9(红色虚线),将
Serial与CMS,ParNew与Serial Old组合 移除。 - JDK14(绿色虚线),将
Parallel scavenge与Serial Old组合废弃。 - JDK14(青色虚线),删除
CMS垃圾收集器。
如何查看当前使用的垃圾收集器:可以使用参数 【-XX:+PrintCommandLineFlags】 。
40、说说对Serial GC与Serial Old GC的了解?
Serial GC:采用复制算法、单线程、串行回收、STW的方式回收新生代内存。
Serial Old GC:采用标记-整理算法、单线程、串行回收、STW的方式回收老年代内存。JDK14之前,Serial Old还可以Parallel Scavenge配合一起使用。也可以作为CMS的后备垃圾收集的备选。在HotSpot虚拟机中,使用参数 【-XX:+UseSerialGC】 指定新生代使用Serial GC、老年代使用Serial Old GC进行垃圾回收。
Serial GC与Serial Old GC 现在一般不使用了~~~~

41、说说对Parallel Scavenge GC与 Parallel Old GC的了解?
Parallel Scavenge GC:采用复制算法、并行回收、STW的方式对新生代进行回收。目标是达到一个可控制的吞吐量,及吞吐量优先。主要适合在后台运算而不需要太多交互任务。例如:后台定时任务,中台系统处理下沉数据等。
Parallel Old GC:采用标记-整理算法、并行回收、STW的方式对老年代进行回收。在JDK8中Parallel Scavenge GC与 Parallel Old GC成为默认的GC。
可以使用参数 【-XX:+UseParallelGC】 或者 【-XX:+UseParallelOldGC】 设置,只要设置一个,另一个也会被默认开启(互相匹配)
设置新生代并行收集器的线程数:可以使用参数 【-XX:+ParallelGCThreads】 进行设置:
- 当CPU/核数小于8个,ParallelGCThreads= CPU/核数的数量
- 当CPU/核数大于8个,ParallelGCThreads = 3+(5*CPU/核数的数量)/8
设置拉垃圾收集器最大停顿时间(STW的时间):可以使用参数 【-XX:MaxGCPauseMillis】
设置拉垃圾收集器时间占总时间比例(用于衡量吞吐量的大小) 【-XX:GCTimeRatio】
设置Parallel Scavenge GC自适应调节策略(Eden和Survivor比例自适应调整):可以使用参数 【-XX:UseAdeptiveSizePolicy】
42、说说对低延迟的CMS(Concurrent Mark and Sweep) GC的了解?
CMS(Concurrent Mark and Sweep) GC:采用标记-清除算法,也会STW,是第一款用于老年代的并发收集器,实现了垃圾收集线程与用户线程同时工作,即:关注点是尽可能压缩垃圾收集时,即:STW的时间(低延迟)。
在JDK14中,将CMS垃圾收集器移除。执行过程分主要为4个阶段,如下:
- 起始标记阶段:仅仅只是标记处GC Roots的可达对象。---- 会STW
- 并发标记阶段:从GC Roots开始遍历所有可达对象的过程,耗时长,与用户线程并发执行。---- 不会STW
- 重新标记阶段:修正在并发标记期间,因为用户线程继续运作而呆滞标记产生变动的那一部分对象的标记记录。---- 会STW
- 并发清除阶段:清除标记阶段判断已经死亡的对象,释放内存空间,与用户线程并发执行。---- 不会STW
优点:并发执行,低延迟。
CMS主要弊端:
- 会产生内存碎片:采用标记-清除算法后会产生碎片化,如果遇到为大对象分配的内存不足时,会触发Full GC。 为什么不换成【标记-整理算法】呢? 因为标记-整理期间必须STW,无法实现与用户线程并发执行。
- 无法处理浮动数据:在并发阶段产生新的垃圾对象(浮动数据),CMS将无法对这些对象进行标记(只有下一次执行GC时才能被标记和回收)。最终可能因为这些新产生的垃圾对象没有被及时回收,而触发Full GC。
CMS GC相关参数设置:
- 【-XX:+UseConcMarkSweepGC】:手动指定使用CMS GC。
- 【-XX:+CMSInitiatingOccupancyFraction】:设置堆内存老年代中使用率的阙值。当达到这个阙值时,会触发CMS。JDK5之前默认是68%;JDK6及之后默认92%。
- 【-XX:ParallelGCThreads】:设置CMS GC的线程数量。
43、什么是基于区域化分代式的G1回收器?
G1(Garbage-First 垃圾优先):是一款面向服务端应用,针对配备多核CPU及大容量内存的垃圾收集器。目标是在延迟时间(STW)可控的情况下获得尽可能高的吞吐量。
JDK9及之后版本,将G1成为默认垃圾收集器。既可以回收新生代,也可以回收老年代。取代了JDK8中默认的Parallel+Parallel Old组合,如果在JDK9之前中要是用G1回收器可以使用参数 【-XX:+UseG1GC】。
G1采用分区算法,其特点如下:
- 并行型与并发性:在回收期间,既有多个GC线程同时工作的场景,也有GC线程与应用线程交替并发工作的场景。
- 分代搜集:将堆空间分为若干个区域(Region),这些区域中包含了逻辑上的年轻代和老年代。所以从分代上看,G1依然属于分代型垃圾收集器。
- 空间整合:G1将内存划分为一个个Region。内存回收是以Region为基本单位。Region之前采用复制算法。但整体上也有标记-整理算法,都避免了内存碎片化。
- 可预测的时间停顿模型:设置一个允许的停顿时间,在垃圾收集时,尽量控制在这个时间内完成,即每次根据允许收集的时间,优先回收价值最大的Region,保证了收集的效率。
常用参数:
- 【-XX:+UseG1GC】:开启G1垃圾收集器。
- 【-XX:G1HeapRegionSize】:设置每个Region的大小。值是2的幂,范围1MB~32MB之间。Region的大小 乘 Region的个数等于堆空间大小
- 【-XX:MaxGCPauseMillis】:设置期望达到的最大GC停顿时间指标,JVM会尽力在这时间内完成垃圾收集。默认为200ms。
使用场景:①超过50%的Java堆被活动的数据所占有;②GC停顿时间过长;③对象分配的频率变化很大。
44、说说对G1中的Region的了解有多少?
G1垃圾收集器还增加了一种新的内存区域,叫做Humongous内存区域,主要用于存储大对象,如果超过1.5个Region,就放在H。如果一个H区装不下大对象,那么G1会寻找连续的H区来存储,如果还是没有连续的H区用来存储,就会触发Full GC。
每一个Region内是一个连续存储对象的,当新的对象需要存储时,通过指针碰撞来实现。

45、G1垃圾回收过程主要包括哪三个环节?
- 年轻代GC:当Eden区用尽无法分配空闲内存给新对象时,开始新生代回收过程。新生代收集阶段是一个并行的独占式的收集器。
- 老年代并发标记过程:当堆内存使用到阙值(默认45%)时,开始老年代并发标记过程。阙值可以通过参数设置。
- 混合回收:当标记完成后,老年代Refion和新生代Region一起回收。
- Full GC:在上述环节中,内存就已经消耗完成会触发Full GC。
46、在G1垃圾回收过程中,什么是Remembered Set? 为什么需要有Remembered Set?
我们知道,在一个对象被不同区域引用的问题中,当回收新生代时,由于需要考虑引用问题,不得不对整个老年代也进行遍历筛选出不可达对象。因为新生代回收非常频繁,每一次都要对老年代进行遍历效率是肯定大大降低的。
为了解决这一问题,无论G1还是其他分代收集器,JVM都使用Remembered Set来避免每一次垃圾收集时都全局扫描:- 每一个Region都有一个对应的Remembered Set;
- 每一个Remembered Set都记录与之相对应Region中对象的引用信息。
- 每一次引用类型数据的写操作时,然后检查引用的对象与当前对象是否为同一个Region;
- 如果在不同的Region中,通过CardTable把引用的信息记录在当前的Region对应的Remembered Set中,避免了多线程同步操作Remembered Set带来的延迟时间问题。
- 在垃圾收集时,在原有的GC Roots中再加入Remembered Set,保证了GC时不进行全局扫描的同时,也确保不会遗漏。
47、G1回收过程中,年轻代GC回收的主要过程都有哪些?
- 扫描GC Roots和Remembered Set。
- 更新Remembered Set。
- 处理Remembered Set。
- 通过复制算法第一次回收对象。
- 第二次回收引用:处理如软引用、弱引用、虚引用等。
48、G1回收过程中,并发标记过程主要过程都有哪些?
- 初始标记阶段:充GC Roots开始标记可达对象,这个阶段是STW的,会触发一次young GC。
- 根区域扫描:GC扫描Survivor在老年代引用并且可达的对象,并标记。
- 并发标记:GC线程与用户线程并发执行,若发现区域对象中的而所有对象都是垃圾,那这个区域会被立即回收。再次过程中,还会计算局域中存活对象的比例。
- 再次标记:修正上一次并发过程中的标记结果,这个阶段是STW的。
- 独占清理:按照Region中对象存活的比例进行排序,识别可以混合回收的区域,这个阶段是STW的。
- 并发清理阶段:识别并清理完全空闲的区域。
49、G1回收过程中,混合回收过程主要过程有哪些?
并发标记结束之后,老年代中百分之百是垃圾的区域被回收了。
混合回收算法和年轻代GC算法一样,只是在原来的基础上增加了老年代的区域一并进行回收,具体过程可以参照年轻代回收过程。50、G1回收过程中,Full GC的出现原因可能有哪些?
- 并发处理完成前内存空间已耗尽。
- Evacuation 的时候没有足够的to-space来存放晋升的对象。
51、说说经典垃圾收集器(Serial、Serial Old、ParNew、Parallel、Parallel Old、CMS、G1)的区别?
52、你常用的GC日志分析工具有哪些?
常用的工具有:GCViewer、GCEasy、GCHisto、GCLogViewer、Hpjmeter、等等。
53、说说Open JDK12推出的Shenandoah GC的优缺点?
Shenandoah GC优点:低停顿时间。
Shenandoah GC缺点:高运行负担下的吞吐量下降。54、说说你对ZGC的了解有哪些?
- 2018年JDK11,推出ZGC–伸缩的低延迟垃圾收集器。
- 2020年3月JDK14,扩展ZGC(实验阶段)在macOs和Windows上的应用。
- 2020年9月JDK15,ZGC转正,之前版本可以使用使用【-XX:+UseZGC】命令行参数打开,相信不久的将来它必将成为默认的垃圾收集器。
ZGC与Shenandoah GC目标相似,就是尽可能对吞吐量影响不大的前提下,实现在任意堆内存大小下都可以把垃圾收集的停顿时间限制在10ms以内的低延迟。
ZGC收集器是基于Region内存布局的,采用标记-整理算法。工作过程主要有:①并发标记(需要STW);②并发预备重分配;③并发重分配;④并发重映射。
主打低延迟的三款垃圾收集器的比较如下:
后续更多关于JVM的相关内容会不断更新中……
······
帮助他人,快乐自己,最后,感谢您的阅读! 所以如有纰漏或者建议,还请读者朋友们在评论区不吝指出!…知识是一种宝贵的资源和财富,益发掘,更益分享…
发表评论
最新留言
关于作者
