JVM之垃圾收集器

starlin 704 2018-09-02

通常虚拟机中往往不止一种GC收集器,该篇就来看看HotSpot虚拟机中有哪些GC收集器,如下图所示:
![HotSpot虚拟机的垃圾收集器](https://raw.githubusercontent.com/smartlin/pic/main/_posts/jvm/jvm%E4%B9%8B%E5%9E%83%E5%9C%BE%E6%94%B6%E9%9B%86%E5%99%A8.md/HotSpot%E8%99%9A%E6%8B%9F%E6%9C%BA%E7%9A%84%E5%9E%83%E5%9C%BE%E6%94%B6%E9%9B%86%E5%99%A8.jpg =888x)

上图展示了7种作用于不同分代的收集器,如果两个收集器之间存在连线,这说明它们之间可以搭配使用。虚拟机所处的区域则代表它是属于新生代收集器还是老年代收集器,现在分别来介绍这些收集器的特性

Serial收集器

Serial收集器是最基本、历史最悠久的收集器,这个收集器是一个单线程的收集器,但它的单线程的意义并不仅仅说明它只会使用一个CPU或一条线程去完成垃圾收集工作,更重要的是它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集完成。实际上这项工作是虚拟机在后台自动发起和自动完成的,下图示意了Serial/Serial Old收集器的运行过程:
![Serial和SerialOld收集器运行示意图](https://raw.githubusercontent.com/smartlin/pic/main/_posts/jvm/jvm%E4%B9%8B%E5%9E%83%E5%9C%BE%E6%94%B6%E9%9B%86%E5%99%A8.md/Serial%E5%92%8CSerialOld%E6%94%B6%E9%9B%86%E5%99%A8%E8%BF%90%E8%A1%8C%E7%A4%BA%E6%84%8F%E5%9B%BE.jpg =888x)

Serial收集器依然是虚拟机运行在Client模式下的默认新生代收集器,它优于其他收集器的地方:简单而高效(与其他收集起的单线程相比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。所以,Serial收集器对于运行在Client模式下的虚拟机来说是一个很好的选择

ParNew收集器

ParNew收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集以外,其余都与Serial收集器完全一样,其工作示意图如下:
![ParNew和SerialOld收集器运行示意图](https://raw.githubusercontent.com/smartlin/pic/main/_posts/jvm/jvm%E4%B9%8B%E5%9E%83%E5%9C%BE%E6%94%B6%E9%9B%86%E5%99%A8.md/ParNew%E5%92%8CSerialOld%E6%94%B6%E9%9B%86%E5%99%A8%E8%BF%90%E8%A1%8C%E7%A4%BA%E6%84%8F%E5%9B%BE.jpg =888x)

ParNew收集器除了多线程之外,其他与Serial收集器相比并没有太多的创新之处,但它却是许多运行在Server模式下的虚拟机中首选的新生代收集器,除了Serial收集器外,目前只有它能与CMS收集器配合工作。
ParNew收集器使用-XX:+UseCOncMarkSweepGC选项后的默认新生代收集器,也可以使用-XX:+UseParNewGC选项来强制指定它,同样可使用-XX:ParallerGCThreads参数来限制垃圾收集器的线程数

Parallel Scavenge收集器

Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器,看上去和ParNew相似,那它有什么特别之处了?

Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量,所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间 + 垃圾收集时间),虚拟机总共运行了100分钟,其中垃圾收集划掉了1分钟,那吞吐量就是99%。

Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数,以及直接设置吞吐量大小的-XX:GCTimeRatio参数
MaxGCPauseMillis参数允许的值是一个大于0的毫秒数,收集器尽可能的保证内存回收花费的时间不超过设定值。那么是否可以认为把这个参数设置越小越好了,GC停顿时间缩短是以牺牲吞吐量和新生代空间来换取的;系统把新生代调小一些,收集300M新生代肯定比收集500M块,这也直接导致垃圾收集发生的更频繁一些,原来10秒收集一次,每次停顿100毫秒,现在变成5秒收集一次,每次停顿70毫秒,停顿时间的确在下降,但吞吐量却降下来了
GCTimeRatio参数的值是一个大于0且小于100的整数,也就是垃圾收集时间占总时间的比率,相当于吞吐量的倒数。

由于与吞吐量关系密切,Parallel Scavenge收集器也经常称为“吞吐量优先”收集器,除了上述两个参数之外,Parallel Scavenge收集器还有个一个参数-XX:UseAdaptiveSizePolicy值得关注,这是一个开关参数,当这个参数打开后,就不需要手工指定新生代的大小(-Xmn)、Eden与Survivor区的比列(-XX:SurvivorRatio)、晋升老年代对象的大小(-XX:PretenureSizeThreshold)等细节参数,虚拟机会根据当前系统的运行情况,动态调整这些参数,这种调节方式称为GC自适应的调节策略(GC Ergonomics)。
当手工优化存在困难的时候,使用Parallel Scavenge收集器配合自适应调节策略,只需要把基本的内存数据设置好(如-Xms:设置最大堆),然后使用MaxGCPauseMillis参数或GCTimeRatio参数给虚拟机设置一个优化目标,其他参数的调节就由虚拟机完成了。

Serial Old收集器

Serial Old收集器是Serial收集器的老年代版本,它同样是一个单线程的收集器,使用“标记-整理”算法,这个收集器主要意义也是在于给Client模式下的虚拟机使用,如果在Server模式下,它主要有两大用途:一种是在JDK1.5之前与Parallel Scavenge收集器搭配使用,另一种用途就是作为CMS收集器的后背预案,在并发收集发生Concurrent Mode Failure时使用,其工作示意图如下和Serial示意图一样:
![Serial和SerialOld收集器运行示意图](https://raw.githubusercontent.com/smartlin/pic/main/_posts/jvm/jvm%E4%B9%8B%E5%9E%83%E5%9C%BE%E6%94%B6%E9%9B%86%E5%99%A8.md/Serial%E5%92%8CSerialOld%E6%94%B6%E9%9B%86%E5%99%A8%E8%BF%90%E8%A1%8C%E7%A4%BA%E6%84%8F%E5%9B%BE-1.jpg =888x)

Parallel Old收集器

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法,这个收集器在JDK1.6中才开始提供,在此之前,新生代的Parallel Scavenge收集器一直处于比较尴尬的状态,原因是如果新生代选择了Parallel Scavenge收集器,老年代除了Serial Old收集器外别无选择,直到Parallel Old收集器出现后,“吞吐量优先”收集器终于有了应用组合,在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge收集器和Parallel Old收集器,其工作示意图如下:
![ParallelScavenge和ParallelOld收集器运行示意图](https://raw.githubusercontent.com/smartlin/pic/main/_posts/jvm/jvm%E4%B9%8B%E5%9E%83%E5%9C%BE%E6%94%B6%E9%9B%86%E5%99%A8.md/ParallelScavenge%E5%92%8CParallelOld%E6%94%B6%E9%9B%86%E5%99%A8%E8%BF%90%E8%A1%8C%E7%A4%BA%E6%84%8F%E5%9B%BE-1.jpg =888x)

CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,目前很大一部分的Java应用集中在互联网或者B/S系统的服务器上,CMS收集器是基于“标记-清除”算法实现的,它的运作过程相对于前面几种收集器来说更复杂一点,整个过程分为4个步骤:

  • 初始标记(CMS initial mark)
  • 并发标记(CMS concurrent mark)
  • 重新标记(CMS remark)
  • 并发清除(CMS concurrent sweep)

初始标记和重新标记这两个步骤仍然需要“Stop the world”。初始标记值是标记一下GC Roots能直接关联到的对象,速度很快;并发标记阶段就是进行GC Roots Tracing的过程,而重新标记则是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会初始标记的时间稍长,但远比并发标记的时间短。
由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户一起并发执行,其工作示意图如下:
![CMS收集器示意图](https://raw.githubusercontent.com/smartlin/pic/main/_posts/jvm/jvm%E4%B9%8B%E5%9E%83%E5%9C%BE%E6%94%B6%E9%9B%86%E5%99%A8.md/CMS%E6%94%B6%E9%9B%86%E5%99%A8%E7%A4%BA%E6%84%8F%E5%9B%BE.jpg =888x)

虽然CMS收集器是一款并发收集、低停顿的优秀收集器,但它有如下3个明显的缺点:

  1. CMS收集器对CPU非常敏感
  2. CMS收集器无法处理浮动垃圾(Floating Garbage),可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉,这一部分称为“浮动垃圾”。也是由于在垃圾收集阶段用户线程还需要运行,那也就需要预留足够的内存空间给用户线程使用,因此CMS收集器不想其他收集器,等待老年代几乎被完全填满后再进行收集,需要预留一部分空间提供并发收集时的程序运作使用,在JDK1.6中,CMS收集器的启动阀值已经提升至92%。要是CMS运行期间预留的内存无法满足程序的需要,就会出现一次“Concurrent Mode Failure”失败,这是虚拟机将启动后备预案,临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿的时间就很长了。所以说参数-XX:CMSInitiatingOccupancyFraction设置得太高很容易导致大量“Concurrent Mode Failure”失败,性能反而降低。
  3. 因CMS是一款基于“标记-清除”算法实现的收集器,意味着收集结束会有大量的空间碎片产生。空间碎片过多时,将会给大对象分配内存空间带来麻烦,往往会出现老年代还有很大的剩余空间,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC,为了解决这个问题,CMS收集器提供了一个-XX:+UseCMSCompactAtFullCollection开关参数(默认是开启的,此参数从 JDK9 开始废弃),用于在CMS收集器顶不住要进行Full GC时开启内存碎片的合并整理过程,内存整理过程是无法并发的,空间碎片问题没有了,但停顿时间不得不变长。
    虚拟机还提供了一个参数-XX:CMSFullGCsBeforeCompaction,这个参数是用于设置执行多少次不压缩的Full GC后,跟着来一次带压缩的(默认值为0,表示每次进入Full GC时都进行 碎片整理)。

G1收集器

G1是一款面向服务端应用的垃圾收集器,G1具备以下特点:

  1. 并行与并发:G1能够充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短“Stop the world”停顿的时间,部分其他收集器需要停顿java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续运行
  2. 分代收集:虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活一段时间、熬过多次GC的旧对象以获取更好的收集效果
  3. 空间整合:与CMS的“标记-清理”算法不同,G1从整体来看是基于“标记-整理”算法实现的收集器,从局部(两个Region之前)来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片
  4. 可预测的停顿:这是G1相对于CMS另一大优势,降低停顿时间是CMS和G1共同关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不超过N毫秒。

在G1之前,其他垃圾收集器进行收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器时,java堆的内存布局就与其他收集器有很大的差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留着新生代和老年代的概念,但新生代和老年代不再物理隔离,它们都是一部分Region(不需要连续)的集合

G1收集器之所以能够建立可预测的停顿时间模式,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台先维护一个列表,每次根据允许的收集时间,优先回收价值最大的Region(这也是Garbage-First名称的来由)

在G1收集器中,Region之间的对象引用以及其他收集器中的新生代与老年代之间的对象引用,虚拟机都是使用Remembered Set来避免全对扫描。G1中每个Region都有一个与之对应的Remembered 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)

对于CMS收集器熟悉的,可以发现和G1收集器的前几个步骤的运作过程和CMS有很多相似之处。初始标记阶段仅仅只是标记一下GC Roots能直接关联到的对象。并发标记阶段是从GC Roots开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。而最终标记阶段则是为了修正在并发标记期间因用户程序继续运作而导致标记产生变化的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set,这阶段需要停顿线程,但是可并行执行。最后在筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,这个阶段其实也可以做到和用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程大幅提供收集效率,其工作示意图如下:
![G1收集器示意图](https://raw.githubusercontent.com/smartlin/pic/main/_posts/jvm/jvm%E4%B9%8B%E5%9E%83%E5%9C%BE%E6%94%B6%E9%9B%86%E5%99%A8.md/G1%E6%94%B6%E9%9B%86%E5%99%A8%E7%A4%BA%E6%84%8F%E5%9B%BE.jpg =888x)

ZGC收集器

ZGC 收集器是 JDK 11 中新增的垃圾收集器,它是由 Oracle 官方开发的,并且支持 TB 级别的堆内存管理,而且 ZGC 收集器也非常高效,可以做到 10ms 以内完成垃圾收集。

在 ZGC 收集器中没有新生代和老生代的概念,它只有一代。ZGC 收集器采用的着色指针技术,利用指针中多余的信息位来实现着色标记,并且 ZGC 使用了读屏障来解决 GC 线程和应用线程可能存在的并发(修改对象状态的)问题,从而避免了Stop The World(全局停顿),因此使得 GC 的性能大幅提升。

ZGC 的执行流程和 CMS 比较相似,首先是进行 GC Roots 标记,然后再通过指针进行并发着色标记,之后便是对标记为死亡的对象进行回收(被标记为橘色的对象),最后是重定位,将 GC 之后存活的对象进行移动,以解决内存碎片的问题。

分代收集

它是指将不同“年龄”的数据分配到不同的内存区域中进行存储,所谓的“年龄”指的是经历过垃圾收集的次数。这样我们就可以把那些朝生暮死的对象集中分配到一起,把不容易消亡的对象分配到一起,对于不容易死亡的对象我们就可以设置较短的垃圾收集频率,这样就能消耗更少的资源来实现更理想的功能了。

通常情况下分代收集算法会分为两个区域:新生代(Young Generation)和老年代(OldGeneration),其中新生代用于存储刚刚创建的对象,这个区域内的对象存活率不高,而对于经过了一定次数的 GC 之后还存活下来的对象,就可以成功晋级到老生代了

对于上面介绍的 7 个垃圾收集器来说,新生代垃圾收集器有:Serial、ParNew、Parallel Scavenge,老生代的垃圾收集器有:Serial Old、Parallel Old、CMS,而 G1 属于混合型的垃圾收集器,如下图所示:
![垃圾收集器分类](https://raw.githubusercontent.com/smartlin/pic/main/_posts/jvm/jvm%E4%B9%8B%E5%9E%83%E5%9C%BE%E6%94%B6%E9%9B%86%E5%99%A8.md/%E5%9E%83%E5%9C%BE%E6%94%B6%E9%9B%86%E5%99%A8%E5%88%86%E7%B1%BB.jpg =888x)

各种垃圾回收器对比

![](https://raw.githubusercontent.com/smartlin/pic/main/_posts/jvm/jvm%E4%B9%8B%E5%9E%83%E5%9C%BE%E6%94%B6%E9%9B%86%E5%99%A8.md/%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E5%99%A8%E5%AF%B9%E6%AF%94.png =888x)

垃圾收集器参数总结

下表整理了JDK1.7中各种垃圾收集器非稳定的运行参数

参数描述
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 MarkSweep)的收集器组合进行内存回收
UseParallelOldGC打开此开关后,使用Parallel Scavenge + Parallel Old的收集器组合进行内存回收
SurvivorRatio新生代中的Eden区域与Survivor区域的容量比值,默认为8,代表Eden:Survivor=8:1
PretenureSizeThreshold直接晋升到老年代的对象大小,设置这个参数后,大于这个参数的对象将直接在老年代分配
MaxTenuringThreshold晋升到老年代的对象年龄。每个对象在坚持过一次Minor GC之后,年龄就增加1,当超过这个参数值时就进入老年代
UseAdaptiveSizePolicy动态调整Java堆中各个区域的大小以及进入老年代的年龄
HandlePromotionFailure是否允许分配担保失败,即老年代的剩余空间不足以应付新生代的整个Eden和Survivor区的所有对象都存活的极端情况
ParallelGCThreads设置并行GC时进行内存回收的线程数
GCTimeRatioGC时间占总时间的比率,默认值为99,即允许1%的GC时间。仅在使用Parallel Scavenge收集器时生效
MaxGCPauseMillis设置GC的最大停顿时间。仅在使用Parallel Scavenge收集器时生效
CMSInitiatingOccpancyFraction设置CMS收集器在老年代空间被使用多少后触发垃圾收集。默认为68%,仅 在使用CMS收集器时生效
UseCMSCompacAtFullCollection设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片整理。仅在使用CMS收集器时生效
CMSFullGCsBeforeCompaction设置CMS收集器在进行若干次垃圾收集后再启动一次内存碎片整理。仅在使用CMS收集器时生效

# JVM