在很多 Java 开发者的认知里,JVM 垃圾收集(GC)往往被简化为一系列晦涩的参数调优和“Stop-The-World”的噩梦。我们习惯于背诵“新生代用复制,老年代用标记整理”,却很少停下来思考:为什么 JVM 要设计得如此复杂?
如果站在更高的维度审视,JVM GC 的本质并非单纯的“内存清理”,而是一场在有限资源约束下,对吞吐量、延迟和内存占用三者进行的动态权衡与自动化管理。
今天,我们将跳出参数配置的微观视角,从目标维度重新解构 JVM GC 的本质,并梳理主流收集器是如何在这场博弈中做出选择的。
一、 GC 的四个本质维度
1. 资源维度:时空置换的艺术
GC 的核心矛盾在于分配效率与空间利用率。
- 为了极致的分配速度,JVM 使用碰撞指针(Bump Pointer),这要求内存连续。
- 但简单的回收会产生碎片,迫使系统退化为效率较低的空闲链表。
因此,GC 算法本质上是在做交易:
- 复制算法:牺牲一半空间,换取无碎片和极速回收(适合新生代)。
- 标记-整理:牺牲 CPU 时间去移动对象,换取连续内存和空间利用率(适合老年代)。
结论:GC 是用 CPU 时间换内存空间,或用内存空间换 CPU 时间的动态平衡。
2. 业务维度:STW 与用户体验的博弈
对于用户而言,GC 的唯一感知就是停顿(Pause)。
- 吞吐量优先:如 Parallel GC,允许较长的 STW,追求单位时间内处理更多请求。
- 延迟优先:如 ZGC、G1,致力于将 STW 拆解为毫秒级片段,保障实时响应。
结论:GC 的目标是将不可控的大规模清理压力,转化为可控的、微小的停顿,以保障 SLA。
3. 数据维度:分代假设的胜利
JVM 并非对所有对象一视同仁,而是基于统计学规律——弱分代假说(绝大多数对象朝生夕死)。
- 通过划分新生代和老年代,JVM 将大规模的全局扫描问题,转化为小规模的局部清理问题。
- 引入**卡表(Card Table)**解决跨代引用,以少量的空间开销避免全堆扫描。
结论:利用数据的生命周期特征进行分区治理,是实现线性扩展能力的关键。
4. 架构维度:从串行到并发协同
随着多核 CPU 和大内存成为常态,GC 从单线程串行演变为高度并发的协同系统。
- 并行(Parallel):多核同时工作,缩短总耗时,但仍需 STW。
- 并发(Concurrent):GC 线程与应用线程交替执行(如 G1、ZGC),将开销平滑分散到时间轴上。
结论:现代 GC 是利用算力换时间,通过并发消除性能毛刺。
二、 主流收集器的抉择:吞吐量 vs 延迟
理解了本质,我们再来看具体的垃圾收集器。它们其实是不同权衡策略的具体实现。
1. 吞吐量王者:Parallel Scavenge + Parallel Old
- 策略:全力冲刺,不管停顿。
- 算法:新生代复制,老年代标记-整理。全程多线程并行,但 GC 期间完全 STW。
- 适用场景:后台批处理、大数据计算、科学运算。
- 为什么选它:它放弃了低延迟的追求,利用多核 CPU 最大化垃圾回收效率,从而让出更多 CPU 时间给业务逻辑。
2. 低延迟先驱:CMS (Concurrent Mark Sweep)
- 策略:尽量不打扰用户(已废弃,但值得回顾)。
- 算法:老年代采用标记-清除,并发执行标记和清除阶段。
- 适用场景:旧版 Web 服务,对响应时间敏感的应用。
- 为什么选它:通过并发减少 STW。但代价是产生内存碎片,且对 CPU 资源敏感,容易因“浮动垃圾”或碎片导致不可控的 Full GC。
3. 均衡大师:G1 (Garbage-First)
- 策略:在可预测的停顿内,做到最好。
- 算法:基于 Region 的分代收集。整体标记-整理,局部复制。引入停顿预测模型。
- 适用场景:JDK 9+ 默认选择,适合大内存(6GB+)、多核服务器。
- 为什么选它:G1 不再物理隔离新老生代,而是根据“回收价值”动态选择 Region。它能在满足用户设定的停顿目标(如 200ms)前提下,最大化吞吐,同时避免了 CMS 的碎片问题。
4. 极致低延迟:ZGC & Shenandoah
- 策略:停顿时间与堆大小无关。
- 算法:染色指针(ZGC)或读屏障(Shenandoah)。几乎全程并发,包括对象重定位。
- 适用场景:高频交易、实时交互系统、TB 级超大堆应用。
- 为什么选它:传统 GC 的 STW 随堆增大而增长,而 ZGC 通过元数据技术将引用维护开销分摊到应用线程中。无论堆是 10GB 还是 10TB,停顿都控制在毫秒级。
三、 总结与建议
| 收集器 | 核心特征 | 权衡倾向 | 典型场景 |
|---|---|---|---|
| Parallel | 并行 + STW | 高吞吐,高延迟 | 后台计算、批处理 |
| CMS | 并发标记清除 | 低延迟,有碎片风险 | 旧系统维护(已淘汰) |
| G1 | 分区 + 预测模型 | 平衡,可预测延迟 | 主流服务端应用 |
| ZGC | 染色指针 + 全程并发 | 极低延迟,高复杂度 | 实时系统、超大堆 |
给开发者的建议:
- 没有银弹:不要盲目追求最新的 ZGC。如果你的应用是离线数据分析,Parallel GC 可能是最高效的选择。
- 理解业务 SLA:先问自己,业务更在乎“每秒处理多少请求”(吞吐),还是“每个请求多久返回”(延迟)?
- 默认即优选:在 JDK 11/17/21 等长期支持版本中,G1 通常是大多数场景下的最佳起点。只有当你对延迟有极端要求(<10ms)或堆内存极大时,才考虑切换到 ZGC。
JVM GC 的演进史,就是一部计算机系统在资源约束下不断追求极致效率的历史。理解其背后的权衡逻辑,比记住任何调优参数都更重要。
评论区