Java主流垃圾收集器(GC)总结

1. Serial收集器

首先Serial是一种单线程的、独占式的收集器,在执行垃圾回收时,所有的Java应用程序线程将会暂停,等待回收完成,这种造成所有Java应用线程暂停的情况,就是经常提到的STW(stop the world)。

Serial收集器作用于年轻代,采用的是复制算法来进行回收。在早期的低版本JDK中,由于受制于当时的CPU速度(没有那时候CPU速度没有现在这么快),在单个CPU的情况下,Serial收集器基本上是年轻代回收唯一的选择,并且目前Serial收集器是HotSpot中Client模式下的默认收集器。

在老年代,Serial提供了用于执行老年代回收的Serial Old收集器,同样是串行回收,并且需要STW,算法方面使用了标记-压缩算法,而不是复制算法。如果在老年代使用复制算法,代价比较大,一方面要复制老年代大量的存活对象,另一方面只能使用一半的内存空间。

如果JVM只有单个CPU可以使用,那么可以考虑采用使用Serial收集器和Serial Old收集器,并且运行在client模式下,这样可以避免CPU进行频繁切换而带来额外的开销。由于STW的独占式回收,它会降低应用程序的吞吐量,这一点在老年代回收时尤为明显(老年代的垃圾回收通常比年轻代需要更多的时间),但是回收质量还是不错的。相比于下文中的ParNew收集器,如果是多CPU环境,使用ParNew收集器可以充分利用硬件的优势,提高应用程序吞吐量。

显示加上JVM选项:-XX:+UseSerialGC,年轻代、老年代的回收均会使用此串行回收器。

老年代串行回收器也可以搭配其他年轻代收集器使用,例如,指定-XX:+UseParNewGC,年轻代将会使用并行收集器,老年代会依旧使用串行回收器。

2. ParNew收集器

ParNew收集器是Serial收集器的多线程版本,也就是说,它在回收时采用的是并行回收,除此之外没有任何区别。它的年轻代同样使用复制算法,同样需要STW。在多CPU、多核心的情况下,可以达到比Serial收集器更高回收效率,相反,在单个CPU的情况下,使用Serial收集器更加高效。

显示加上JVM选项:-XX:+UseParNewGC,年轻代将会使用并行收集器,老年代依旧使用串行收集器。

3. Parallel收集器

在HotSpot年轻代中,除了ParNew收集器外,Parallel收集器同样采用了复制算法、并行回收、STW机制。和ParNew收集器不同的是,我们可以人为控制吞吐量的大小,它是一种吞吐量优先的垃圾收集器。通过显示加上选项:-XX:GCTimeRatio 可以设置内存回收的时间所占JVM运行总时间的比例(通过控制GC频率来达到),公式为 1 / (N+1) 默认值为99,也就是说将只有1%的时间用于垃圾回收。除此之外,Parallel收集器还提供 -XX:MaxGCPauseMills 选项来控制执行垃圾回收时,STW的时间阈值,如果指定了该选项,JVM会尽可能在设定的时间范围内完成内存的回收。

值得注意的地方是,-XX:GCTimeRatio选项控制的是程序的吞吐量,-XX:MaxGCPauseMills 选项控制的是STW时程序的延时。高吞吐量、低延时本身就是相互矛盾的,如果以吞吐量优先,JVM必须要降低内存回收的频率,但是这样就会造成每次GC需要更长的暂停时间来执行内存回收;如果以低延时有先,JVM为了减少每次暂停的时间,势必要增加GC的频率并且缩减年轻代的内存空间大小(-XX:MaxGCPauseMills 值的改变会也会使年轻代的空间大小自动调整),造成吞吐量的降低。所以在设置这两个值的时候,要控制在一个折中的范围内。Parallel手机器还提供了一个选项 -XX:UseAdaptiveSizePolicy 用于设置GC的自动分代大小调节策略。设置了该选项后,意味着开发人员不需要再显示设置年轻代的一些细节参数,JVM会根据自身的情况动态调整这些参数。这些参数仅限于:虚拟机的最大堆、目标吞吐量(GCTimeRatio)、停顿时间(MaxGCPauseMills)。

Parallel收集器同样提供了用于老年代回收的Parallel Old收集器,它采用了标记-压缩算法,基本并行回收、STW机制。在吞吐量优先的程序中,Parallel收集器和Parallel Old收集器的组合在server模式下有很不错的运行效率。

