【JVM实战】GC回收器详解

在Java虚拟机中,GC回收器不仅仅只有一种,在哪一块内存区域以及场景使用什么垃圾回收器,对于我们编写的程序的性能都有着至关重要的影响,因为,如果我们熟悉每一个GC回收器的运行机制就可以给我们的程序的性能带来很大的提升,本篇文章中,我们来探究一下每一种GC回收器的结构原理。

首先,在JVM中,总共有以下几种GC回收器:

  • 新生代串行回收器
  • 老年代串行回收器
  • 新生代ParNew回收器
  • 新生代ParallelGC回收器
  • 老年代ParallelOldGC回收器
  • CMS回收器
  • G1回收器

新生代串行回收器

串行回收器是所有垃圾回收器中最古老的一种,也是最基本的垃圾回收器之一,串行回收器主要有以下两个特点:

  1. 它仅仅使用单线程进行垃圾回收。
  2. 它是独占式的垃圾回收。
    在串行收集器进行垃圾回收时,Java应用程序中的线程都需要暂停,等待垃圾回收的完成。因此在实时性要求比较高的应用场景中,这种现象往往是不能被接受的。
    从回收器的名字上可以看出,它应用在新生代,使用复制算法,当使用参数-XX:+UseSerialGC可以指定使用新生代串行收集器和老年代串行收集器,当虚拟机在Client模式下运行时,它是默认的垃圾收集器。

老年代串行回收器

它和上面的回收器一样,不同的是它使用在老年代中,使用标记-压缩算法,因此,在堆内存空间较大的应用程序中,一旦老年代串行收集器启动,应用程序很可能会因此停顿较长的时间。

它一般和其他的新生代回收器配合使用,同时它也作为CMS回收器的备用回收器,当CMS回收器回收不及时导致内存不够用的时候就会启用该回收器来回收。

如果要启用老年代串行回收器,可以尝试使用下面参数:

  • -XX:+UseSerialGC:新生代,老年代都使用串行回收器。
  • -XX:+UseParNewGC:新生代使用ParNew回收器,老年代使用串行回收器。
  • -XX:+UseParallelGC:新生代使用ParallelGC回收器,老年代使用串行收集器。

新生代ParNew回收器

ParNew回收器是一个工作在新生代的垃圾回收器,它只是简单地将串行回收器多线程化,它的回收策略,算法以及参数和新生代串行回收器一样,只是多线程发挥了多核CPU的作用,提高了回收的效率,但是在单CPU下,反而多线程的切换的压力不如单线程性能好。

开启ParNew回收器可以使用下面的参数:

  • -XX:+UseParNewGC:新生代使用ParNew回收器,老年代使用串行回收器。
  • -XX:+UseConcMarkSweepGC:新生代使用ParNew回收器,老年代使用CMS。

ParNew回收器工作时的线程数量可以使用-XX:ParallelGCThreads参数指定,一般,最好与CPU数量相当,避免过多的线程数,影响垃圾收集性能。在默认情况下,当CPU数量小于8个时,ParallelGCThread的值等于CPU数量,当CPU数量大于8个时,ParallelGCThreads的值等于3+(5*CPU_Count)/8。

新生代ParallelGC回收器

新生代ParallelGC回收器也是使用复制算法的回收器,虽然它和ParNew回收器一样,是多线程,独占式的回收器,但是它有一个重要的特点:非常关注系统的吞吐量。

可以使用下面的参数来启用该回收器:

  • -XX:+UseParallelGC:新生代使用ParallelGC回收器,老年代使用串行回收器。
  • -XX:+UseParallelOldGC:新生代使用ParallelGC回收器,老年代使用ParallelOldGC回收器。

ParallelGC回收器有两个重要的参数来控制系统的吞吐量:

  1. -XX:MaxGCPauseMillis:设置最大停顿时间。它的值是一个大于0的整数,ParallelGC在工作时,会调整Java堆大小或者其他一些参数,尽可能地把停顿时间控制在该参数以内。如果读者希望减少停顿时间,而把这个值设的很小,为了达到预期的停顿时间,虚拟机可能会使用一个较小的堆,而这种导致垃圾回收变得很频繁,从而增加了垃圾回收总时间,降低了吞吐量。
  2. -XX:GCTimeRatio:设置吞吐量大小,它的值是一个0到100之间的整数,假设GCTimeRatio的值为n,那么系统将花费不超过1/(1+n)的时间用于垃圾收集。

