本文主要内容是介绍常见的垃圾回收器。总体分为年轻代回收器和老年代类回收器,如下图所示:

可见,除了G1回收器外,年轻代和老年代回收器各三个。接下来将分别介绍。

1. Serial + Serial Old

SerialSerial Old是最古老的垃圾回收器。其特点是单线程执行,且在工作时,需要暂停其它所有的线程。它的优势是简单且高效,额外内存消耗小。目前是HotSpot虚拟机运行在客户端模式下的默认新生代和老年代垃圾回收器。有如下特点:

  •  针对新生代;
  •  采用复制算法;
  •  单线程收集;
  •  进行垃圾收集时,必须暂停所有工作线程,直到完成;

年轻代采用复制算法实现,老年代采用标记-整理算法实现。

1.1 优势

简单高效,由于采用的是单线程的方法,因此与其他类型的收集器相比,对单个cpu来说没有了上下文之间的的切换,效率比较高。

1.2 劣势

会在用户不知道的情况下停止所有工作线程。

1.3 使用场景

  •  Client 模式(桌面应用): 在用户的桌面应用场景中,可用内存一般不大,可以在较短时间内完成垃圾收集,只要不频繁发生,这是可以接受的
  •  单核服务器:对于限定单个CPU的环境来说,Serial收集器没有线程切换开销,可以获得最高的单线程收集效率

开启方法: -XX:+UseSerialGC,即可指定年轻代和老年代都适用串行回收器。

2. ParNew回收器(年轻代)

可以看作是Serial的多线程并行版本。优化了Serial回收器停顿时间较长的缺陷。目前ParNewSerial是唯二可以和CMS配合使用的新生代回收器。

采用复制算法实现。

2.1 优势

多线程版本的Serial,可以更加有效的利用系统资源

2.2 劣势

同Serial,会在用户不知道的情况下停止所有工作线程

2.3 使用场景

Server模式下使用,亮点是除Serial外,目前只有它能与CMS收集器配合工作,是一个非常重要的垃圾回收器。

使用-XX: +UseConcMarkSweepGC参数开启CMS垃圾回收器时,默认就选择了ParNew作为新生代的垃圾回收器。

3. Parallel Scavenge + Parallel Old

这两个回收器是JDK8默认的垃圾回收器,多线程并行回收。主要关注系统的吞吐量,最大的特点是能自动调整堆大小

年轻代采用复制算法,老年代采用标记-整理算法。

有如下特点:

  •  多线程收集;
  •  关注点与其他收集器不同:
    •   CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间;
    •   而Parallel Scavenge收集器的目标则是达一个可控制的吞吐量.

Parallel Scavenge提供了两个参数用于精确控制吞吐量,分别为:

  • -XX: MaxGCPauseMillis:最大垃圾收集停顿时间,是一个大于0的毫秒数。回收器将尽可能保证回收花费的时间不超过用户的设定值;
  • -XX: GCTimeRatio:吞吐量的大小。允许设置(0, 100)范围内的整数。默认值为99,表示最大允许1 / ( 1 + 99 ) = 1%的垃圾收集时间;

此外,Parallel Scavenge还提供了一个参数XX: +UseAdaptiveSizePolicy用于开启自适应调节策略。在开启该策略之后,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整新生代大小(-Xmn)、Eden区和Survivor区的比例(-XX:SurvivorRatio)等参数,以提供最合适的停顿时间或者最大的吞吐量。在手工优化存在困难的情况下,使用自适应调节策略是个不错的选择。

3.1 优势:

追求高吞吐量,高效利用CPU,是吞吐量优先,且能进行精确控制。

3.2 劣势:

应该说是特点,追求高吞吐量必然要牺牲一些其他方面的优势,不能做到既,又。ParNew收集器关注点在于尽可能的缩短垃圾收集时用户线程的停顿时间,原本10s收集一次, 每次停顿100ms, 设置完参数之后可能变成5s收集一次, 每次停顿70ms. 停顿时间变短, 但收集次数变多。

3.3 使用场景:

根据相关特性,我们很容易想到它的使用场景,即:当应用程序运行在具有多个CPU上,对暂停时间没有特别高的要求时,程序主要在后台进行计算,而不需要与用户进行太多交互等就特别适合ParNew收集器。例如,那些执行批量处理、订单处理、工资支付、科学计算的应用程序等。

4. CMS回收器(Concurrent Mark Sweep)

^9b1727

CMS更加关注的是系统的停顿时间,允许用户线程和垃圾回收线程在某些步骤中同时进行,减少用户线程的等待时间。

采用标记-清除算法实现。