添加选项:-XX:+UseParallelGC 可以指定年轻代使用并行回收收集器,老年代使用串行收集器。

添加选项:-XX:+UseParallelOldGC 可以指定年轻代、老年代都使用并行回收收集器。

添加选项:-XX:ParallelGCThreads 可以设置垃圾回收时的线程数量。

4. CMS收集器

通过选项-XX:+UseConcMarkSweepGC,指定年轻代使用并行收集器(ParNew收集器),老年代使用CMS。

CMS全称是:Concurrent Mark Sweep。在吞吐量优先的情景下,使用Parallel收集器是不错的选择,如果是对系统响应速度比较高的系统,可以采用CMS收集器。它是一款老年代的收集器,低延迟,采用的是标记-清除算法(会产生内存空间的碎片),同样,也会因为STW的机制造成短暂的暂停。

CMS的执行可以分为四个阶段:初始标记(Initial Mark)、并发标记(Concurrent Mark)、再次标记(Remark)、并发清除(Concurrent Sweep)。

在初始标记阶段,CMS收集器会器标记内存中那些被根对象集合所连接的目标对象是否可达,在这个阶段,所有的Java应用程序线程会因为STW而出现短暂的暂停,一旦标记完成,就会恢复之前被暂停的应用程序线程。然后进入并发标记阶段,这个阶段会将之前标记为不可达的对象标记为垃圾对象,此阶段应用程序线程和垃圾回收线程可以同时运行,也正是因为这个因素,此阶段无法有效确保之前被标记为垃圾的无用对象的引用关系遭到更改。为了解决这个问题,CMS会进入再次标记阶段,此阶段会因为STW机制而再次出现短暂的暂停,以确保这些垃圾对象能够被成功且正确地标记。最后进入并发清除阶段,执行内存回收,释放无用对象所占用的内存空间。

目前所有的垃圾收集器都做不到完全不需要STW,只是尽可能去缩短这个时间。

由于采用的是标记-清除算法,因此会存在并积累内存碎片,CMS收集器提供选项-XX:+UseCMS-CompactAtFullCollectioin来指定执行完Full GC后是否对内存空间进行压缩整理,以避免内存碎片的产生。对内存的整理同样需要STW,所以将会使停顿时间变长。CMS还有一个选项-XX:CMSFullGCs-BeforeCompaction,用于设置在执行多少次Full GC后对内存空间进行整理。

在并发标记阶段,由于应用程序的线程和垃圾收集线程是同时云心或者交叉运行的,如果在这个阶段产生新的垃圾对象,CMS是无法对这些垃圾进行标记的,最终会导致这些垃圾对象没有被及时回收,只能在下一次执行GC的时候来处理它们。

-XX:CMSInitiatingOccupanyFraction选项可以设置当老年代中的内存使用率达到多少百分比时执行内存回收(低版本的JDK默认值为68%,JDK6以上默认值为92%),这里的内存回收范围仅限于老年代,而非整个堆空间(严格来说,Full GC的回收范围几乎覆盖整个堆空间),因此通过该选项便可以有效降低Full GC的执行次数。之所有说降低,而不是消除,是因为如果一旦在CMS执行过程中出现“Promotion Failed”或“Concurrent Mode Failure”时,仍然有可能会触发Full GC操作。

CMS收集器在进行主要的工作时,和应用程序并发执行,相互抢占CPU,所以在CMS执行期间会对应用程序吞吐量造成一定的影响。CMS默认启动的线程数是 (ParallelGCThreads + 3) / 4,ParallelGCThreads是年轻代并行收集器的线程数,也可以通过-XX:ParallelCMSThreads 手工设定CMS的线程数量。当资源比较紧张时,收到CMS收集器线程的影响,应用程序的性能可能比较糟糕。

由于CMS收集器不是独占式的,在CMS回收过程中,应用程序仍然在工作,在这期间又会不断产生垃圾,这些新生成的垃圾需要在下一次回收时才能处理,因此我们要确保在CMS回收过程中,应用程序有足够的内存可以使用。因此CMS收集器不会等待堆内存饱和时才进行垃圾回收,而是达到一定阈值时便开始进行回收。这个阈值可以通过上文提到的-XX:CMSInitiatingOccupanyFraction选项来指定。如果很不幸,如果回收过程中出现了内存不足的情况,此时CMS回收将会失败,JVM将启动老年代串行垃圾收集器进行垃圾回收,这会导致应用程序完全中断,直到垃圾收集完成,这期间停顿的时间可能比较长。如果我们的应用程序内存增长缓慢,可以设置一个比较大的阈值,这样可以减少CMS触发的老年代回收的频率,可以较为明显改善应用程序性能;如果增长较快,则应该设置一个较小的阈值,来减少触发老年代串行收集器的频率。

