本节将会介绍一下HotSpot虚拟机中的常用垃圾收集器:Serial,ParNew,Parallel Scavenge,Serial Old,Parallel Old,CMS,G1,会对每个垃圾收集器的概念和常用参数做下介绍。
目录
概述
Serial收集器
ParNew收集器
Parallel Scavenge收集器
Serial Old收集器
Parallel Old收集器
CMS收集器
G1收集器
推荐看下Java关于收集器调优的官方文档:https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/toc.html,这个文档中介绍了JVM的常用垃圾收集器、各个垃圾收集器的常用参数和调优策略等。另外,还有一个plumbr系列博客也不错:Java Garbage Collection handbook | Plumbr – User Experience & Application Performance Monitoring。
上节《判断对象是否可回收、垃圾回收算法》介绍了垃圾回收算法的相关理论,这节将会介绍一下这些算法的实现——垃圾收集器。下图展示了HotSpot虚拟机中的七种垃圾收集器:上面三种作用于新生代,下面三种作用于老年代,G1比较特殊,新生代老年代都可以使用。图中的连线代表两种垃圾收集器可以搭配使用,我们看到基本上都是新生代收集器搭配老年代收集器,只有一个特殊的就是CMS-Serial Old,两者都是老年代收集器,Serial Old是CMS的后备方案。
这个关系不是一成不变的,由于维护和兼容性测试的成本,在JDK 8时将Serial+CMS、
ParNew+Serial Old这两个组合声明为废弃(JEP 173),并在JDK 9中完全取消了这些组合的支持(JEP 214)。
没有完美的收集器,我们需要根据业务场景选择合适的收集器进行搭配使用。下面对每个收集器做下单独介绍,CMS和G1是介绍的重点。
我们下面讨论垃圾收集器时,并行与并发的含义如下:
常用收集器分类如下:
我们介绍垃圾收集器时会用到STW,其代表的是"Stop The World"。
Serial是最早的垃圾收集器,用于新生代的垃圾回收,采用复制算法,它是一款单线程的垃圾收集器,垃圾回收时会暂停所有工作线程——"Stop The World"!它是虚拟机运行在client模式下默认的新生代垃圾收集器,适用于内存不太大的桌面应用。
ParNew是Serial收集器的多线程版本,也是用于新生代垃圾回收,采用复制算法,除了使用多个线程进行垃圾回收之外,其余都与Serial收集器完全一样。它是虚拟机运行在server模式下的首选的新生代垃圾收集器,其中有一个原因是除了Serial收集器外,只有它能与CMS(Cocurrent Mark Sweep)搭配使用,目前CMS+ParNew的搭配组合用的的挺多的。当老年代选用CMS收集器后默认的新生代收集器就是ParNew,也可以使用-XX:+UseParNewGC选项强制指定它。
ParNew收集器在多CPU的情况下更能发挥出其优势,它默认开启的垃圾收集线程数与CPU数量相同,可以使用-XX:ParallelGCThreads=N(CPU> 8 N=5/8; CPU<8 N=CPU,并行垃圾收集器都可以使用此参数)来限制垃圾收集的线程数。
ParNew/Serial Old 收集器的工作过程如下:
Parallel Scavenge收集器是一个新生代收集器,采用复制算法,是一个并行的多线程收集器。它的目标是达到一个可控制的吞吐量(Throughput),因此被称为"吞吐量优先"的垃圾收集器,适用于后台运算而不需要太多交互的任务(虚拟机在server模式下默认使用吞吐量优先收集器)。吞吐量就是CPU用于运行用户代码的时间与CPU运行总时间的比值,吞吐量=运行用户代码时间 / (运行用户代码时间+垃圾收集时间)。
Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量:-XX:MaxGCPauseMillis、-XX:GCTimeRatio
MaxGCPauseMillis参数允许的值是一个大于0的毫秒数,收集器将尽可能地保证内存回收花费的时间不超过设定值。把这个参数设小的话会降低吞吐量,因为要把新生代空间调小,导致GC频率增加。
GCTimeRatio参数的值应当是一个大于0且小于100的整数,也就是垃圾收集时间占总时间的比率,相当于是吞吐量的倒数。如果把此参数设置为19,那允许的最大GC时间就占总时间的5%(即1/(1+19)),默认值为99,就是允许最大1%(即1/(1+99))的垃圾收集时间。
Parallel Scavenge收集器还有一个开关参数-XX:+UseAdaptiveSizePolicy,当这个参数打开之后,就不需要手工指定新生代的大小(-xmn)、Eden与Survtvor区的比例(-XX:SurvrvorRatio)、晋升老年代对象年龄(-XX.PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞量,这种调节方式称为GC自适应的调节策略(GC Ergonormcs)。自适应调节策略也是ParallelScavenge收集器与ParNew收集器的一个重要区别。
关于堆内存的自适应调节有如下三个参数:调整堆是按照每次20%增长,按照每次5%收缩
Serial Old收集器是Serial收集器的老年代版本,也是单线程的,其采用"标记-整理"算法,主要用于虚拟机的client模式下。其还有一个重要作用是作为CMS收集器的后备方案,在并发收集发生Concurrent Mode Failure时使用。Serial/Serial Old收集器的工作工程如下:
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记一整理”算法。其只能与Parallel Scavenge收集器搭配使用,两者是"吞吐量优先"的垃圾收集器组合。在注重吞吐量及CPU资源敏感的场合,可以优先考虑 Parallel Old 加Parallel Scavenge 的组合,两者工作过程如下:
CMS(Concurrent Mark Sweep)收集器用于老年代的内存回收,采用"标记-清除"算法,它以获取最短回收停顿时间为目标,适合用于互联网B/S系统的服务端上,使用 +UseConcMarkSweepGC 参数启用CMS+ParNew的收集器组合,它的工作过程分为六个步骤:
1、初始标记(CMS initial mark)——STW:这个阶段是标记从GcRoots直接可达的对象,速度很快 。这个过程在JDK7之前单线程,JDK8之后并行,可以通过参数CMSParallelInitialMarkEnabled调整。
2、并发标记(CMS concurrent mark):并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行。在并发标记过程中,应用线程还在跑,因此会导致有些对象会从新生代晋升到老年代、有些老年代的对象引用会被改变、有些对象会直接分配到老年代,这些受到影响的老年代对象所在的card(card table)会被标记为dirty,用于重新标记阶段扫描。
3、并发预清理(CMS concurrent preclean):此阶段标记从新生代晋升的对象、新分配到老年代的对象以及在并发阶段被修改了的对象,因此,这个阶段也需要扫描新生代+老年代。此阶段做的事情跟重新标记类似,目的是为了让重新标记阶段的STW尽可能短。
因为上面说了,并发预清理阶段需要扫描新生代,所以需要快速知道新生代中存活的对象,这时如果能进行一次Minor GC就比较好了。CMS 有两个参数:CMSScheduleRemarkEdenSizeThreshold、CMSScheduleRemarkEdenPenetration,默认值分别是2M、50%。两个参数组合起来的意思是并发预清理后,eden空间使用超过2M时启动可中断的并发预清理(CMS-concurrent-abortable-preclean),直到eden空间使用率达到50%时中断,进入remark(重新标记)阶段。
可终止的预清理需要设置一个时间,CMS提供了一个参数CMSMaxAbortablePrecleanTime ,默认为5S。只要到了5S,不管发没发生Minor GC,有没有到CMSScheduleRemardEdenPenetration都会中止此阶段,进入remark(重新标记)阶段。
如果在5S内还是没有执行Minor GC怎么办?CMS提供CMSScavengeBeforeRemark参数,使remark前强制进行一次Minor GC。这样做好的一面是减少了remark阶段的停顿时间,坏的一面是Minor GC后紧跟着一个remark pause。如此一来,停顿时间也比较久。
4、重新标记(CMS remark)——STW:而重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
5、并发清除(CMS concurrent sweep):用户线程被重新激活,同时清理那些无效的对象。
6、并发重置(CMS concurrent reset):CMS清除内部状态,为下次回收做准备。
由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。其工作示意图如下:
CMS的优点是并发收集、低停顿,另外它也有下面3个缺点:
另外,CMS收集器还有如下常用参数:
G1(Garbage-First)收集器是JDK1.7中新出的一款面向服务端应用的垃圾收集器,目的是想替换掉CMS收集器,用参数 -XX:+UseG1GC开启,与其它GC收集器相比,G1具备如下特点:
G1之前的收集器收集的范围都是整个新生代或者老年代,G1跟这些收集器有很大区别。G1收集器将整个Java堆划分为多个大小相等的独立区域(Region),虽然还有新生代、老年代的概念,但是新生代、老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。Region中还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过了一个
Region容量一半的对象即可判定为大对象。每个Region的大小可以通过参数-XX:G1HeapRegionSize设定,取值范围为1MB~32MB,且应为2的N次幂。而对于那些超过了整个Region容量的超级大对象,将会被存放在N个连续的Humongous Region之中,G1的大多数行为都把Humongous Region作为老年代的一部分来进行看待。
G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾回收的价值大小(回收所获得的空间大小以及回收所有时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这也就是Garbage一First名称的来由)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。
在G1收集器中,Region之间的对象引用以及其他收集器中的新生代与老年代之问的对象引用,虚拟机都是使Remembered Set来避免全堆扫描的。G1中每个Region都有一个与之对应的Rcmembered Set,虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中(在分代的例子中就是检查是否老年代中的对象引用了新生代中的对象),如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set之中。当进行内存回收时,在GC根节点的枚举范围中加人Remembered Set即可保证不对全堆扫描也
不会有遗漏。
如果不计算维护Remembered Set的操作,G1收集器的运作大致可划分为以下几个步骤:
初始标记(Initial Marking)
并发标记(Concurrent Marking)
最终标记(Final Marking)
筛选回收(Live Data Counting and Evacuation)
G1的前几个步骤的运作过程和CMS有很相似。
初始标记阶段仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这阶段需要停顿线程,但耗时很短。
并发标记阶段是从GC Root开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。
最终标记阶段则是为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但是可并行执行。
筛选回收阶段负责更新Region的统计数据,对各个Region的回首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。
G1收集器除了并发标记阶段外,其余阶段都是要暂停用户线程的。
注意:G1收集器没有full GC,而是Mixed GC,Mixed GC会回收young 区和部分old区。
G1关于Mixed GC调优常用参数:
其他常用参数:
常用垃圾收集器参数:
参数 | 描述 |
---|---|
UseSerialGC | 虚拟机运行在Client模式下的默认值,打开此开关后,使用Serial + Serial Old的收集器组合进行内存回收 |
UseParNewGC | 打开此开关后,使用ParNew + Serial Old的收集器组合进行内存回收 |
UseConcMarkSweepGC | 打开此开关后,使用ParNew+ CMS + Serial Old的收集器组合进行内存回收。Serial Old收集器将作为CMS收集器出现Concurrent Mode Failure失败后的后备收集器使用 |
UseParallelGC | 虚拟机运行在Server模式下的默认值,打开此开关后,使用Parallel Scavenge + Serial Old (PS Mark Sweep)的收集器组合进行内存回收 |
UserParallelOldGC | 打开此开关后,使用Parallel Scavenge + Parallel Old的收集器组合进行内存回收 |
SurvivorRatio | 新生代中Eden区域与Survivor区域的容量比值,默认为8,代表Eden: Survivor = 8:1 |
PretenureSizeThreshold | 直接晋升到老年代的对象大小,设置这个参数后,大于这个参数的对象将直接在老年代分配 |
MaxTenuringThreshold | 晋升到老年代的对象年龄。每个对象在坚持过一次Minor GC之后,年龄就增加1,当超过这个参数值时就进入老年代 |
UseAdaptiveSizePolicy | 动态调整Java堆中各个区域的大小以及进入老年代的年龄 |
HandlePromotionFailure | 是否允许分配担保失败,即老年代的剩余空间不足以应付新生代的整个Eden和Survivor区的所有对象都存活的极端情况 |
ParallelGCThreads | 设置并行GC时进行内存回收的线程数 |
GCTimeRatio | GC时间占总时间的比率,默认值是99, 即允许1%的GC时间。仅在使用Parallel Scavenge收集器时生效 |
MaxGCPauseMillis | 设置GC的最大停顿时间。仅在使用Parallel Scavenge收集器时生效 |
CMSInitiatingOccupancyFraction | 设置CMS收集器在老年代时间被使用多少后触发垃圾收集。默认值为68%,仅在使用CMS收集器时生效 |
UseCMSCompactAtFullCollection | 设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片整理。仅在使用CMS收集器时生效 |
CMSFullGCsBeforeCompaction | 设置CMS收集器在进行若干次垃圾收集后再启动一次内存碎片整理,仅在使用CMS收集器时生效 |
UseG1GC | 开启G1垃圾收集器 |
参考:《深入理解Java虚拟机第二版》
CMS垃圾收集器参考:详解CMS垃圾回收机制 - Master3513 - 博客园