除了关注系统的吞吐量,ParallelGC回收器与ParNew回收器另一个不同之处在于它还支持一种自适应的GC调节策略,使用-XX:+UseAdaptiveSizePolicy可以打开自适应GC策略。

老年代ParallelOldGC回收器

它和新生代ParallelGC回收器一样,它也是一种关注吞吐量的回收器,不过它使用在老年代中,使用标记-压缩算法,参数-XX:ParallelGCThreads用于设置垃圾回收时的线程数量。

CMS回收器

与ParallelGC和ParallelOldGC不同,CMS回收器主要关注于系统停顿时间,CMS是Concurrent Mark Sweep的缩写,意为并发标记清除,所以它使用标记-清除算法,是一个使用多线程并行回收的垃圾回收器。

CMS的工作过程分为:初始标记,并发标记,预清理,重新标记,并发清除和并发重置。其中初始标记和重新标记是独占系统资源的,而预清理,并发标记,并发清理和并发重置是可以和用户线程一起执行的。因此,从整体上来说,CMS回收器不是独占式的,它可以在应用程序运行过程中进行垃圾回收。
【JVM实战】GC回收器详解_第1张图片
初始标记,并发标记,重新标记都是为了标记出需要回收的对象,并发清理则是在标记完成后,正式回收垃圾对象,并发重置是指在垃圾回收完成后,重新初始化CMS数据结构和数据,为下一次垃圾回收做好准备。并发标记、并发清理和并发重置都是可以和应用程序线程一起执行的。

在整个CMS回收的过程中,默认情况下,在并发标记之后,会有一个预清理的操作,可以通过开关-XX:-CMSPrecleaningEnabled关闭,不进行预清理。由于重新标记是独占CPU的,如果在一次新生代GC过后,立即触发一次重新标记,那么一次停顿时间可能会很长,为了避免这种情况,预处理时,会刻意等待一次新生代GC的发生,然后根据历史性能数据预测下一次新生代GC可能发生的时间,然后在当前时间和预测时间的中间时刻,进行重新标记,这样,从最大程度上避免新生代GC和重新标记重合,尽可能减少一次停顿时间。

可以使用参数-XX:+UseConcMarkSweepGC来启用该回收器。由于CMS回收器不是独占式的回收器,在CMS回收过程中,应用程序任然在不停地工作,在应用程序工作过程中,又不断地产生垃圾。这些新生成的垃圾在当前CMS回收过程中是无法清除的,同时,因为应用程序没有中断,所以在CMS回收过程中,还应该确保应用程序有足够的内存可用。因此CMS回收器不会等待堆内存饱和时才进行垃圾回收,而是当堆内存使用率达到某一个阀值时便开始进行回收,以确保应用程序在CMS工作过程中,依然有足够的空间支持应用程序运行。

这个回收阀值可以使用-XX:CMSInitiatingOccupancyFraction来指定,默认是68,即当老年代空间使用率达到68%时,会执行一次CMS回收,如果应用程序的内存使用率增长很快,在CMS的执行过程中,已经出现了内存不足的情况,此时,CMS回收就会失败,虚拟机将启动老年代串行收集器进行垃圾回收。

由于CMS是使用标记-清除算法,因此会产生内存碎片,-XX:+UseCMSCompactAtFullCollection开关可以使CMS在垃圾收集完成后,进行一次内存碎片整理,内存碎片的整理不是并发进行的。-XX:CMSFullGCBeforeCompaction参数可以用于设定进行多少次CMS回收后,进行一次内存压缩。

G1回收器

G1回收器是JDK1.7中正式使用的全新的垃圾回收器,从长期目标来看,它是为了取代CMS回收器,G1回收器拥有独特的垃圾回收策略,这和之前提到的回收器截然不同,从分代来看,它任然属于分代垃圾回收器,它区分年轻代和老年代,但是它使用了分区算法,其特点如下:

  1. 并行性:G1在回收期间,可以由多个GC线程同时工作,有效利用多核计算能力。
  2. 并发性:G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此一般来说,不会在整个回收期间完全阻塞应用程序。
  3. 分代GC:G1依然是一个分代收集器,但它可以兼顾年轻代和老年代。
  4. 空间整理:它会进行碎片的整理,比CMS的标记-清理更有优势。
  5. 可预见性:由于分区的原因,G1可以只选取部分区域进行内存回收,这样缩小了回收的范围,因此对于全局停顿也能得到较好的控制。

