先说结论
JVM 垃圾回收主要解决两个问题:哪些对象已经没用了,以及怎样回收更划算。理解可达性分析、分代假设和不同收集器的停顿取舍,基本就抓住了 GC 的主线。
对象何时可以回收
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 的默认选择通常是合理起点。
排查顺序建议:
- 开启统一 GC 日志并记录时间戳。
- 观察分配速率、晋升速率、堆使用量和停顿分位数。
- 判断是内存泄漏、堆配置不足、对象分配过快还是收集器不匹配。
- 必要时结合堆转储、对象直方图和线程 dump 定位根因。
容易踩坑的地方
System.gc()只是请求,具体行为受 JVM 参数和实现影响。- 对象进入老年代不代表永远不会回收。
- 堆越大不一定越好,更大的堆可能增加内存成本和某些阶段的处理量。
- Minor GC、Major GC、Full GC 的术语在不同收集器和工具中可能含义不同,应以日志事件为准。
常见问题
追问:什么对象可以作为 GC Root?
线程栈中的引用、静态字段引用、JNI 引用以及 JVM 内部持有的关键对象等。具体集合随实现与阶段而变化。
追问:堆越大停顿一定越长吗?
不一定,停顿还受收集器、存活对象量和回收阶段影响;但更大堆会提高内存成本,并可能增加部分阶段工作量。