YGC 触发的时机
G1会控制停顿时间,尽量去满足用户设定的最大停顿时间目标,即 -XX:MaxGCPauseMillis。
G1会根据当前堆的使用情况和历史GC数据,预测下一次GC的停顿时间。如果预测的停顿时间远小于 MaxGCPauseMillis,G1会增加Eden区的region,直到某次Eden区放满时,计算的停顿时间接近MaxGCPauseMillis,会触发YGC。
-XX:G1NewSizePercent和-XX:G1MaxNewSizePercent等参数也会影响年轻代的大小,从而影响 GC 的行为。
YGC 的执行流程
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 中处理软、弱、虚引用的流程
- 发现阶段:
- GC 会扫描所有对象,在发现
java.lang.ref.Reference的子类对象时,把它们加入到对应的 Reference Queue(内部数据结构),不立即处理其 referent。
- GC 会扫描所有对象,在发现
- 判断 referent 是否存活:
- 如果 referent 在 tracing(从 GC Roots 遍历)中被标记为存活,那么:
- 将该引用对象的状态设为“已处理”,保持引用;
- 不将该引用对象加入 ReferenceQueue;
- 如果 referent 没有被标记为存活,且满足特定条件(如内存不足),则:
- 将引用对象加入 ReferenceQueue;
- 其 referent 将成为 GC 的回收目标。
- 如果 referent 在 tracing(从 GC Roots 遍历)中被标记为存活,那么:
- 清理阶段(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结束后开启一次并发标记过程