Skip to content

Java 虚拟机常用垃圾收集器总结

Published: at 16:48:08

通常虚拟机中往往不止一种垃圾收集器,Hotspot一共有7种收集器:

  1. Serial 收集器
  2. ParNew 收集器
  3. Parallel Scavenge收集器
  4. Serial Old 收集器
  5. Parallel Old 收集器
  6. CMS 收集器
  7. G1 收集器

Serial 收集器

Serial 收集器是最基本、历史最悠久的收集器,Serial(串行),从名字就可以看出这个收集器是一个单线程的收集器。它在进行垃圾收集时,必须要暂停其他所有的工作线程,知道它垃圾收集结束。这个暂停其他工作线程的动作一般被成为“Stop The World”,简称STW

采用的是复制算法。 untitled.jpg

优点:

简单,高效,适合运行在client模式下虚拟机

缺点:

会产生STW,整个应用停顿

ParNew 收集器

ParNew收集器其实就是Serial收集器的多线程版本。除了使用多线程进行垃圾收集外,其余行为包括Serial收集器可用的所有控制参数、收集算法、STW现象、回收策略等都与Serial一致。

采用的是复制算法。 untitled.jpg

可以使用JVM参数 -XX:+UseParNewGC 选项来指定它:

ParNew收集器在单CPU的环境下不会比Serial收集器有更好的效果,因为会存在线程交互的开销。可以使用JVM参数: -XX:ParalleGCThreads 参数来限制垃圾收集线程数。

Parallel Scavenge 收集器

Parallel Scavenge 收集器同样用的是复制算法,也是并行多线程收集。Paralle Scavenge的目标是达到一个可控制的吞吐量(Throughput)。所谓的吞吐量就是CPU用于运行用户代码的时间和CPU总消耗时间的比值,即吞吐量 = CPU运行用户代码时间 / CPU运行用户代码时间 + 垃圾收集时间

Parallel Scavenge收集器提供了两个参数用来精准控制吞吐量,分别是控制最大垃圾收集的停顿时间和直接设置吞吐量大小的参数,分别是:-XX:MaxGCPauseMillis-XX:GCTimeRatio

MaxGCPauseMillis 参数运行设置一个大于0的毫秒数。

GCTimeRatio 参数允许设置一个0-100的整数数字,也就是垃圾收集时间占总时间的比例,相当于吞吐量的倒数。如果把这个参数设置成19,那么允许的最大GC时间就是5%(计算过程:1/(1+19)),默认值是99,就是允许最大GC时间是1%(1/(1+99))。

Parallel Scavenge收集器还有一个参数:-XX:+UseAdaptiveSizePolicy。这是一个开关,打开这个参数后,就不需要手工指定新生代的大小、Eden和Survivor区的比例、晋升老年代对象年龄等细节参数了,虚拟机会收集性能监控信息,动态的调整这些参数,以提供最适合的停顿时间或者最大吞吐量,这种方式被称为GC自适应调节策略(GC Ergonomics)

使用GC自适应调节策略时,我们只需要设置好最大堆,然后使用MaxGCPauseMillis(更关注GC最大停顿时间) 或者 GCTimeRatio (更关注吞吐量)参数告诉虚拟机一个优化的目标,具体的细节参数调整就由虚拟机自己完成了。

💡GC自适应调节策略是Paralle Scavenge 收集器和ParNew收集器一个重要的区别。

Serial Old 收集器

Serial Old 是Serial收集器的老年代版本,它同样是一个单线程收集器。

但是它使用的不是复制算法,而是使用的“标记—整理”算法。

它有两个主要用途:

  1. 在JDK1.5及之前的版本中与Parallel Scavenge 收集器搭配使用
  2. 作为CMS收集器的后背预案,在并发收集发生Concurrent Mode Failure时使用。 untitled.jpg

Parallel Old 收集器

Parallel Old 是 Parallel Scavenge 收集器的老年版本, 使用多线程和“标记—整理”算法。

这个收集器是在JDK1.6中才开始提供的。

在注重吞吐量和CPU资源敏感的场合下,可以优先考虑ParallelScavenge 和 Parallel Old 收集器。

CMS 收集器(Concurrent Mark Sweep)

CMS 收集器是一款以获得最短回收停顿时间为目的的垃圾收集器。

CMS 收集器非常符合重视服务的响应速度、希望停顿时间最短这类应用的需求。

从名字就可以看出,CMS收集器是基于“标记—清理”算法来实现的,它的整个运作过程分为4个步骤(三个标记,一个清除):

  1. 初始标记(initial mark)
  2. 并发标记(concurrent mark)
  3. 重新标记(remark)
  4. 并发清除(concurrent sweep)

其中,初始标记和重新标记这两个步骤仍然需要STW。

初始标记阶段仅仅只是标记一下GC Roots能直接关联到的对象,速度很快;

并发标记就是进行GC Roots Tracing(追踪) 的过程;

重新标记阶段则是为了修正并发标记阶段期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长,但远比并发标记的时间短。 untitled.jpg

CMS收集器优点:

  1. 并发收集、停顿时间短

