JVM 从入门到入土 ⑤:垃圾回收 GC

深入学习 JVM-JVM 安全点和安全区域
垃圾收集主要是针对 方法区 进行
程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后也会消失,因此不需要对这三个区域进行垃圾回收

简历:熟悉 GC 常用算法,熟悉常见垃圾回收器,具有实际 JVM 调优实战经验

1. 定位垃圾

引用计数算法(ReferenceCount)

给对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。

缺点: 两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。
正因为循环引用的存在,因此 Java 虚拟机不使用引用计数算法。

1
2
3
4
5
6
7
8
9
10
11
public class ReferenceCountingGC {

public Object instance = null;

public static void main(String[] args) {
ReferenceCountingGC objectA = new ReferenceCountingGC();
ReferenceCountingGC objectB = new ReferenceCountingGC();
objectA.instance = objectB;
objectB.instance = objectA;
}
}

可达性分析算法(RootSearching)

通过 GC Roots 作为起始点进行搜索,能够到达到的对象都是存活的,不可达的对象可被回收

GC Roots 一般包含以下内容:

  • 虚拟机栈中局部变量表中引用的对象
  • 本地方法栈中 JNI 中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中的常量引用的对象

2. 常见的垃圾回收算法

2.1. 标记清除(mark sweep):

将存活的对象进行标记,然后清理掉未被标记的对象

扫描两次:

  1. 一次扫描先标记存活对象
  2. 再一次扫描清除未被标记对象

缺点: 扫描两次,位置不连续,产生碎片
存活对象比较多的情况下效率较高

2.2. 拷贝算法(copying)

将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理

缺点:需移动对象,浪费空间
优点:扫描一次,没有碎片
适用于存活对象比较少的情况

2.3. 标记压缩(mark compact)

让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存

缺点:扫描两次,需移动对象,效率低下
优点:没有碎片,方便对象分配,不会产生内存减半

2.4. 分代收集

现在的商业虚拟机采用分代收集算法,它根据对象存活周期将内存划分为几块,不同块采用适当的收集算法

  • 新生代使用: 复制算法
  • 老年代使用: 标记 - 清除 或者 标记 - 整理 算法

除 Epsilon ZGC Shenandoah 之外的 GC 都是使用逻辑分代模型
G1 是逻辑分代,物理不分代
除此之外的不仅逻辑分代,而且物理分代

3. 常见的垃圾回收器

Card Table
由于做 YGC 时,需要扫描整个 OLD 区,效率非常低,所以 JVM 设计了 CardTable, 如果一个 OLD 区 CardTable 中有对象指向 Y 区,就将它设为 Dirty,下次扫描时,只需要扫描 Dirty Card
在结构上,Card Table 用 BitMap 来实现

垃圾回收器:

连线表示垃圾收集器可以配合使用

垃圾收集器跟内存大小的关系:

  1. Serial 几十兆
  2. PS 上百兆 - 几个 G
  3. CMS - 20G
  4. G1 - 上百 G
  5. ZGC - 4T - 16T(JDK13)

STW: stop the world。指 GC 中让用户线程全部暂停而产生的停顿。任何一个垃圾回收器都有 STW,减少 STW 能提升用户响应时间,但也会减少吞吐量。

Serial


用于收集年轻代垃圾的收集器,只会使用 一个 GC 线程 进行垃圾收集工作。

Parallel Scavenge

用于收集年轻代垃圾的收集器,使用多个 GC 线程收集垃圾

关注吞吐量,目标是 达到一个可控制的吞吐量,它被称为 “吞吐量优先” 收集器

  • 高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运算任务,适合在后台运算而不需要太多交互的任务
  • 缩短停顿时间是以牺牲吞吐量和新生代空间来换取的:新生代空间变小,垃圾回收变得频繁,导致吞吐量下降

ParNew

用于收集年轻代垃圾的收集器。新版本的 Parallel Scavenge,与其相比做了增强以便能与 CMS 配合使用

HotSpot Virtual Machine Garbage Collection Tuning Guide

  • ParNew 响应时间优先(配合 CMS)
  • Parallel Scavenge 吞吐量优先

Serial Old

用于收集老年代垃圾的收集器。Serial 收集器的老年代版本。

  • 在 JDK 1.5 以及之前版本(Parallel Old 诞生以前)中与 Parallel Scavenge 收集器搭配使用
  • 作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用(效率低下)

Parallel Old

用于收集老年代垃圾的收集器。Parallel Scavenge 收集器的老年代版本。

ConcurrentMarkSweep

用于收集老年代垃圾的收集器。垃圾回收和应用程序同时运行,降低 STW 的时间。

流程:
在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿

  1. 初始标记: 仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿
  2. 并发标记: 进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿
  3. 重新标记: 为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿
  4. 并发清除: 不需要停顿

