JVM|HotSpot垃圾收集器概述

如果说垃圾收集算法(标记-清除算法、复制算法、标记-整理算法)是内存回收的方法论,垃圾收集器就是内存回收的具体实现(以下垃圾收集器介绍仅限于HotSpot虚拟机,截止到Java 12)
1 Serial / Serial Old收集器 串行收集器是最古老、最稳定以及效率高的收集器,可能会产生较长的停顿,只使用一个线程去回收。新生代、老年代使用串行回收;垃圾收集的过程中会“Stop The World”(服务暂停)。
Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。
2 ParNew收集器 ParNew收集器其实就是Serial收集器的多线程版本。除了使用多线程进行垃圾收集之外,其余行为包括Serial收集器可用的所有控制参数、收集算法、“Stop The World”、对象分配规则、回收策略等都与Serial收集器完全一样。
3 Parallel Scavenge / Parallel Old收集器 Parallel Scavenge收集器是一个新生代收集器,它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间),虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和标记-整理算法。
4 CMS收集器 CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,是HotSpot虚拟机真正意义上第一款实现了并发的收集器。从名字上就可以看出CMS收集器是基于标记-清除算法实现的(可以设置碎片收集的周期),收集的范围是老年代。整个过程分为5个步骤:包括:

  • 初始标记:停止用户线程,标记一下GC Roots能直接关联到的对象,速度很快;
  • 并发标记:进行GC Root Tracing的过程,也就是从根节点往下去寻找引用对象的过程;
  • 重新标记:停止用户线程,修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短;
  • 并发清理:对未被标记的对象做清除工作,这个阶段如果有新增对象产生,会不做任何处理,等待下一次GC;
  • 并发重置:重置本次GC过程中的标记数据。
由于整个过程中耗时最长的并发标记和并发清理过程中,收集器线程都可以与用户线程一起工作,所以总体上来说,CMS收集器的内存回收过程是与用户线程一起“并发”地执行。
但是需要留意的是,如果存在上一次GC还没执行完,然后垃圾收集又被触发的情况,也就是说上一次还没回收完就再次触发GC,特别是在并发标记和并发清理阶段,此时会触发“concurrent mode failure”。在此期间会停止用户线程,用Serial Old收集器来进行回收。
5 G1收集器 上面提到的垃圾收集器,收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器时,Java堆的内存布局与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),即Region块。有Eden块、Survivor块、Old块和专门用来存放大对象的Humongous块(当一个对象的存储空间超过一个Region块大小的50%的时候,就会被放入到Humongous块中。如果一个大对象太大,可能会横跨多个Region块来存储)之分。虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔阂了,它们都是一部分(可以不连续)Region的集合。
G1收集器的运作过程与CMS较为相似,前三步是一样的。其过程如下:
  • 初始标记:同CMS的初始标记;
  • 并发标记:同CMS的并发标记;
  • 最终标记:同CMS的重新标记;
  • 筛选回收:停止用户线程,首先排序各个Region的回收价值和成本,然后根据用户期望的GC停顿时间来制定回收计划。最后按计划回收一些价值高的Region中的垃圾对象。回收时采用复制算法,从一个或多个Region块中复制存活对象到堆上的另一个空的Region块,并且在此过程中压缩和释放内存。G1不会像CMS那样回收完因为有很多内存碎片还需要整理一次。
值得一说的是,CMS清理阶段是跟用户线程一起并发执行的,而G1因为内部实现太过复杂所以暂时没能实现并发回收。不过到了Shenandoah收集器就实现了并发收集,Shenandoah收集器可以看作是G1的升级版本。
G1相对于CMS另外一个更大的优势,就是可以设置可预测的停顿模型,能够使开发者明确指定在长度为M毫秒的时间片段内,消耗在垃圾收集器上的时间不能超过N毫秒。这个特性使得G1可以适合几十个G的大内存使用场景,因为在那种情况下,如果不加控制,即使是Young GC也要消耗很长的时间。
G1垃圾收集的分类如下,其与一般的GC也不太相同:
  • Young GC:不同于常规的Young GC在Eden区满了的时候会进行,G1会计算这次Eden区回收大概需要多少时间,如果回收时间远小于设定好的停顿时间值,就会选择增加新的年轻代的Region块,以此来存放新的对象,直到下一次Eden区满。如果下一次GC的预测时间接近设定的值,那么就会触发Young GC;
  • Mixed GC:老年代的空间占用率达到参数设定的值时会触发,回收所有的Young区和部分的Old和Humongous区,使用复制算法的时候如果发现没有足够的空Region块能承载拷贝对象的时候,会触发一次Full GC;
  • Full GC:停止系统线程,然后采用单线程进行清理工作,空闲出的一批Region块以供下一次的Mixed GC来使用,这个过程是比较耗时的(如上所说,Shenandoah收集器已经优化成多线程进行收集了)。