其工作过程分为以下四个步骤:

  • 初始标记:标记GC Root能够直接关联到的对象,速度很快,但需要Stop The World, STW
  • 并发标记:从GC Root的直接关联对象开始遍历整个内存,虽然过程时间较长,但并不需要暂停用户线程;
  • 重新标记:用于修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,仍然需要STW
  • 并发清除:清理删除掉标记阶段判断的已经死亡的对象,可以和用户线程并发执行;

4.1 优势

  1. 停顿时间短;
  2.  吞吐量大;
  3.  并发收集

4.2 劣势

  1. 由于使用了标记-清除算法,导致垃圾收集后会出现大量的内存碎片,CMS会在Full GC时进行碎片整理,导致用户线程暂停。提供的解决方案是,调整为N次FullGC后再进行整理。-XX:CMSFullGCsBeforeCompaction=N,其中N默认为0。
  2. 在并发清理过程中,由于和用户线程同时进行,可能会出现一下浮动垃圾,这些垃圾无法回收;
  3. 如果老年代内存不足,无法分配对象的话,CMS会直接退化成Serial Old来单线程回收老年代垃圾;
  4. 并发阶段会影响到用户线程执行的性能。

4.3 使用场景

  •  与用户交互较多的场景;
  •  希望系统停顿时间最短,注重服务的响应速度;
  •  以给用户带来较好的体验;

开启方法:-XX:+UseConcMarkSweepGC即可开启CMS

5. G1回收器(Garbage First)

^1c405c

全称是Garbage First。其设计目标是在延迟可控的情况下获得尽可能高的吞吐量

有如下特点:

  •  并行与并发
  •  分代收集,收集范围包括新生代和老年代
  •  结合多种垃圾收集算法,空间整合,不产生碎片
  •  可预测的停顿:低停顿的同时实现高吞吐量
  •  面向服务端应用,将来替换CMS

它的回收逻辑颠覆了之前的传统思维,使用Mixed GC的模式,可以面向堆内存的任何部分来组成”回收集(Collection Set, CSet)”进行回收。衡量标准不再是该内存区域属于哪个分代,而是哪块内存中存放垃圾最多,回收收益最大。

G1将Java堆划分为多个大小相等的独立区域,称为Region。每个Region都已根据需要,扮演Eden 、Survivor 、或老年代空间, 相对于之前的垃圾回收器,更加灵活。同时,Region也是单次回收的最小单位。G1会跟踪每个Region的垃圾”回收价值”大小,在后台维护一个优先级列表,并根据用户所设定的收集停顿时间(-XX: MaxGCPauseMillis,默认200毫秒)优先回收价值最大的Region。

此外,堆中还有一个区域会被识别为Humongous区域,专门用来存储大对象(G1将大小超过Region容量一半的对象识别为大对象)。超过了整个Region容量的超级大对象,会被存放在多个连续的Humongous Region中。G1在大多数情况下,都将Humongous Region作为老年代的一部分看待。

G1收集器的工作过程大致可划分为以下四个步骤:

  1. 初始标记:仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS指针的值。这个阶段需要停顿用户线程,但耗时很短;(需要STW

    G1会为每个Region分配两个被称为TAMS的指针,发回收时新分配的对象地址都必须要在这两个指针之间。

  2. 并发标记:从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行;
  3. 最终标记:对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的记录;(需要STW
  4. 筛选回收:负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。(需要STW

5.1 优势

  •  能充分利用多CPU、多核环境下的硬件优势;
  •  能独立管理整个GC堆(新生代和老年代),而不需要与其他收集器搭配;
  •  不会产生内存碎片,有利于长时间运行;
  •  除了追求低停顿处,还能建立可预测的停顿时间模型;
    G1收集器是当今收集器技术发展的最前沿成果。

5.2 劣势

G1 需要记忆集 (具体来说是卡表)来记录新生代和老年代之间的引用关系,这种数据结构在 G1 中需要占用大量的内存,可能达到整个堆内存容量的 20% 甚至更多。而且 G1 中维护记忆集的成本较高,带来了更高的执行负载,影响效率。
按照《深入理解Java虚拟机》作者的说法,CMS 在小内存应用上的表现要优于 G1,而大内存应用上 G1 更有优势,大小内存的界限是6GB到8GB。所以,尽管是最前沿的成果,也不是完美无缺的。

关于G1的详细信息可以查看以下两篇文章:

  1. 美团技术团队-Java Hotspot G1 GC的一些关键技术
  2. GC - Java 垃圾回收器之G1详解

5.3 使用场景

个人以为G1已经基本全面压制cms、parallel等回收器,缺点见上面的劣势。但如果不是追求极致的性能,基本可以无脑G1。

以上只介绍了常用的收集器,还有较新的收集器,如:ShenandoahZGC等,后续再进行讨论。