CMS 的问题:
CMS 问题比较多,所以现在没有一个版本默认是 CMS,只能手工指定
CMS 既然是 MarkSweep,就一定会有碎片化的问题,碎片到达一定程度,CMS 的老年代分配对象分配不下的时候,使用 SerialOld 进行老年代回收

  • 吞吐量低: 低停顿时间是以牺牲吞吐量为代价的,导致 CPU 利用率不够高
  • 无法处理浮动垃圾:
    • 浮动垃圾: 指并发清除阶段由于用户线程继续运行而产生的垃圾。这部分垃圾只能到下一次 GC 时才能进行回收
    • 需预留内存: 由于浮动垃圾的存在,因此需要预留出一部分内存。这意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收
    • Concurrent Mode Failure: 如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS
    • 解决方案:
      降低触发 CMS 的阈值
      –XX:CMSInitiatingOccupancyFraction 92%:内存打到 92% 时才会触发 CMS。可以降低这个值,让 CMS 保持老年代足够的空间
  • 空间碎片: 标记 - 清除算法导致的空间碎片,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC
    -XX:+UseCMSCompactAtFullCollection
    -XX:CMSFullGCsBeforeCompaction 默认为 0 指的是经过多少次 FGC 才进行压缩

算法:三色标记 + Incremental Update

G1

特点:

  • 并发收集
  • 压缩空间,不会延长 GC 的暂停时间
  • 更容易预测 GC 的暂停时间
  • 适用不需要实现很高吞吐量的场景
  • 每个内存区域不是固定的,可能这次存放新生代下次就存放老年代了
  • 新老年代比例: 5%-60%。不用手工指定,G1 以此作为预测停顿时间的基准

Edne、Survivor、Old、Humongous(超过单个 Region 的 50%)

CSet(Collection Set):

  • 一组可被回收的分区的集合
  • 在 CSet 中存活的数据会在 GC 过程中被移动到另一个可用分区
  • CSet 中的分区可以来自 Eden 空间、Survivor 空间或老年代
  • CSet 占用不到整个堆空间的 1% 大小

RSet(Remembered Set):

  • 是一块存放在 Region 内部的 Map,记录了其他 Region 中的对象到本 Region 的引用
  • 使垃圾回收器不用去扫描整个堆来获取引用了当前分区的对象,只需要扫描 RSet 即可

当对象无法分配时也会产生 FullGC,如何解决:

  • 扩内存
  • 提高 CPU 性能(回收的快,业务逻辑产生对象的速度固定,垃圾回收越快,内存空间越大)
  • 降低 MixedGC 触发的阈值,让 MixedGC 提早发生(默认是 45%)

    MixedGC:类似 CMS
    XX:InitiatingHeapOccupacyPercent

    • 默认值 45%
    • 当分配堆空间超过这个值,启动 MixedGC

算法:三色标记 + SATB
三色标记:

  • 黑色:自身和成员变量均已标记完成
  • 灰色:自身被标记,成员变量未被标记
  • 白色:未被标记的对象

漏标:
当一个黑色的对象引用了一个白色对象,且这个白色对象只被这个黑色对象引用时会漏标
该白色对象已无法被遍历到了

解决漏标的方案:

  • Incremental update:关注引用的增加。黑色的对象引用白色对象时把黑色对象重新标记为灰色,下次重新扫描属性,CMS 使用
  • SATB(snapshot at the beginning):关注引用的删除。当引用消失时把这个引用推到 GC 的堆栈,保证引用的对象还能被 GC 扫描到。

G1 使用 SATB,Incremental update 把黑色对象变为灰色对象后,后续还得对其成员变量进行扫描,效率不高。
SATB 配合 RSet 判断记录的白色对象是否为漏标对象
垃圾优先型垃圾回收器调优

其他

  1. ZGC (1ms) PK C++
    算法:ColoredPointers + LoadBarrier
  2. Shenandoah
    算法:ColoredPointers + WriteBarrier
  3. Epsilon

4. 常见垃圾回收器组合

  • -XX:+UseSerialGC = Serial New (DefNew) + Serial Old:小型程序。默认情况下不会是这种选项,HotSpot 会根据计算及配置和 JDK 版本自动选择收集器

  • -XX:+UseParNewGC = ParNew + SerialOld:这个组合已经很少用(在某些版本中已经废弃)

    Why Remove support for ParNew+SerialOld and DefNew+CMS in the future?

  • -XX:+UseConc(urrent)MarkSweepGC = ParNew + CMS + Serial Old

  • -XX:+UseParallelGC = Parallel Scavenge + Parallel Old (1.8 默认) 【PS + SerialOld】

  • -XX:+UseParallelOldGC = Parallel Scavenge + Parallel Old

  • -XX:+UseG1GC = G1

Linux 中没找到默认 GC 的查看方法,而 windows 中会打印 UseParallelGC

  • java +XX:+PrintCommandLineFlags -version
  • 通过 GC 的日志来分辨