CMS收集器的3个明细缺点:

  1. 对CPU资源非常敏感

    CMS收集器默认启动的回收线程数是(CPU数量+3)/4,也就是说4个CPU以上时,垃圾收集线程不会少于25%的CPU资源;并且可能随着CPU数的增加而下降。当CPU不足4个时,CMS会用户程序的影响可能会更大。为了应付这种情况,虚拟机提供了一种称为“增量式并发收集器”——简称i-CMS的CMS收集器变种,就是在并发标记、并发清除的时候GC线程和用户线程交替运行,尽量减少GC线程独占资源的时间,这样整个垃圾收集的时间会更长。在目前版本i-CMS 已经被声明为“deprecated”,不再提倡使用。

  2. 无法处理浮动垃圾

    浮动垃圾可能会出现“Concurrent model failure”——并发模式失败,从而导致另一次FullGC 的产生。

    由于CMS并发清除时用户线程还在运行,这个时候运行程序还可能正在产生垃圾对象,这些对象出现在标记过程之后,CMS收集器就无法在此次收集中回收它们,只能留给下一次GC时清理,这一部分垃圾就被称为**“浮动垃圾”**。

    因为在垃圾收集阶段,要处理浮动垃圾,就需要给用户线程预留足够的空间,所以CMS收集器不能等到老年代几乎满了再进行收集动作,必须要预留一部分空间给并发收集时用户程序使用。在JDK1.6中,当老年代使用了92%的空间后CMS收集器就会被触发。

    可以使用-XX:CMSInitiatingOccupancyFraction 的值来设置触发百分比。

    要是CMS运行期间预留的内存无法满足程序运行,就会出现一次 “Concurrent model failure”失败,这时候虚拟机会启动后备预案:临时启动Serial Old收集器来重新进行老年代的垃圾回收,这样停顿时间就会更长了。

    所以CMSInitiatingOccupancyFraction 设置过高更容易导致并发模式失败,性能反而会更低。

  3. 会产生碎片(采用的Mark Sweep算法)

    因为CMS是采用的“标记—清除”算法,意味着收集结束后一定会产生大量的碎片。当虚拟机无法找到足够大的连续空间来为对象分配内存的时候,不得不提前触发一次Full GC。为了解决这个问题,CMS收集器提供了一个参数:-XX: UseCMSCompactAtFullCollection 开关(默认开启),用于CMS收集器顶不住要进行FullGC的时候开启内存碎片合并整理的过程,但是停顿时间就会变长。虚拟机还提供了另一个参数-XX: CMSFullGCsBeforeCompaction,这个参数用于设置执行多少次不压缩的FullGC后,跟着再执行一次带压缩的FullGC(默认是0,表示每次进入FullGC都要进行碎片整理)。

G1 收集器(Garbage-First)

G1是一款面向服务端应用的垃圾收集器,Hotspot开发团队赋予它的使用是未来可以替换掉JDK1.5中发布的CMS收集器。与其他收集器相比,G1收集器具备以下特点:

  1. 并行与并发

    G1能充分利用多CPU和多核的特性来缩短STW的时间,其他垃圾收集器原本需要停顿JAVA线程来执行的GC动作,G1收集器可以通过并发的方式让JAVA程序继续运行

  2. 分代收集

    分代的概念仍然在G1中保留,G1已经不需要其他收集器配合就可以独立管理整个JAVA堆。

  3. 空间整合

    G1从整体上看是基于“标记—整理”算法实现的收集器,但是从局部(两个Region之间)上来看是基于“复制”算法实现的,所以无论如何都意味着G1不会产生内存空间碎片。这种特性有利于程序长时间运行。

  4. 可预测停顿

    G1除了和CMS一样追求低停顿外,还可以建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。

使用G1收集器的时候,JAVA堆的内存布局完全不同于其他收集器的布局,G1将整个堆划分为多个大小不一的独立区域(Region),仍然保留了新生代和老年代的概念,但是新生代和老年代不再是物理隔离的了。

G1收集器是如何做可预测停顿时间模型的?

G1会跟踪各个Region里面的垃圾堆积的价值大小(回收能获得的空间大小和回收所需要时间的经验值),在后台维护一个优先列表,每次根据允许收集的时间,优先回收价值大的Region(这也是Garbage-First名字的由来)。这种使用Region划分空间以及有优先级的空间回收方式,保证了G1收集器在有限的时间内可以获取更高的回收效率。

一个对象分配在Region中,它并非只能被本Region区域内的对象来引用,而是可以和整个Java堆任意的对象发生引用关系的。那么在做可达性分析判断对象存活的时候,G1岂不是也要扫描整个JAVA堆才能保证准确性?

在G1收集器中,Region之间对象的引用以及其他收集器新生代和老年代之间的对象引用,虚拟机都是使用Rememberd Set来避免全堆扫描的。G1中每个Region 都有一个与之对应的Rememberd Set。当进行内存回收时,在GC根节点的枚举范围内加入Rememberd Set即可保证不对全堆扫描也不会有遗漏。

G1收集动作大致有以下几个步骤:

  1. 初始标记(InitIal Marking)

    初始标记阶段仅仅只是标记一下GC Roots能直接关联到的对象,并且修改了TAMS(Next To At Mark Start)的值,让下一阶段用户程序并发执行时,能在正确可用的Region中创建新对象,这个阶段需要停顿线程,但耗时很短。

  2. 并发标记(Concurrent Marking)

    并发标记阶段是从GC Root开始对堆中对象进行可达性分析,找出存活的对象,这个阶段耗时很长,可与用户程序并发执行,不需要停顿线程。

  3. 最终标记(Final Marking)

    为了修正并发标记阶段期间因用户程序继续运行而导致标记产生变化的那一部分标记记录,虚拟机将这一部分变化记录记录到了线程Rememberd Set Logs 里面,最终标记阶段需要把Logs里面的数据合并到Rememberd Set中。这个阶段可以并行执行,但是需要停顿线程。

  4. 筛选回收(Live Data Counting and Evacuation)

    筛选回收阶段首先会对各个Region区域的回收价值和成本进行排序,根据用户期望的GC停顿时间来定制回收计划,这个阶段可以做到和用户线程一起并发执行,但是因为只回收一部分的Region区域,时间是用户控制的,而且停顿用户线程可以大幅提升回收的效率。 untitled.jpg