一、GC基础概念与原理
1.1 垃圾回收到底在干嘛?
简单来说,垃圾回收(Garbage Collection,GC)就是自动帮我们管理内存的机制。你想想看,以前写 C/C++ 的时候,malloc 出来的内存如果忘了 free,就会泄漏。Java 不一样,JVM 会自己盯着堆内存,发现哪些对象没人用了,就悄悄把它们回收掉。
我个人习惯把 GC 理解为「JVM 内部的保洁阿姨」——她定期巡视,把废弃的东西清走,腾出空间给新对象。但保洁阿姨清扫的时候,其他人得停下手里的活(这就是 Stop-The-World),所以 GC 不是免费的。
我曾经遇到过一线上系统,每 10 秒就出现一次长暂停。排查发现是一个 HashMap 缓存只增不减,导致 Full GC 频繁触发。后来改成弱引用 + 过期策略,问题就解决了。
1.2 GC 到底解决了什么问题?
GC 的核心作用其实就三个:
- 自动内存管理——你只管 new 对象,JVM 帮你管生死。
- 防止内存泄漏——至少能兜住大部分显性泄漏,但隐性的(比如长生命周期对象引用短生命周期对象)还是需要开发者注意。
- 提高开发效率——不用手动 free,代码出 bug 的概率低很多。
但这不代表你可以完全不管内存。我见过一个项目,因为循环创建大量临时对象,导致 Young GC 每秒触发几十次,CPU 都耗在 GC 上了。所以了解 GC 原理,是为了更好地驾驭它。
1.3 JVM 里的 GC 是怎么实现的?
JVM 的 GC 实现,说白了就是一套「分代收集」策略。为什么分代?因为大部分对象活不长,朝生夕死。把堆分成几个区域,用不同的策略处理,效率更高。
| 区域 | 作用 | GC 频率 | 常用算法 |
|---|---|---|---|
| 年轻代(Young) | 存放新对象 | 高(Minor GC 非常频繁) | 复制算法 |
| 老年代(Old) | 存放长期存活对象 | 低(Major/Full GC 较少) | 标记-整理 / 标记-清除 |
| 元空间(Metaspace) | 存放类元数据 | 极少(类卸载时触发) | 具体实现依赖 GC 收集器 |
常用的垃圾收集器也有不少,我列举几个你大概率会碰到的:
| 收集器 | 特点 | 适用场景 |
|---|---|---|
| Serial | 单线程,暂停时间短但集中 | 单核、客户端模式 |
| Parallel | 多线程并行,追求吞吐量 | 后台批处理、科学计算 |
| CMS | 低延迟,但浮动垃圾多 | 互联网应用(已被 G1 取代) |
| G1 | 分区化,可预测暂停时间 | 多核大内存服务器(默认 JDK 9+) |
| ZGC | 超低延迟(亚毫秒级) | 超大堆、低延迟要求高 |
想观察 GC 行为,最简单的方式就是在启动参数里加上:
-Xms2G -Xmx2G -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log
然后看 gc.log 文件,Minor GC、Full GC 的耗时和频率一目了然。
我曾经用这个日志定位过一个诡异问题——每次 Full GC 都要 5 秒,后来发现是因为堆分配了 16G,但 JVM 没有启用 G1 的 -XX:G1HeapRegionSize 调优,导致 region 数量过多。调小 region 后,暂停时间降到了 1 秒以内。
GC 日志本身也会带来性能开销,线上环境建议使用
-XX:+UnlockDiagnosticVMOptions -XX:+PrintGCApplicationStoppedTime 来精确记录暂停时间,不要开太多 Detail 日志。
1.4 启动阶段的 GC:为什么会影响速度?
应用启动的时候,大量对象被创建——类加载、框架初始化、连接池预热……年轻代很快就满了。此时 Minor GC 会很频繁。如果堆配置不当,甚至可能在启动过程中触发 Full GC,把启动时间拖长好几倍。
这就是我们做「GC 抑制优化」的切入点——不是要禁止 GC,而是在启动阶段尽量减少 GC 的触发次数和暂停时长,让应用尽快完成初始化。
举个真实案例:某个微服务启动需要 30 秒,其中 GC 暂停就占了 8 秒。我们把初始堆大小调到和老年代预估大小相等,同时调整晋升阈值,启动时间缩短到了 18 秒。说白了,就是让 GC 在启动阶段「少干活」。
- GC 是自动内存管理工具,但需要付出暂停代价。
- JVM 采用分代收集 + 多收集器选择。
- 启动阶段 GC 频繁会拖慢初始化,这就是我们需要优化的地方。