Skip to content

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 的锅,最后针对性优化分配压力和堆配置。

基于 VitePress 重建