经典的垃圾回收器

Posted 杂货铺的学习之路

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了经典的垃圾回收器相关的知识,希望对你有一定的参考价值。

经典的垃圾回收器


从不同的角度分析垃圾回收器,可以将 GC 分为不同的类型。

GC 的分类

按照线程数划分:串行垃圾回收器、并行垃圾回收器。
按照碎片处理方式划分:压缩式垃圾回收器、非压缩式垃圾回收器。
按照工作的内存区间划分:年轻代垃圾回收器、老年代垃圾回收器。
按照工作模式划分:并发式垃圾回收器、独占式垃圾回收器。

(图 1:串行与并行垃圾回收器)

经典的垃圾回收器

(图 2:独占式与并发垃圾回收器)

图 1 的串行回收默认被应用在客户端的 Client 模式下的 JVM 中。和串行回收相反的是并发回收可以运用多个 CPU 同时执行垃圾回收。不过并行回收仍然和串行回收一样,都是采用独占式,使用了"Stop-the-world"机制。独占式回收器一旦运行,就停止应用中所有的用户线程,直到垃圾回收过程结束。并发式垃圾回收器与之相反,它可以在应用运行时执行垃圾回收。

GC 的性能指标

吞吐量:运行用户代码的时间占总运行时间(程序运行时间 + 内存回收时间)的比例。
暂停时间:执行垃圾回收时,程序的工作线程被暂停的时间。
内存占用:Java 堆区占用内存的大小。
吞吐量优先,程序能容忍较高的暂停时间。因此高吞吐量的程序有更长的时间基准,快速响应是不必考虑的。
暂停时间优先,意味着尽可能让单次 STW 的时间最短。


高吞吐量与低暂停时间是一对矛盾的目标。
如果选择吞吐量优先,那么必然需要降低内存回收的执行频率,就需要 GC 更长的暂停时间来执行内存回收。相反的,选择低暂停时间(低延迟),为了降低每次执行内存回收时的暂停时间,就需要频繁地执行内存回收,但这又引起了程序吞吐量的下降。
现在的标准:在最大吞吐量优先的情况下,降低暂停时间。

7 种经典的垃圾回收器

串行垃圾回收器:Serial、Serial Old。
并行垃圾回收器:ParNew、Parallel Scavenge(简称:Parallel)、Parallel Old。
并发垃圾回收器:CMS、G1。

经典的垃圾回收器

(图 3 :垃圾回收器的组合关系)

经典的垃圾回收器

(图 4 :垃圾回收器的区别)

图 3 表示垃圾回收器之间的组合关系。其中 Serial Old 作为 CMS 出现"Concurrent Mode Failure"失败后的后备预案。红色虚线,在 JDK 8 中声明为废弃,并在 JDK 9 中完全取消了这些组合的支持,即:移除。绿色虚线,JDK 14 中弃用组合。青色虚线,JDK 14 中删除 CMS。

查看默认垃圾回收器相关命令

-XX:+PrintCommandLineFlags:查看命令行相关参数。
使用命令行指令查看使用的回收器:jinfo -flag 相关垃圾回收器参数 进程ID

Serial GC

JDK 1.3 之前新生代唯一的选择。为 HotSpot 中 Client 模式下的默认新生代垃圾回收器。
Serial 采用复制算法、串行回收、STW 机制的方式执行内存回收。
Serial Old 采用串行回收和 STW 机制,内存回收算法采用的是标记-压缩算法。
Serial Old 在 Server 模式下主要有两个用途:Parallel + Serial Old、作为老年代 CMS 收集器的后背方案。

经典的垃圾回收器

(图 5 :Serial + Serial Old)

优势:简单高效(与其他回收器的单线程比)。在 HotSpot 虚拟机中,使用 -XX:+UseSerialGC 参数指定年轻代和老年代都是用串行回收器。

ParNew GC

ParNew GC 为 Serial GC 的多线程版本。ParNew 除了采用了并行回收的方式执行内存回收外,两者几乎没有任何区别。

经典的垃圾回收器

(图 6 : ParNew + Serial Old)

