跳到主要内容

分代回收、屏障与安全点

分代收集利用“大多数对象很快失效”的工作负载特征,频繁回收年轻对象,较少扫描长期存活对象。为了只扫描部分堆,收集器必须记录跨区域引用;为了并发标记,还要维护对象图变化带来的正确性。

1. 分代是假设,不是 Java 语义

典型分代堆把新对象放入年轻代,存活若干轮后晋升到老年代。年轻代收集主要处理新对象,老年代收集频率较低。

这个策略对短命临时对象多的服务有效,但 JVM 规范没有要求堆必须分代。不同收集器的代、Region、晋升和回收周期也不相同。

“Minor GC”“Major GC”“Full GC”在不同收集器和日志工具中的含义不完全一致。诊断应看收集器名称、具体 phase、回收范围和是否 Stop-The-World。

2. 只扫描年轻代需要知道老年代谁指向它

oldOrder.latestEvent = new Event();

若只扫描年轻代和线程根,却不扫描整个老年代,就可能漏掉这个从老对象指向新对象的引用。

HotSpot 收集器通常通过写屏障在引用写入时维护 card table、remembered set 或类似结构。年轻代收集时扫描这些摘要,而不是遍历全部老对象。

屏障是 JVM 在引用访问周围插入的小段逻辑,不是 CPU 内存屏障的同义词。它会增加每次读写成本,以换取较小的回收扫描范围或并发能力。

3. 并发标记期间对象图仍在变化

收集器和应用线程同时运行时,应用可能:

  • 删除一条原有引用。
  • 新增从已扫描对象到未扫描对象的引用。
  • 移动或覆盖对象关系。

如果不记录这些变化,收集器可能漏标仍存活的对象。不同算法采用 SATB、增量更新、读屏障、写屏障或组合策略维护标记不变量。

理解屏障时先问它保护什么正确性条件,再看具体收集器实现。把 G1、ZGC 和 Shenandoah 的屏障机制混成一套固定步骤,会得到错误结论。

4. 安全点提供一致的线程状态

JVM 在执行某些全局操作时,需要线程到达可以准确解释栈和对象引用的位置。HotSpot 通过 safepoint 或更细粒度的 thread-local handshake 协调这些操作。

安全点相关停顿可以拆成:

总停顿 = 等待线程到达安全状态 + JVM 在停顿中执行的工作

GC 日志里的某个 phase 很短,不代表应用停顿必然同样短;线程进入安全点的延迟、引用处理、类卸载等都可能增加时间。

5. Stop-The-World 不是“所有 GC 都全程停顿”

Serial 和 Parallel 的主要回收工作在停顿中完成。G1、ZGC、Shenandoah 会把部分标记、整理或重定位与应用并发,但仍存在很短或不同用途的 STW 阶段。

评价收集器要同时看:

  • 暂停次数和分位数。
  • GC 与屏障消耗的 CPU。
  • 应用吞吐。
  • 并发周期所需堆余量。
  • 分配停滞和退化回收。

低停顿不是零停顿,也不意味着没有吞吐代价。

6. 晋升与分配失败会改变回收路径

年轻对象存活后需要复制或晋升。如果目标空间不足,可能出现 evacuation failure、promotion failure、分配停滞或更重的回收路径,具体名称取决于收集器。

常见诱因:

  • live set 接近堆上限。
  • 分配速率突然升高。
  • 大对象或跨代引用过多。
  • 并发标记启动太晚,旧代没有足够空闲空间。

这类问题不能只通过“降低停顿目标”解决。更激进的停顿目标可能让每次回收工作更少、频率更高,并增加并发周期压力。

7. 从统一日志观察过程

java -Xlog:gc*,safepoint:file=gc.log:time,uptime,level,tags -jar app.jar

重点关联:

  • GC 原因和回收类型。
  • 各阶段持续时间。
  • 回收前后 used、committed 和 live set。
  • 并发周期是否及时完成。
  • safepoint 到达时间与停顿执行时间。
  • 同期分配率、吞吐和请求延迟。

先用默认日志建立时间线,再按问题打开更细 tag,避免一次记录大量调试日志干扰生产。

8. 常见问题

8.1 写屏障和 volatile 的内存屏障是一回事吗

不是。GC 写屏障维护收集器所需的引用信息或标记不变量;Java 内存模型相关屏障约束跨线程内存顺序。底层实现可能都生成额外指令,但目的不同。

8.2 安全点只会由 GC 触发吗

不会。去优化、类重定义、偏向撤销的历史实现、某些诊断命令等 JVM 操作也可能需要全局协调。具体原因从 safepoint/JFR 事件判断。

8.3 年轻代越大,性能一定越好吗

不一定。更大年轻代可能降低收集频率,却增加单次扫描与复制工作,还会挤压老年代余量。应按分配率、存活率和延迟目标验证。

9. 面试题

9.1 为什么分代收集需要写屏障和 Remembered Set

出现公司:阿里巴巴

考察重点

  • 跨代引用会让局部扫描漏掉存活对象。
  • 屏障在写入时维护引用摘要。
  • 空间换取扫描时间的权衡。

相关内容:第 1 节“分代是假设,不是 Java 语义”至第 3 节“并发标记期间对象图仍在变化”。

参考回答

只回收年轻代时,如果老对象指向年轻对象而收集器完全不扫描老年代,就会漏掉存活引用。JVM 在引用写入周围执行写屏障,把可能包含跨代引用的卡页或来源 Region 记录到 remembered set,回收时只扫描这些摘要。

并发标记还要处理应用线程持续修改对象图的问题,具体用 SATB、增量更新或其他屏障取决于收集器,不能把所有机制混为一谈。

9.2 什么是安全点,GC 为什么需要它

出现公司:阿里云

考察重点

  • JVM 在可解释线程状态上执行全局操作。
  • 到达安全点的时间和停顿工作时间。
  • 并发收集器仍有 STW 阶段。

相关内容:第 4 节“安全点提供一致的线程状态”和第 5 节“Stop-The-World 不是‘所有 GC 都全程停顿’”。

参考回答

安全点是 JVM 能准确解释线程执行状态和引用位置、可以协调全局操作的状态。GC 的某些阶段需要稳定根集合或更新全局结构,会请求线程到达安全点;等待线程到达的时间也属于用户感知停顿的一部分。

并发收集器只是把大量工作与应用并发,仍有初始标记、重映射或其他短暂停顿。分析要区分到达安全点和停顿内工作。