目标:你能把 GC 调优说清楚为"以指标为导向的系统工程",并掌握线上常见故障(频繁 Full GC、OOM、吞吐下降)的排查套路。
1. GC 调优的第一原则:先确认目标
常见目标:
- 低延迟:P99/尾延迟更重要(典型:网关、交易)
- 高吞吐:单位时间处理更多任务(典型:离线、批处理)
- 稳定性:避免 OOM、避免长时间停顿
不同目标会影响你选择 GC(G1/ZGC)和参数策略。
2. 你必须会的内存结构(够用版)
- 堆:新生代 + 老年代(以 G1 视角则是 Region)
- 元空间:类元数据(动态生成类会涨)
- 栈:线程栈(线程太多会撑爆内存)
面试高频:
- 为什么线程太多会 OOM:因为每个线程都有栈内存(
-Xss)。
3. 看懂 GC 日志:先抓"现象",再猜"根因"
你至少要能回答:
- GC 是否频繁?
- Full GC 是否频繁?
- 单次停顿多长?
- 回收后内存是否能降下去?
3.1 三个高价值指标
- 分配速率:新对象产生有多快
- 晋升速率:对象从新生代进入老年代有多快
- 回收效率:一次 GC 回收多少、停顿多久
直觉:
- 分配速率高 -> Minor GC 频繁
- 晋升速率高 -> 老年代涨得快 -> Full GC/混合回收频繁
- 回收后降不下来 -> 可能泄漏或缓存无界
4. 频繁 Full GC:最常见的线上事故
4.1 常见根因
- 老年代增长过快:
- 大对象直接进入老年代(大数组、批量组装)
- 短命对象存活过久(大事务、超长链路持有引用)
- 缓存/集合无界增长
- GC 无法回收:
- 内存泄漏
4.2 处理思路(先止血,再根治)
- 止血 :
- 临时扩容(堆/实例数)
- 降低流量(限流/熔断)
- 关闭/缩小某些大缓存
- 根治 :
- 找到"谁在持有对象",解决无界引用
5. OOM 分类:不同 OOM 不同打法
5.1 java.lang.OutOfMemoryError: Java heap space
- 堆爆了:
- 泄漏或无界缓存
- 单次请求构造超大对象
5.2 OutOfMemoryError: Metaspace
- 动态类太多:
- CGLIB/JDK Proxy 动态生成过多(比如不停创建新的类加载器)
- 热部署/脚本引擎不释放
5.3 OutOfMemoryError: unable to create new native thread
- 线程太多:
- 线程池失控、阻塞堆积
-Xss太大
面试加分点:这类 OOM 不是堆不够,是系统线程资源耗尽。
6. 内存泄漏排查:从"怀疑"到"证据"
6.1 你要找的不是"谁创建了对象",而是"谁持有它不放"
泄漏本质是:对象已经"业务上无用",但仍被引用链持有。
常见泄漏源:
- 静态集合(static map)
- ThreadLocal 未清理
- 缓存无 TTL/无上限
- 监听器/回调注册未取消
6.2 证据链(通用方法)
- Dump 堆(heap dump)
- 找到占用最大的对象类型(Top consumers)
- 查看 GC root 引用链(是谁把它挂住了)
如果你能说出"从 dump 到引用链"的路径,面试官会认为你做过真实排障。
7. 参数调优:不追求"神配置",追求可解释
7.1 堆大小
- 堆太小:频繁 GC
- 堆太大:单次停顿可能更长,且容器场景可能触发 OOMKill
建议:
- 结合容器内存限制与应用峰值对象量
- 留出非堆空间(元空间、线程栈、直接内存)
7.2 选择 GC
- G1:通用默认、较平衡,适合大多数服务端应用
- ZGC:更偏低延迟(新版本 JVM 支持较好),但要考虑 CPU 开销与版本
面试表达建议:说"我会根据延迟目标和 JVM 版本选择",而不是一句"就用 ZGC"。
8. 线上自救清单(按优先级)
- 先确认是 GC 导致的 RT:看 STW 时间、Full GC 次数
- 确认内存回收后是否下降 :
- 能下降:可能是流量/分配速率问题
- 降不下:高度怀疑泄漏/无界缓存
- 确认线程数:线程爆炸会带来 native thread OOM
- 短期措施:限流、扩容、降级、重启(谨慎,重启只能缓解不能根治)
9. 面试背诵稿(45 秒)
GC 调优我会先明确目标是低延迟还是高吞吐,然后用 GC 日志和监控指标来驱动。
重点看分配速率、晋升速率、Full GC 频率以及回收后内存是否能降下来。
频繁 Full GC 通常是老年代增长过快或存在无界缓存/泄漏;如果回收后降不下来就要做 heap dump,通过对象占用排行和 GC root 引用链定位是谁持有对象。
OOM 也要分类看:heap space、metaspace 和 native thread 的处理思路完全不同,线上先止血限流扩容,再做根因修复。