图 6 对于新生代,回收次数频繁,使用并行方式高效。对于老年代,回收次数少,使用串行方式节省资源(切换线程的资源)。除了 Serial 外,只有 ParNew 能与 CMS 配合工作。
-XX:+UseParNewGC 手动指定 ParNew 回收器执行内存回收任务。它表示年轻代使用并行收集器,不影响老年代。
-XX:ParallelGCThreads 现在线程数量,默认开启和 CPU 数量相同的线程数。

Parallel GC(吞吐量优先)

HotSpot 的年轻代中除了 ParNew 是基于并行回收以外,Parallel Scavenge 同样采用了复制算法、并行回收和 STW 机制。 与 ParNew 收集器不同的是,Parallel 的目标是达到一个可控制的吞吐量,他也被称为吞吐量优先的垃圾回收器。 Parallel 在 JDK 1.6 时提供了用于执行老年代的垃圾回收器 Parallel Old,来代替 Serial Old。
Parallel Old 采用标记-压缩算法,同样基于并行回收和 STW 机制。
JDK 8 中,默认的垃圾回收器组合 Parallel + Parallel Old。
-XX:+UseParallelGC -XX:+UseParallelOldGC 两者互相激活。
-XX:MaxGCPauseMillis 设置垃圾回收器最大停顿时间,单位毫秒。一般不设置。
-XX:GCTimeRatio 垃圾回收时间占总时间的比例(1/(N+1)),衡量吞吐量的大小。取值范围是(0, 100),默认 99。
-XX:UseAdaptiveSizePolicy 设置 Parallel 的自适应调节策略。这个模式下,年轻代的大小、Eden 和 Survivor 的比例、晋升老年代的对象年龄等参数会自动调整。

CMS GC(低延迟)

CMS(Concurrent-Mark-Sweep)是 HotSpot 虚拟机中第一款并发回收器,它第一次实现让垃圾收集线程与用户线程同时工作。
CMS 收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间。
CMS 采用标记-清除算法,并且也会 "Stop-the-world"。
CMS 作为老年代的回收器,无法与 Parallel 配合工作。新生代只能选择 ParNew 或 Serial 中的一个。

经典的垃圾回收器图 7 :CMS GC)


初始标记:这个阶段的主要任务仅仅只是标记出 GC Roots 能直接关联到的对象。
并发标记:从 GC Roots 的直接关联对象开始遍历整个对象图的过程。不需要停顿用户线程,可以与垃圾收集线程一起并发运行。
重新标记:修正并发标记期间,因用户程序继续运作导致标记产生变动的那一部分对象的标记记录。
并发清理:清理删除标记阶段判断的已经死亡的对象,释放内存空间。

尽管 CMS 采用的是并发回收(非抢占式),但是在其初始化标记和再次标记这两个阶段仍然需要执行 STW 机制暂停程序中的工作线程,不过时间不会太长。由于最耗时的并发标记与并发清除阶段不需要暂停工作,所以整体的回收是低停顿的。
由于 CMS 在垃圾回收阶段用户线程没有中断,所以在 CMS 回收过程中,还应该确保应用程序有足够的内存可用。因此 CMS 不能如其他回收器那样等到老年代几乎完全被填满了在进行回收,而是当堆内存使用率达到某一阈值时,就开始回收,确保程序在 CMS 工作中依然有足够的空间支持程序运行。如果 CMS 运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败。这时虚拟机将启动后备预案:临时启动 Serial Old 来对老年代进行垃圾回收,停顿时间就会很长了。
CMS 采用的垃圾回收算法是标记-清除算法,这就意味着将会不可避免的产生一些内存碎片。在 CMS 为新对象分配内存时,将无法使用指针碰撞技术,而只能选择空闲列表执行内存分配。

经典的垃圾回收器

(图 8 :标记-清除)

CMS 的弊端:会产生内存碎片,导致并发清除后,用户线程空用空间不足。在无法分配大对象时,不得不提前触发 Full GC。无法处理浮动垃圾,在并发标记阶段如果产生新的垃圾对象,CMS 无法对这些垃圾对象进行标记,最终会导致这些新产生的垃圾对象没有被及时回收,只能在下一次执行 GC 时释放这些之前未被回收的内存空间。
-XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupanyFraction :设置堆内存使用率的阈值,JDK 5 默认 68%,JDK 6 后默认 92%。

Garbage First GC