G1的回收过程主要分为下面4个阶段:

  1. 新生代GC
  2. 并发标记周期
  3. 混合收集
  4. 可能会进行full GC

G1的新生代GC和其他回收器一样,使用复制算法,当Eden区满的时候,就会触发年轻代GC,将存活对象移动到survivor区,算法和复制算法基本是一样的。

G1的并发标记周期

并发标记周期可以分为以下几步:

  1. 初始标记:标记从根节点直接可达的对象,这个阶段会伴随一次新生代GC,它是会产生全局停顿的,应用程序线程在这个阶段必须停止执行。
  2. 根区域扫描:这个过程可以和应用程序并发执行,但是根区域扫描不能和新生代GC同时执行,因此如果恰巧在此时需要进行新生代GC,GC就需要等待根区域扫描结束后才能进行,如果发生这种情况,新生代GC的时间就会延长。
  3. 并发标记:和CMS类似,并发标记将会扫描并查找整个堆中的存活对象,并做好标记,这是并发的过程,并且这个过程可以被一次新生代GC打断。
  4. 重新标记:和CMS一样,重新标记也是会产生应用程序停顿的。由于并发标记阶段,应用程序还在运行,因此标记结果可能需要进行修正,所以在此对上一次的标记结果进行补充,在G1中,这个过程使用SATB(Snapshot-At-The-Beginnig)算法完成,即G1会在标记之初为存活对象创建一个快照,这个快照有助于加速重新标记的速度。
  5. 独占清理:这个阶段是会引起停顿的,它将计算各个区域的存货对象和GC回收比例进行排序,识别可供混合回收的区域。在这个阶段,还会更新记忆集(Remebered Set)。该阶段给出了需要被混合回收的区域并进行了标记,在混合回收阶段,需要这些信息。
  6. 并发清理阶段:这里会识别并清理完全空闲的区域。它是并发的清理,不会引起停顿。

在并发标记之后,系统的一些区域会被标记为G的区域,是因为它们内部的垃圾比例较高,因此希望在后续的混合GC中进行收集,这些将要被回收的区域会被G1记录在一个称为Collection Sets(回收集)的集合中。

在混合回收阶段,它会优先回收一些垃圾比例高的区域,这也是G1回收器名字的由来(Garbage frist),混合回收,即同时处理了新生代和老年代。

和CMS类似,并发收集是与应用程序和GC线程交替工作,因此总不能完全避免在特别繁忙的场合会出现在回收过程中内存不充足的情况。当遇到这种情况时,G1也会转入一个Full GC进行回收。此外,如果在混合GC时发生空间不足或者在新生代GC时,survivor区和老年代无法容纳幸存对象,都会导致一次full gc产生。

G1相关参数

可以使用-XX:+UseG1GC标记打开G1收集器开关,对G1收集器进行设置时,最重要的一个参数是-XX:MaxGCPauseMills,它用于指定目标最大停顿时间,如果任何一次停顿超过这个设置值时,G1就会尝试调整新生代和老年代的比例,调整堆大小,调整晋升年龄等手段,试图达到预设目标。

还有一个参数是-XX:ParallelGCThreads,它用来设置并行回收时,GC的工作线程数量。

参数-XX:InitiatingHeapOccupancyPercent参数可以指定当整个堆使用率达到多少时,触发并发标记周期的执行,默认值是45,即当整个堆的占用率达到45%时,执行并发标记周期,这个值一旦设定,就不会被G1所修改,如果设置的过大,那么就会导致并发标记迟迟不能执行,引起Full GC的可能性也大大增加,反之,一个过小的值,使得并发周期非常频繁,大量GC线程抢占CPU,会导致应用程序的性能有所下降。

本篇文章就介绍到这里,如有问题欢迎留言指正!!!

参考资料:《实战Java虚拟机-JVM故障诊断与性能优化》

你可能感兴趣的:(JVM)