对象何时可以回收

JVM 主流实现通过可达性分析判断对象是否存活。从 GC Roots 出发沿引用关系遍历,不可达对象才具备被回收的条件。GC Roots 常见来源包括线程栈中的引用、静态字段引用、JNI 引用和 JVM 内部引用。

引用计数无法可靠处理循环引用,因此不是 HotSpot 判断对象存活的主要算法。

三类基础回收算法

  • 标记—清除:标记存活对象后清理其余空间,速度直接但会产生碎片。
  • 标记—复制:把存活对象复制到另一块区域,分配简单但需要预留空间。
  • 标记—整理:将存活对象向一端移动,消除碎片,但移动对象有成本。

分代回收根据对象生命周期采用不同策略。年轻代对象死亡率高,适合复制;老年代存活率高,更偏向标记清除或标记整理。

Stop The World 与安全点

部分 GC 阶段需要暂停应用线程以获得一致的对象关系视图,这就是 STW。停顿时间不只受堆大小影响,也受存活对象数量、引用处理、类卸载和系统负载影响。

线程通常在安全点暂停。排查长停顿时要区分“到达安全点耗时”和“GC 工作耗时”。

常见收集器

G1

G1 将堆划分为多个 Region,跟踪各区域的回收价值,并以可预测停顿为目标选择回收集合。它仍可能发生 Full GC;MaxGCPauseMillis 是软目标,不是严格保证。

ZGC

ZGC 通过并发标记、并发转移和染色指针等技术减少停顿,适合大堆和低延迟场景。选择前要确认 JDK 版本、平台支持、吞吐损耗和团队运维经验。

Parallel GC

Parallel GC 强调吞吐量,适合批处理等对单次停顿不敏感的任务。

如何选择与调优

先定义目标:吞吐量、P99 延迟、内存成本还是启动速度。然后使用与生产相同的流量模型压测。不要一开始就堆叠大量 JVM 参数,现代 JDK 的默认选择通常是合理起点。

排查顺序建议:

  1. 开启统一 GC 日志并记录时间戳。
  2. 观察分配速率、晋升速率、堆使用量和停顿分位数。
  3. 判断是内存泄漏、堆配置不足、对象分配过快还是收集器不匹配。
  4. 必要时结合堆转储、对象直方图和线程 dump 定位根因。

常见误区

  • System.gc() 只是请求,具体行为受 JVM 参数和实现影响。
  • 对象进入老年代不代表永远不会回收。
  • 堆越大不一定越好,更大的堆可能增加内存成本和某些阶段的处理量。
  • Minor GC、Major GC、Full GC 的术语在不同收集器和工具中可能含义不同,应以日志事件为准。

核心考点清单

  • 可达性分析从 GC Roots 出发,不可达对象才具备回收条件。
  • 回收算法包括标记清除、复制和标记整理,各有碎片与移动成本。
  • G1 面向可预测停顿,ZGC 面向超低停顿,Parallel GC 强调吞吐。
  • 调优必须先定义吞吐、P99 延迟和内存成本目标,再用日志和压测验证。

高频追问与参考回答

追问:什么对象可以作为 GC Root?

线程栈中的引用、静态字段引用、JNI 引用以及 JVM 内部持有的关键对象等。具体集合随实现与阶段而变化。

追问:堆越大停顿一定越长吗?

不一定,停顿还受收集器、存活对象量和回收阶段影响;但更大堆会提高内存成本,并可能增加部分阶段工作量。

机制全景图

下面把「JVM 垃圾回收算法与收集器选择」从输入到结果压缩成一条可复述的主链路。面试时先用图建立全局坐标,再进入局部实现,能避免只背零散结论。

flowchart LR
    A["对象持续分配"]
    A --> B["可达性标记"]
    B --> C["存活对象复制或整理"]
    C --> D["回收不可达空间"]
    D --> E["调整代际与并发周期"]

完整链路:从输入到结果

沿着「对象持续分配 → 可达性标记 → 存活对象复制或整理 → 回收不可达空间 → 调整代际与并发周期」观察输入、状态与输出,下面每个阶段都对应一个可以在源码、日志或系统表中验证的位置。

1. 对象持续分配

分配速率、对象大小和引用关系决定 GC 压力,吞吐问题常先从分配端产生。

2. 可达性标记

GC Roots 沿引用图标记存活对象,引用类型、类加载器和线程局部变量都会影响可达性。

3. 存活对象复制或整理

复制算法适合低存活率区域,标记整理减少碎片但移动成本更高,标记清除可能留下碎片。

4. 回收不可达空间

回收阶段可能 Stop-The-World 或与应用并发;并发收集仍有短暂停顿和额外 CPU。

5. 调整代际与并发周期

分代假设让年轻对象高频回收,晋升与老年代并发周期需要根据实际存活率和暂停目标调整。

源码与实现定位