官方给 G1 设定的目标是在延迟可控的情况下获得尽可能高的吞吐量,所以才当前“全功能回收器”的重任与期望。JDK 9 及之后的默认。
G1 是一个并行回收器,它把堆内存分割为很多不相关的区域(Region)物理上不连续,使用不同的 Region 来表示 Eden、Survior 0、Servior 1、老年代。G1 跟踪各个 Region 里面的垃圾堆积的价值大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region。
G1 使用了全新的分区算法,有如下特点
  • 并行与并发
    • 并行性,在回收期间,可以有多个 GC 线程同时工作,此时用户线程 STW。
    • 并发性,G1 部分工作可以和应用出现同时执行。
  • 分代收集
    • G1 仍然属于分代型垃圾回收器,它还是会区分年轻代、老年代,但从堆的结构上看,它不要求整个代区连续,也不再坚持固定大小和固定数量。
    • 将堆空间分为若干个区域,这些区域中包含逻辑上的年轻代和老年代。它同时兼顾年轻代和老年代的回收。
  • 空间整合
    • G1 内存回收是以 Region 作为基本单位的,Region 之间是复制算法。避免内存碎片,有利于程序的长久运行。

-XX:+UseG1GC -XX:G1HeapRegionSize

G1 它将整个 Java 堆区划分为约 2048 个大小相同的独立 Region 块,每个 Region 块的大小根据堆空间的实际大小而定,控制在 1 - 32 M 之间且为 2 的 N 次幂。虽然还保留有新生代和老年代的概念。它们是通过 Region 的动态分配方式实现逻辑上的连续。

经典的垃圾回收器

(图 9 :Region)

G1 还新增了一种新的内存区域,叫做 humongous。主要存储大对象,如果超过 1.5 个 Region,就放到 H。
对于堆中的大对象,默认会被分配到老年代,若是它是一个短期存在的大对象,就会对垃圾回收器造成负面影响。为了解决这个问题,G1 划分了一个 humongous 区,它用来存放大对象。如果一个 H 区装不下一个大对象,那么 G1 会寻找连续的 H 区来存储。为了能找到连续的 H 区,有时候不得不启动 Full GC。G1 大多数行为都把 H 区作为老年代的一部分看待。
G1  垃圾回收过程主要有三个环节:年轻代 GC(Young GC)、老年代并发标记过程(Concurrent Marking)、混合回收(Mixed GC)。若是需要,Full GC 还是继续存在,它针对 GC 的评估失败提供了一种失败保护机制,即强力回收。

(图 10 :G1 回收器的回收过程)

当年轻代的 Eden 区用尽时开始年轻代回收过程。G1 的年轻代收集是一个并行的独占式回收器。在收集年轻代时,暂停所有的应用程序线程,启动多线程执行年轻代的回收。将年轻代区间存活对象到 Servicor 区或老年区间。当堆内存使用到达默认 45% 时,开始老年代并发标记过程。标记完成马上开始混合回收过程。G1 的老年代不需要整个老年代被回收,一次只需要扫描/回收一部分老年代的 Region 就可以了。

记忆集(Remembered Set)
一个 Region 不可能是孤立的,一个 Region 中的对象可能被其他任意 Region 中对象引用,那么再判断对象存活时,是否需要扫描整个 Java 堆才能确保?
为了解决上述问题,不论是 G1 还是其他分代收集器,JVM 都是使用 Remembered Set 来避免全局扫描,每个 Region 都有一个对应的记忆集。

(图 11 :Remembered Set)

每次 Reference 类型写入时,都会产生一个 Write Barrier 暂时中断操作。
检查写入引用的对象是否在和引用类型再不同的 Region(其他收集器,检查老年代对象是否引用了新生代对象)。如果不同,会通过 CardTable 把相关引用信息记录到引用指向对象所在的 Region 对应的记忆集中。


参考资料

《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)》周志明 

《JVM 从入门到精通(第一部分)》宋红康

以上是关于经典的垃圾回收器的主要内容,如果未能解决你的问题,请参考以下文章

经典垃圾回收器

JVMJava 中的经典垃圾回收器

JVM垃圾回收篇(经典垃圾回收器讲解)

JVM垃圾回收篇(经典垃圾回收器讲解)

JVM系列之经典垃圾回收器(上篇)

经典面试题 | 讲一下垃圾回收器都有哪些?