P60 面试重灾区:GC 耗时 10 毫秒为什么系统却卡顿 10 秒
面试题:GC 明明只有 10ms,为什么用户感觉系统卡了 10 秒?
1. 核心:GC 的 10ms 只是"一次"的耗时,问题出在"次数"
如果 1 秒内发生 1000 次 10ms 的 GC,累积起来就是 10 秒的停顿。所以先要看:
text
GC 频率 × 单次 GC 耗时 = 实际停顿时间2. 为什么会频繁 GC
① 对象创建速度太快(分配压力大)
- 高并发下大量短生命周期对象(如日志、JSON 序列化、循环里 new 对象);
- Eden 区很快被填满 → 频繁 Minor GC;
- 如果对象能快速回收还好,但每次 GC 都有 STW。
② 大对象直接进老年代,触发频繁 Full GC
- 大对象(大数组/大 List/大字符串)直接分配到老年代;
- 老年代很快满 → Full GC 频率上升;
- 老年代 GC 通常秒级甚至更久。
③ 晋升过快 / 老年代回收效率低
- Minor GC 后存活对象多 → 大量晋升老年代;
- 老年代碎片化 → CMS/G1 回收慢。
④ 内存泄漏 / 缓存膨胀
- 对象被误持有无法回收 → 堆被占满 → 持续 Full GC 仍腾不出空间。
3. 另一个关键点:GC 之外还有别的停顿
10 秒卡顿不全是 GC:
- 线程调度/锁等待:大量线程竞争锁、上下文切换;
- IO 阻塞:磁盘慢、数据库慢、网络超时;
- 垃圾回收以外的 STW:偏向锁撤销、代码缓存清理、JIT 编译、安全点(SafePoint)到达;
- 内存换页:物理内存不足,发生 swap,比 GC 更可怕。
所以"GC 10ms 却卡 10 秒"要先区分:卡顿是不是 GC 造成的。
4. 排查手段
bash
jstat -gcutil <pid> 1000 # 看 GC 频率和耗时
jmap -heap <pid> # 看各区使用- GC 日志(
-Xlog:gc*)统计单次 GC 耗时分布和频率; top -H+ jstack 看是否有别的线程在忙/阻塞;- 如果 GC 频率正常,卡顿就是 GC 之外的原因(锁、IO、swap)。
5. 优化方向
- 减少对象创建(复用对象、StringBuilder、避免循环内 new);
- 调优堆和新生代大小(
-Xmn、-XX:MaxTenuringThreshold); - 用 G1/ZGC 降低停顿(ZGC 目标亚毫秒级 STW);
- 修内存泄漏、控制缓存上限。
一句话总结
"GC 10ms 卡 10 秒"通常意味着GC 频率极高(10ms×1000 次=10s),或卡顿来自锁竞争/IO/换页等非 GC 因素;用 jstat+GC 日志先算"总停顿时间",再区分是不是 GC 的锅,最后针对性优化分配压力和堆配置。