本文为《深入理解Java虚拟机JVM高级特效与最佳实践(第三版)》一书的摘要总结
HotSpot虚拟机的垃圾收集器:
从JDK9开始,Serial+CMS与ParNew + CMS的组合就不再被支持
Serial 收集器
Serial收集器属于新生代收集器,他是一个单线程工作的收集器,在它进行垃圾收集时,必须暂停所有其他的工作线程,直到它收集完成。
Serial+Serial Old收集器的运行过程:
Serial收集器是HotSpot虚拟机运行在客户端模式下的默认新生代收集器,他是所有收集器里额外内存消耗最少的。
ParNew 收集器
ParNew收集器是Serial收集器的多线程并行版本,除了同时使用多条线程进行垃圾收集之外,其余的行为都更Serial完全一致。
ParNew+Serial Old收集器的工作流程:
ParNew是不少运行在服务端模式下HotSpot虚拟机首选的收集器,其中有一个重要原因就是:除Serial收集器外,它是唯一一个能与CMS收集器配合工作的。
ParNew收集器是激活CMS后(使用:-XX:+UseConcMarkSweepGC选项)的默认新生代收集器,也可以使用-XX:+/-UseParNewGC选项来选择或者禁用它。
但从JDK9开始,Serial+CMS与ParNew + CMS的组合就不再被支持,取消了-XX:+/-UseParNewGC选项,所以ParNew只是CMS的默认新生代收集器,它再也不能与其他收集器搭配使用了。
ParNew默认开启的收集线程数与处理器核心数相同,我们可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。
Parallel Scavenge
Parallel Scavenge收集器是新生代收集器,基于标记-复制算法实现,也是能够并行收集的多线程收集器。但是Parallel Scavenge收集器关注的是 吞吐量(Throughput):
吞吐量 = 运行用户代码时间 ÷(运行用户代码时间+运行垃圾收集时间)。
-
-XX:MaxGCPauseMillis:
最大垃圾收集停顿时间,参数允许一个大于0的毫秒级数;
-
-XX:GCTimeRatio:
吞吐量大小,一个大于0,小于100的整数。如果设置为19,那么最大垃圾收集时间占总时间的比例为:1 ÷ (1+19) = 5%;如果设置为99,那么最大垃圾收集时间占总时间的比例为:1 ÷(1+99) = 1%。
-
-XX:+UseAdaptiveSizePolicy:
这是一个开关参数,当这参数被激活之后,就不需要人工置顶新生代(-Xmn)的大小、Eden与Survivor区的比例(-XX:SuvivorRatio)、晋升老年代对象大小(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会自动调整这些参数,以达到最最合适的停顿时间或者最大的吞吐量。
Serial Old 收集器
Serial Old收集器是Serial收集器的老年代版本,同样是单线程收集器,使用标记-整理算法。主要有两个作用:
- 在JDK5之前的版本中与Parallel收集器搭配使用
- 作为CMS收集器发生失败后的后备预案。
Parallel Old 收集器
Parallel Old 收集器是Parallel Scavenge收集器的老年代版本,在Parallel Old 出现之前,Paral Scavenge收集器就只能与Serial Old收集器配合使用,单线程的老年代收集效率并不高,直到Parallel Old的出现,“吞吐量优先”收集器终于有了名副其实的搭配组合。在注重吞吐量或者处理器资源较为稀缺的场合,都可以优先考虑Parallel Scavenge + Parallel Old收集器的组合,它们的工作流程如下:
CMS 收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,它是基于标记-清除算法的。
CMS收集器的运作工程分为4步:
- 初始标记(CMS initial mark):标记GC Roots 能直接关联到的对象,这一步需要停顿用户线程,但速度很快。
- 并发标记(CMS concurrent ):从GC Roots能直接关联到的对象开始遍历整个对象图,这个过程耗时较长,但是不需要停顿用户线程,与用户线程并发运行。
- 重新标记(CMS Remark):为了修正并发期内标记产生变动的那部分对象的标记记录,这个阶段停顿的时间比初始标记稍长一些,但也比并发标记阶段短。
- 并发清除(CMS concurrent sweep):清理被标记为死亡的对象,由于不需要移动存活对象,所以这一步也是并发的。
-
优点:并发,低停顿
-
缺点
-
对处理器资源非常敏感。在并发阶段,需要占用一部分线程,而导致应用程序编变慢,降低总吞吐量。CMS默认启动的回收线程数是:(处理器核心数量 + 3) ÷ 4
-
CMS收集器无法处理“浮动垃圾”,有可能出现"Concurrent Mode Failure"失败而导致另一次暂停用户线程的Full GC的产生。
在并发清理阶段产生的新的垃圾对象被称为浮动垃圾,这部分来及在这次GC中无法被回收,是有在下次GC是在清理掉。
同时,因为是并发执行,所以必须预留一部分空间供并发收集时的程序运作使用。JDK5的默认设置是CMS收集器在老年代使用了68%的空间后就会被激活,在JDK6,该默认值被修改为92%;
当CMS在运行期间,剩余的内存空间无法满足程序分配给新对象,就会出现一次“并发失败”(Concurrent Mode Failure),这时候就会启动后背方案:使用Serial Old收集器来重新收集老年代的垃圾。
所以我们可以使用-XX:CMSInitiatingOccupancyFraction参数来设置一个合适的CMS触发百分比。
-
因为使用的是标记-清除算法,所以会产生空间碎片
在JDK9之前,开关参数:-XX:+/-UseCMSCompactAtFullCollection默认开启,从JDK9开始被弃用。如果开启该开关,那么在CMS不得不Full GC时,对碎片惊醒合并整理。
-XX:CMSFullGCsBeforeCompaction(JDK9开始弃用):CMS在执行过改参数指定次数的不整理碎片的Full GC之后,下一次进入Full GC之前会先进行碎片整理。
Garbage First 收集器
Garbage First(简称G1)收集器开创了收集器面向局部收集的设计思路和基于Region的内存布局形式。从JDK9开始策划岗位服务端模式下的默认垃圾收集器。
停顿时间模型:在一个长度为M毫秒的时间段内,消耗在垃圾收集上的时间大概率不超过N毫秒。
G1基于Region的堆内存布局实现了停顿时间模型。G1不在坚持固定大小及固定数量的分代区域划分,而是把连续的Java堆划分为 多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden,Survivor空间或者老年代空间。
Region中还有一个特殊的 Humongous 区域,专门用来存放大对象。G1认为大小超过Region容量一半的对象就可以判定为大对象。那些大小超过1个Region容量的对象会被放在多个连续的Region中。
Region的大小可以通过-XX:G1HeapRegionSize参数来设定,取值范围为1MB~32MB,且应为2的N次幂(即:1,2,4,8,16,32)。
G1将一个Region作为垃圾收集单次回收的最小单位,每次回收到的内存空间都是Region容量的整数倍。
G1会去跟踪各个Region里面垃圾堆积的“价值”大小,这里的价值指回收所获得的空间大小和回收所需时间的经验值,然后在后台维护一个优先级列表,每次根据收集停顿时间(-XX:MaxGCPauseMillis,默认为200毫秒),优先处理那些回收价值收益最大的Region。
G1收集器的运作过程大致可分为4个步骤:
-
初始标记(Initial Marking):需要停顿用户线程,但耗时很短。
仅仅标记一下GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行是,你那个正确的在可用的Region中分配新的对象。TAMS(Top at Mark Start):每个Region中都有两个名为TAMS指针,把Region中的一部分空间划分出来用于并发回收过程中的新对象分配,并发回收过程中的新对象分配地址必须在这两个指针的位置之上。G1收集器默认在这个地址以上的对象是被隐式标记过的,即默认它们是存活的,不纳入回收范围。
-
并发标记(Concurrent Marking):并发执行,耗时较长
从GC Roots开始对堆中对象进行可达性分析,找出要回收的对象。对象图扫描完成后,还要重新处理SATB记录下的并发时所有引用变动的对象。SATB(Snapshot At The Begining):在G1并发标记阶段,用户线程可能会删除或者修改对象的引用,G1使用写前屏障(Pre-Write Barrier) ,在每次引用赋值前将旧的引用记录下来,在对象图扫描完之后,以这些旧引用为根进行可达性分析,这样就能避免漏标记。但是如果引用的旧值实际上不再是存活对象,那么它就变成了浮动对象,只有在下次GC时被回收。
-
最终标记(Final Marking):需要停顿用户线程
对用户线程的另一次短暂暂停,用于处理并发阶段结束后仍遗留下俩的那最后少量的SATB记录。并发标记阶段处理SATB记录的同时,用户线程可能继续在生产SATB数据,所以还会有一小部分SATB记录没有处理。
-
筛选回收(Live Data Counting And Evacuation):需要停顿用户线程
更新 Region统计数据,对各个Region的回收价值和成本进行 排序,根据用户所期望的停顿时间 制定回收计划,那后把觉得回收的那一部分Region中的存活对象 复制 到空的Region中,最后 清理 掉整个旧Region的全部空间。
这部分可参考面试官问我G1回收器怎么知道你是什么时候的垃圾?