聊聊你熟悉的垃圾回收器
# 你一般使用什么 JDK 版本?默认的垃圾回收器是什么
- JDK 11:现在大多数公司已经稳定运行在 11 上,生态成熟,Spring Boot 2.x 对其支持很好。
- JDK 17:随着 Spring Boot 3.x 和 Spring Cloud 的普及,17 已经是一个非常好的选择。它带来了更强的 GC 能力、更好的容器感知,并且比 8 的性能有明显提升。
- JDK 8:虽然很多老系统还在用,但目前新项目已不太适合作为起点。如果现在还在选 8,可能会在未来 1-2 年遇到 Spring 新版本不支持的问题。
| JDK 版本 | 默认垃圾回收器 (Default GC) | 备注 |
|---|---|---|
| JDK 7 (及更早) | Parallel GC | 在JDK 7u4版本后,老年代也使用了Parallel Old GC 。 |
| JDK 8 | Parallel GC | 延续了JDK 7后期的默认选择,是许多线上系统的常见版本。 |
| JDK 9 及以上 | G1 GC (Garbage-First) | 从JDK 9开始成为默认,并持续作为后续LTS版本(如JDK 11, 17, 21)的默认GC 。 |
| JDK11 | ZGC | 引入了分代ZGC (Generational ZGC),进一步优化了性能和内存开销。 |
# 使用什么垃圾回收器?
这个要看具体场景,不能一概而论:
| 场景 | 推荐 GC | 理由 |
|---|---|---|
| 低延迟服务(在线业务) | G1GC | 可预测停顿时间,默认自适应,适合大多数互联网应用 |
| 高吞吐量(离线计算、批处理) | Parallel GC | 吞吐量优先,GC 停顿时间可以稍长 |
| 超低延迟(金融交易) | ZGC / Shenandoah | 停顿时间 < 1ms,JDK 17 已经非常成熟 |
| 大堆内存(> 32GB) | ZGC | G1 在大堆下 Full GC 风险增大,ZGC 更稳定 |
目前线上我一般用 G1GC(-XX:+UseG1GC),然后根据监控调整 MaxGCPauseMillis,默认 200ms,通常调到 100ms 左右。到了 JDK 17,如果堆内存超过 32GB,我会切换到 ZGC。
# 常用常见的JVM参数有哪些?
-Xms4g -Xmx4g
-XX:+UseG1GC
-XX:MaxGCPauseMillis=100
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/path/to/dump
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:/path/to/gc.log
| 参数 | 作用 |
|---|---|
-Xms / -Xmx | 堆内存初始 / 最大值 |
-XX:MaxGCPauseMillis | G1 的目标停顿时间 |
-XX:+HeapDumpOnOutOfMemoryError | OOM 时自动 dump 内存 |
-XX:+DisableExplicitGC | 禁止显式 System.gc()(防止框架误触 Full GC) |
-Xloggc | GC 日志路径(JDK 9+ 用 -Xlog:gc*) |
# 为什么堆内存的初始大小(-Xms)和最大值(-Xmx)要设置一样?
- 避免扩容开销:JVM 在堆内存不足时会触发扩容(从初始到最大值),这个过程涉及重新分配内存、调整 GC 阈值,属于 Stop-The-World 操作。直接设成一样,一开始就把内存占好,省去扩容这一步。
- 避免弹性回收:如果初始小、最大大,JVM 在低负载时会缩容,高负载时再扩容。这个过程中的内存调整本身会造成性能抖动。固定堆大小可以让性能曲线更平稳,对在线业务更友好。
- 容器环境更必要:在 K8s 里,容器内存限制是固定的,
-Xms和-Xmx保持一致可以避免 JVM 反复申请、释放内存,减少被 cgroup 驱逐的风险。
如果有特殊情况需要 Xms < Xmx(比如多应用混部、内存敏感的场景),也可以,但通常不推荐。对于大多数在线服务,最稳定的配置就是 -Xms = -Xmx。
# 分区、分代是如何工作的
分代回收的理论基础是“弱分代假说”,即绝大多数对象存活时间很短。基于此,JVM将堆内存划分为新生代(Young Generation) 和老年代(Old Generation),并对它们采用不同的回收算法和频率。
# 新生代回收 (Minor GC / Young GC)
- 触发时机:Eden区空间不足时。
- 回收算法:复制算法。
- 回收过程:
- Eden区存活的对象 + 当前Survivor区(S0)存活的对象,被复制到另一个空闲的Survivor区(S1)。
- 复制过程中,存活对象的年龄会+1。
- 清空Eden和S0区。
- 年龄达到晋升阈值(默认15岁)的对象,会被晋升(Promotion) 到老年代。
- 特点:频率高,速度快,但会暂停所有应用线程(STW,Stop-The-World)。
# 老年代回收 (Major GC / Full GC)
- 触发时机:老年代空间不足、晋升失败、或System.gc()被显式调用时。
- 回收算法:标记-清除(Mark-Sweep) 或 标记-整理(Mark-Compact)。
- 回收过程:
- 标记:标记所有存活的对象。
- 清除:回收未被标记的对象(标记-清除);或将所有存活对象向一端移动,然后清理边界外的内存(标记-整理)。
- 特点:频率低,但耗时较长,通常也是STW。
总的来说:
- 分代是策略,目的是根据对象生命周期来优化回收效率。所有主流回收器都遵循这一策略。
- 分区是实现,是对堆内存的具体划分方式。传统回收器如Parallel GC采用物理分区,而G1/ZGC等现代回收器采用逻辑分区(Region)。
- 回收过程:无论哪种回收器,核心都是 “标记存活对象 → 回收死亡对象 → 整理/复制存活对象” 的过程。区别在于:
- Parallel GC 追求高吞吐量,但可能带来较长的停顿。
- G1 GC 追求可控的低停顿,通过增量回收和优先级回收来实现。
上次更新: 2026-06-29 17:10:35