G1之YGC理论

479 阅读5分钟

YGC 触发的时机

G1会控制停顿时间,尽量去满足用户设定的最大停顿时间目标,即 -XX:MaxGCPauseMillis

G1会根据当前堆的使用情况和历史GC数据,预测下一次GC的停顿时间。如果预测的停顿时间远小于 MaxGCPauseMillis,G1会增加Eden区的region,直到某次Eden区放满时,计算的停顿时间接近MaxGCPauseMillis,会触发YGC。

-XX:G1NewSizePercent-XX:G1MaxNewSizePercent 等参数也会影响年轻代的大小,从而影响 GC 的行为。

YGC 的执行流程

G1 YGC.drawio.png

STW

判断是否需要并发标记,如果需要,记录在YCG中标记为存活并复制到Survivor区的对象,作为并发标记的起点。

并行执行阶段

1. 选择CSet

CSet是指本次GC中要被回收的Region集合。选择的依据主要为以下几点

  • 回收价值,垃圾比例 = (Region 大小 - 活对象大小) / Region 大小。
  • 预测的停顿时间,如果某个Region的回收价值很高,但是停顿时间会很长,可能会被延迟到下一次GC。

2. 处理根集合

从GC Roots遍历,查找从Roots直达到收集集合的对象,移动他们到Survivor区域的同时将他们的引用对象加入标记栈。

3. 处理RSet

确保跨代引用不会被误删,将RSet作为Roots,从RSet出发,标记老年代中指向年轻代的引用,移动他们到Survivor区域的同时将他们的引用对象加入标记栈。

4. 复制对象到Survivor区

遍历标记栈,将栈内的所有所有的对象移动至Survivor区域。

串行处理阶段

1. JIT编译代码位置调整

  • 背景:在 Java 中,方法的字节码在运行过程中会被 JIT(Just-In-Time)即时编译器优化为本地机器码,以提升执行效率。为了提高性能,JIT 编译器会进行激进优化,比如:方法内联;引用地址缓存;逃逸分析后的对象消除等。
  • 调整的原因:
    • 在垃圾回收过程中,对象在内存中的位置发生了变化(例如从 Eden 区复制到 Survivor 区或晋升到 Old 区),原来的引用地址就失效了
    • 而 JIT 编译器在本地代码中可能会缓存了对象的旧地址或偏移,如果不调整,继续使用旧地址会访问到错误的位置,甚至导致 JVM 崩溃。
  • 调整的方式:JVM 会在 GC 后检查和更新这些地址缓存,会修复 JIT 编译生成的代码中所有直接引用对象地址的地方。

2. 引用处理

清理软引用、弱引用、虚引用等,注意引用处理可以并行,但需要特定参数开启。

  • 强:不可达时才会被回收;
  • 弱:当GC内存不足时才进行回收;
  • 软:内存不足时回收;
  • 虚:GC后通知。
YGC 中处理软、弱、虚引用的流程
  1. 发现阶段:
    • GC 会扫描所有对象,在发现 java.lang.ref.Reference 的子类对象时,把它们加入到对应的 Reference Queue(内部数据结构),不立即处理其 referent。
  2. 判断 referent 是否存活:
    • 如果 referent 在 tracing(从 GC Roots 遍历)中被标记为存活,那么:
      • 将该引用对象的状态设为“已处理”,保持引用;
      • 不将该引用对象加入 ReferenceQueue;
    • 如果 referent 没有被标记为存活,且满足特定条件(如内存不足),则:
      • 将引用对象加入 ReferenceQueue;
      • 其 referent 将成为 GC 的回收目标。
  3. 清理阶段(Enqueue):
    • GC 会将处理结果为“不可达”的引用对象入队(enqueue)到其关联的 ReferenceQueue;
    • 应用线程可以从 ReferenceQueue 中取出这些对象,执行相应的清理逻辑。
为什么要单独处理引用对象?
  • 语义要求不同

    • 普通对象只需要通过可达性判断即可;

    • 引用对象需要结合GC策略、内存压力、用户自定义逻辑决定保留referent;

    • 不能简单的复制清理,需要额外的处理逻辑。

3. 字符串去重

  • 字符串去重只会处理那些经历过几次垃圾收集仍然存活的字符串,这确保多数生命周期很短的字符串不会被处理。字符串的这个最小存活年龄是通过JVM参数-XX:StringDeduplicationAgeThreshole=3管理的(3是默认值)。
  • 由单独的去重线程完成。开始去重时,会先查找字符数组是否存在,若存在则调整指针,共享字符串数组,释放多余的字符串数组。

4. Redirty处理,重建RSet

在对象复制后,其地址已经发生了改变。老年代引用的年轻代对象所在的Rset还没有修改,所以要将旧的RSet进行清理,并在复制后的年轻代对象所在的Region构建新的RSet。

5. 尝试回收大对象

  • 大对象占用空间更多,回收性价比高
  • 大对象单独在一个Region当中,回收操作容易

6. 释放CSet,对象回收

回收CSet当中的Region,将这些Region给释放,加入到自由分区

7. 尝试启动并发标记

当老年代内存使用率达到45%,在YGC结束后开启一次并发标记过程