需要说明的一点是,从Java 9开始,默认的垃圾收集器已经改为了G1,代替了之前的Parallel Scavenge(新生代) + ParallelOld(老年代)的组合。
6 ZGC收集器 ZGC收集器是从Java 11开始支持的具有实验性质的垃圾收集器,其目标如下:
  • GC暂停时间不应超过10毫秒;
  • 处理堆的大小从相对较小(几百兆字节)到非常大(多太字节)不等;
  • 与使用G1相比,应用程序吞吐量减少不超过15%;
  • 为未来的GC功能和优化利用彩色指针和负载障碍奠定基础。
ZGC收集器是一款基于Region内存布局的,暂时不设分代,使用了读屏障、颜色指针等技术来实现可并发的标记-整理算法的,以低延迟为首要目标的一款垃圾收集器。其设计上借鉴了Azul System公司开发的C4(Concurrent Continuously Compacting Collector)收集器。
ZGC中的Region块分为大、中、小三种容量:
  • 小型Region:容量固定为2MB,用于放置小于256KB的小对象;
  • 中型Region:容量固定为32MB,用于放置大于等于256KB但小于4MB的对象;
  • 大型Region:容量不固定,可以动态变化,但必须为2MB的整数倍,用于放置4MB或以上的大对象。
ZGC的运作过程如下:
  • 并发标记:与G1一样,并发标记是遍历对象图做可达性分析的阶段。它的初始标记和最终标记也会出现短暂的停顿,与G1不同的是,ZGC的标记是在指针上而不是在对象上进行的,标记阶段会更新染色指针中的Marked 0、Marked 1标志位;
  • 并发预备重分配:这个阶段需要根据特定的查询条件统计得出本次收集过程要清理哪些Region,将这些Region组成重分配集(Relocation Set)。ZGC每次回收都会扫描所有的Region,用范围更大的扫描成本换取省去G1中记忆集的维护成本;
  • 并发重分配:重分配是ZGC执行过程中的核心阶段,这个过程要把重分配集中的存活对象复制到新的Region上,并为重分配集中的每个Region维护一个转发表(Forward Table),记录从旧对象到新对象的转向关系。ZGC收集器能仅从引用上就明确得知一个对象是否处于重分配集之中,如果用户线程此时并发访问了位于重分配集中的对象,这次访问将会被预置的内存屏障(读屏障)所截获,然后立即根据Region上的转发表记录将访问转发到新复制的对象上,并同时修正更新该引用的值,使其直接指向新对象,ZGC将这种行为称为指针的“自愈”(Self-Healing)能力;
  • 并发重映射:重映射所做的就是修正整个堆中指向重分配集中旧对象的所有引用,但是ZGC中对象引用存在“自愈”功能,所以这个重映射操作并不是很迫切。ZGC很巧妙地把并发重映射阶段要做的工作,合并到了下一次垃圾收集循环中的并发标记阶段里去完成,反正它们都是要遍历所有对象的,这样合并就节省了一次遍历对象图的开销。一旦所有指针都被修正之后, 原来记录新旧对象关系的转发表就可以释放掉了。
7 Epsilon收集器 Java 11中还加入了一个比较特殊的垃圾收集器——Epsilon,该垃圾收集器被称为“no-op”收集器,将处理内存分配而不实施任何实际的内存回收机制。 也就是说,这是一款不做垃圾回收的垃圾回收器。这个垃圾回收器看起来并没什么用,主要可以用来进行性能测试、内存压力测试等,Epsilon GC可以作为度量其他垃圾回收器性能的对照组。大神Martijn说,Epsilon GC至少能够帮助理解GC的接口,有助于成就一个更加模块化的JVM。
8 Shenandoah收集器 Java 12中新加入的垃圾收集器,Shenandoah最初的目标是把GC停顿时间降到10毫秒以下,并且对内存的支持扩展到TB级别。为了降低停顿时间,回收器需要使用更多的线程来并行处理回收任务。而要在降低停顿时间的同时能够支持更大的堆空间,回收器对CPU的多核处理能力提出了更高的要求。相比于G1,Shenandoah不仅进行并行的垃圾标记,在压缩堆空间时也是并行进行的。
【JVM|HotSpot垃圾收集器概述】值得一提的是,作为首个由非Oracle开发,由RedHat领导开发的垃圾收集器,其目标又与Oracle在Java 11中发布的ZGC几乎完全一致,两者天生就存在竞争。于是Oracle马上用实际行动抵制了这个新收集器,在OracleJDK 12里把Shenandoah的代码通过条件编译强行剔除掉(OpenJDK 12中仍然可用)。

    推荐阅读