入口 阅读重点
-Xlog:gc* 停顿、阶段与堆区域
JFR ObjectAllocationSample 对象分配热点

源码或系统表应按上表顺序追踪:先确认入口实际走到哪条路径,再用运行时数据验证,而不是仅凭类名或配置推测。

参数配置与可复现实验

java -Xms4g -Xmx4g -XX:+UseG1GC   -Xlog:gc*,safepoint:file=gc.log:time,uptime,level,tags -jar app.jar

以真实对象存活率跑 30 分钟稳态和 5 倍峰值;对比 GC 前后占用、分配、晋升和业务 P99。

验证步骤与预期结果

1. 固定输入和基线

先在没有故障注入的环境执行上述配置,固定数据规模、并发度、运行时版本和预热时间。以「GC 后堆」为主基线,记录值应满足「稳态水平线」;同时保存 分配速率、停顿 P95/P99,使后续变化能够回到同一时间轴比较。

2. 从实现入口确认路径

在「-Xlog:gc*」确认请求确实进入「停顿、阶段与堆区域」对应的实现,再沿「JFR ObjectAllocationSample」观察「对象分配热点」。如果入口路径都未命中,就不应继续调整下游参数,而应先检查调用条件、版本或路由是否与假设一致。

3. 注入本文特有的失败模式

优先复现「只调停顿目标不降低分配速率」,并把单一变量逐级放大,直到「GC 后堆」越过「持续阶梯上涨」。随后再分别验证「堆设得过满导致并发周期来不及」和「用平均停顿掩盖 Full GC 长尾」,三类故障分开执行,避免多个变量同时变化而无法归因。

4. 执行止损和根因修复

第一轮只应用「先降低分配热点」,确认它能控制影响范围;第二轮应用「为并发 GC 留 CPU 余量」,验证核心链路恢复;最后落实「按 SLO 选择收集器」,消除同类问题再次出现的条件。每一步都保留变更前后数据,不用“感觉变快了”替代测量。

5. 通过退出条件

实验只有同时满足三项才算通过:「GC 后堆」回到「稳态水平线」、「暂停 P99」回到「低于 SLO 10%」、「分配速率」回到「容量内稳定」,并且业务结果差异为零。若性能恢复但结果不一致,仍应视为失败;若指标恢复后很快再次越线,则说明只完成了临时止损,没有消除根因。

量化基线

指标 样例基线/口径 风险线 结论
GC 后堆 稳态水平线 持续阶梯上涨 泄漏/缓存增长
暂停 P99 低于 SLO 10% 突破预算 收集器/存活集
分配速率 容量内稳定 翻倍伴随发布 代码分配回归

这些数值是实验口径或示例告警线,不是可复制到所有系统的固定答案;上线阈值应由本系统稳态、峰值和故障演练共同确定。

事故复盘:低延迟服务 P99 周期性抖动

监控只显示平均响应正常,GC 日志却表明每两分钟出现 Mixed GC,原因是缓存批量刷新制造大量中等寿命对象并集中晋升。拆散刷新批次、减少临时对象后,比先改收集器参数更直接。

失败模式 首要证据 第一处置动作
只调停顿目标不降低分配速率 分配速率 先降低分配热点
堆设得过满导致并发周期来不及 停顿 P95/P99 为并发 GC 留 CPU 余量
用平均停顿掩盖 Full GC 长尾 晋升与存活率 按 SLO 选择收集器

发布与回滚检查点

  • 发布前:确认「-Xlog:gc*」对应实现和上述配置在目标版本仍然有效,并保存「GC 后堆」基线。
  • 灰度中:同时观察 分配速率、停顿 P95/P99、晋升与存活率;任一指标越过表中风险线,就停止继续扩量。
  • 回滚时:先执行「先降低分配热点」控制影响,再回退代码或参数;涉及持久状态时必须额外核对结果差异。
  • 发布后:至少覆盖一个完整峰值周期,确认「只调停顿目标不降低分配速率」没有再次出现,才关闭变更观察窗口。

方案对比与选型

方案 更适合的场景 主要收益 代价与边界
G1 中大堆与通用暂停目标 Region 化、成熟通用 极低停顿目标下并发余量有限
ZGC 大堆与亚毫秒级停顿诉求 并发移动、停顿短 需要较新 JDK 与足够 CPU/内存余量
Parallel GC 吞吐优先的离线计算 实现简单、吞吐高 停顿随堆和存活对象增长

选型至少带上 堆与非堆容量、对象分配速率、存活率和停顿目标,并用上面的量化基线验证;未知数据应明确为待测假设。

设计边界与工程取舍

收集器选择服从 SLO、堆规模和 CPU 预算;先确认对象分配与存活模式,再调整参数,不能把 GC 调优等同于换算法。

工程落地遵循:以可观测证据驱动参数调整,避免脱离业务负载套用参数模板。回答时直接引用「-Xlog:gc*」、配置实验和事故数据,比复述固定模板更有说服力。