使用选项-XX:+UseConcMarkSweepGC时,年轻代的并行收集器工作线程数量可以使用-XX:ParallelGCThreads指定,一般最好和CPU数量相当,过多的线程数会影响垃圾收集器性能。默认情况下,当CPU数量小于8个,ParallelGcThreads的值等于CPU数量,大于8个ParallelGCThreads的值等于 3 + ( 5 * CPU_COUNT / 8 )。

5. Garbage First(G1) GC

选项:-XX:+UseG1GC指定使用G1 GC

G1 GC采用了和之前提到的几种GC不同的方式,从而解决了他们很多的缺陷。

它将JVM的堆内存切分为多个区间(Region),从而避免了很多GC操作在整个java堆或者整个年轻代进行。这些Region是比较随意的,不需要指定哪些属于年轻代,哪些属于年老代,且年轻代和年老代也不需要一大块连续的内存,只是说它们由一系列Region组成而已,而且一个Region到底属于年轻代还是年老代也是不固定的,例如,开始的时候RegionA被分配给了年轻代,经历过一次年轻代回收后,RegionA被放回了空闲Region队列,然后下一次分配的时候可能分配给了一个老年代的对象去使用。

G1的年轻代收集是一个并行的独占收集器,和其他HotSpot一样,当一个年轻代收集进行时,整个年轻代会被回收,所有应用程序的线程会被中断。和年轻代不同的是,老年代的G1收集器只需要扫描/回收一小部分老年代的Region就可以了,而且这个老年代Region是和年轻代一起被回收的。

在CMS GC里,我们只需要单独判断老年代的占有率来判断是否启动CMS收集器,而在G1 GC中,判断的是整个Java堆内部老年代的占有率。通过选项-XX:InitiatingHeapOccupancyPercent(默认是Java堆空间的45%)可以设置堆占有率阈值,在一次年轻代GC结束之后,G1会评估剩余的对象是否达到了45%这个阈值,如果达到了,会开始一次老年代回收的动作。这个值越小,GC约频繁,反之,值越大,可以让应用程序执行时间更长,但是如果内存消耗很快,也不建议设置太大。

当达到-XX:InitiatingHeapOccupancyPercent阈值,GC会自动开始一个并行标记循环。并行标记循环可以分为五个阶段,分别为初始标记阶段、根区间扫描阶段、并行标记阶段、重标记阶段、清除阶段,其中初始标记阶段和重标记阶段是独占式的,会引起Java应用程序线程的暂停。初始标记阶段可以和年轻代GC一起运行,一旦初始化标记阶段结束,并行多线程的标记阶段就开始启动区标记所有老年代还存活的对象,当这个标记阶段运行结束,为了再次确认是否有逃过扫描的对象,再次启动一个独占式的再次标记阶段,尝试标记所有遗漏的对象。如果一旦在再次标记阶段发现任何完全空闲(没有任何存活对象)的Region,G1 GC会立即将它放入空闲区间对列,而不是将它放入排队序列,等待一个混合垃圾回收暂停阶段的回收。

关于混合回收,当越来越多的对象从年轻代进入到了老年代,再加上进入单独区域的大对象区间,整个老年代或者说整个Java兑取的负荷量/保有量也在逐渐上升,为了应对这个局面,也为了避免后续出现堆空间不足的情况发生,JVM需要去初始化垃圾回收,这个回收不仅仅是针对年轻代,还需要能够额外增加一些针对老年代、大对象区间的回收,这就是混合回收。一次混合回收循环只有在超过-XX:InitiatingHeapOccupancyPercent阈值时才会触发,并且必须发生在一次并行标记循环之后。在一些选项的帮助下,可以为一次混合回收暂停设定多次混合回收暂停,通过-XX:G1MixedGCCountTarget和-XX:G1HeapWastePercent选项来决定一次混合回收内部的混合回收发生的次数,关于这两个选项的描述见下文。

一次混合回收循环内部的混合回收次数可以被每一次混合回收暂停阶段执行的最小老年代区间集合数量以及堆空闲百分比所控制。即选项-XX:G1MixedGCCountTarget和-XX:G1HeapWastePercent,关于他们的介绍见下文。

关于回收集合(CSet),任何一次垃圾回收都会释放CSet里面所有的区间,一个CSet由一系列等待回收的区间所组成。在一次垃圾回收过程中,这些回收候选区间的存活对象会被整体评估,并且在回收结束后这些区间会被加入到空闲区间队列。在一次年轻代回收过程中,CSet只包含年轻代区间,而在一个混合回收过程中,CSet会在年轻代区间基础上再包含一些老年代区间。至于选择哪些老年代区间进行回收,取决于对这些区间的评估,标记有较多垃圾的老年代区间会被回收。G1 GC提供了两个选项用于帮助选择进入CSet的候选老年代区间,分别为:-XX:G1MixedGCLiveThresholdPercent和-XX:G1OldCSetRegionThresholdPercent,具体介绍见下文。

关于Region,G1把整个Java堆划分为若干个区间(Region),每个Region大小为2的倍数,范围在1MB~32MB之间。所有的Region都是一样的大小,并且他们的大小在JVM生命周期内不会被改变。一开始,在进行对象分配的时候,G1会从可用Region队列里挑出一个Region设置为Eden Region(供Eden区使用),一个Eden Region里面填满对象以后,又会从可用Region队列里再跳一个,当所有的Eden Region被填满时,一个年轻代GC收集就会开始执行。

关于大对象,如果一个对象的大小超过一个Region大小的50%以上,它就被认为是一个大对象。G1会挑选出一组连续可用的Region,相加后只要能确保总大小可以存放这个大对象,就会分配给这个大对象,如果没有满足条件的连续的可用Region,将会触发一次Full GC,用于压缩Java堆。大对象的Region属于老年代的一部分。

下面列出一些G1比较重要的选项:

-XX:ConcGCThreads:设置与Java应用程序线程并行执行的GC线程数量,如果设置过大,会造成Java应用程序可使用的CPU资源的减少,如果过小,会增加GC并行循环的执行时间,从而减少Java应用程序的运行时间(因为独占期时间拉长了)。

-XX:G1HeapRegionSize:这个选项可以设置每个Region的大小,默认为堆大小的1/2000。

-XX:G1MixedGCCountTarget:这个选项可以设置一个并行循环之后启动多少个混合GC,默认是8个。设置一个比较大的值可以让G1 GC在老年代Region多花一些时间,如果一个混合GC的停顿时间很长,说明它要做的事情比较多,可以增大这个值,但是如果过大,则会造成并行循环等待混合GC完成的时间增加。每一次混合回收暂停的最小老年代区间数目 = 混合收集循环确认的候选老年代区间总数 / G1MixedGCCountTarget。

-XX:G1HeapWastePercent: 用于控制G1 GC不会回收的空闲内存比例。G1 GC在回收过程中会回收所有Region的内存,并持续地做这个工作,直到空闲内存比例达到设置的这个值。默认值为整个堆空间的5%。

-XX:GCTimeRatio:这个选项可以指定Java应用线程花费的时间与GC线程花费时间的比率。

-XX:MaxGCPauseMills:设置G1的目标停顿时间,G1 GC会尽可能确保年轻代的回收时间控制在这个值以内。默认200ms。

-XX:MinHeapFreeRatio:默认为堆内存的40%,当空闲堆内存的大小小于这个值,我们需要判断-Xms和-Xmx,如果这两个值不同,并且目的堆的总大小小于-Xmx,就可以进行扩展堆内存。

-XX:MaxHeapFreeRatio:默认为堆内存的70%,这个选项和上面那个正好相反,当空闲比率达到这个值时,G1 GC会自动减少堆内存大小,并且需要判断-Xms和-Xmx来决定是否能缩小。

-XX:G1MixedGCLiveThresholdPercent:默认值为一个G1 GC区间的85%,这个值是一个存活对象的阈值,并且起到了从和混合回收的CSet里排除一些老年代区间的作用,可以理解为G1 GC限制CSet仅包含低于这个阈值的老年代区间,这个选项有助于减少垃圾回收过程中拷贝对象所消耗的时间。

-XX:G1OldCSetRegionThresholdPercent:默认值为整个Java堆区的10%,这个值设置了可以被用于一次混合回收暂停所回收的最大老年代区间数量。可以理解为限制了进入CSet的老年代Region的最大大小。

 

 

参考文献:

[深入理解JVM&G1 GC].周明耀.2017.6

你可能感兴趣的:(JVM)