推荐服务突然OOM挂掉。运维重启之后,过了四十分钟又挂了。这道题表面考 JVM,实际考的是生产事故应急能力和内存现场分析能力。
兄弟们,说个真事。
我一个哥们上周面字节跳动,三面都过了,结果挂在二面的一道题上。对,你没看错——二面,不是三面。
面试官喝了口水,不紧不慢地抛出问题:
"凌晨两点,你负责的推荐服务突然OOM挂掉。运维重启之后,过了四十分钟又挂了。你是Owner,现在给你五分钟,告诉我怎么做。"
我那哥们一听,心想这题不简单吗?
"内存不够那就加内存啊,把-Xmx从 4G 翻到 8G。实在不行,先重启稳住,白天再慢慢查。"
面试官放下水杯,眼神变了——
"加内存?如果是 ThreadLocal 泄漏,你加到 128G 它也能给你吃干净。重启?案发现场你一个键都不留就重启?你知不知道jmap在生产环境执行一次,整个服务可能 STW 十几秒?"
一句话总结结局:凉了,当场凉了。
这道题表面考 JVM,实际考的是生产事故应急能力和内存现场分析能力。加内存是小白本能反应,但面试官要的是能独立扛事故的人,不是只会氪金的 RMB 玩家。
今天就把这道 P7 级大题拆开揉碎,别让兄弟们在同一个坑里摔两次。

一、第一层|OOM 不是一种病——先把敌人认清楚
面试时很多人张嘴就"OOM 了",但面试官心里已经在摇头了:OOM 有三种典型死法,你说的是哪一种?
1. 老年代塞满——Heap Space OOM(出场率最高)
日志里会看到:
java.lang.OutOfMemoryError: Java heap space
翻译成人话:你家的仓库满了,垃圾回收车跑了N趟还是腾不出地方。
典型作案现场:
- 某接口把几十万条数据塞进一个ArrayList做内存分页,压根没考虑流式处理
- ThreadLocal里存了用户 Session,用完从没remove()
- 本地缓存无上限地往里塞,以为对象会自动消失
2. 类装不下了——Metaspace OOM
java.lang.OutOfMemoryError: Metaspace
这不是仓库爆了,是你家的档案室(元空间)塞不下了。跑着跑着,JDK 动态代理、CGLib、Spring AOP 在运行期间悄悄生成了巨量的代理类,把装 Class 的元空间撑炸了。
3. 隐形炸弹——堆外内存溢出
日志里没有明显的OutOfMemoryError,但进程就是被系统kill -9了。这才是最坑的——Direct Buffer Memory 不归 JVM 堆管,你用 Netty 写了个ByteBuffer.allocateDirect()没释放,堆内看着一切正常,但 OS 视角下进程内存已经逼近上限了。
💡 金句:不同的死法有不同的刀口,认错伤口就开错药。
二、第二层|别急着拔电源——先把监控录像拷出来
面试官问的"快速止血",核心不是"重启",而是保留案发现场。
1. 止血的正确姿势
(1) 第一步——切断流量(不是重启!)
先去你的注册中心(Nacos/Dubbo/ZK),把这台病号机器从服务列表里摘掉。流量切走了,事故不会扩散,你也赢得了排查时间。
(2) 第二步——确认"遗书"有没有写好
你的 JVM 启动参数里有没有这一行?
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/data/dump/oom.hprof
这两个参数就是 JVM 的遗嘱——挂了之前,它会自动把内存快照拍下来存好。上线前不加这两行,等于出门不带身份证,出了事哭都来不及。
(3) 第三步——手动 Dump 的大坑
如果服务没死透,你需要手动抓快照。但这里有一个 P7 级别的大坑:
jmap -dump:format=b,file=heap.hprof <PID>
这条命令在 8G+ 的大堆上执行,JVM 会进入STW(Stop The World),整个服务直接僵住十几秒甚至更久。如果你忘了先摘流量就执行——恭喜你,亲手制造了二次事故。
还有一个点千万别搞错:jmap命令里不要加:live参数。加了之后它会强行触发一次 Full GC,在内存已经摇摇欲坠的时候,这就是最后一脚。
💡 金句:重启是毁灭证据,Dump 是保护现场——先关水龙头,再拍照片。
三、第三层|MAT 抓鬼——让内存里的凶手无处遁形
快照文件拿到了,几个 G 的.hprof。用记事本打开?全是乱码。这时候要上 Eclipse MAT(Memory Analyzer Tool)。
1. 第一步:看嫌疑人名单(Leak Suspects)
打开 MAT,它会自动给你一份报告,第一页叫Leak Suspects。
它会直截了当地告诉你:
"兄弟,你这个HashMap里有个内部类Node,占了整个堆的 73%"
好,嫌疑对象锁定了——进度 50%。
2. 第二步:查组织架构图(Dominator Tree)
点开 Dominator Tree,这玩意儿就像公司的组织架构图。两个概念必考:
- Shallow Heap:对象本身占多少。比如一个String对象,撑死几十个字节。
- Retained Heap:对象 + 它下面管的所有小弟,总共占多少。这才是我们要找的——回收它能释放的真实内存量。
按 Retained Heap 降序排列,排第一的那个就是幕后大 Boss。
进度——80%。
3. 第三步:顺藤摸瓜(Path to GC Roots)
找到大 Boss 还不够,你得知道是谁把它拴住了不让回收。
操作:右键那个嫌疑对象 →Path to GC Roots→ 勾选exclude phantom/weak/soft references。
MAT 会给你画出一条链:
大 HashMap
↑
└── ThreadLocalMap$Entry
↑
└── Thread[http-nio-8080-exec-3]
真相大白——线程池里的一条工作线程,它的ThreadLocalMap里拴着一个用完没清理的HashMap。这个线程被线程池复用了,Map 就永远赖在里面,一次次请求叠上去,最终炸了。
特别提醒:如果 .hprof 文件里堆内内存看着挺空,但进程就是 OOM,那基本是堆外内存炸了。用 MAT 搜一下DirectByteBuffer对象数量,同时打开 NMT(Native Memory Tracking)配合排查。
💡 金句:找到胖子不算本事,找到是谁在喂他——才算破案。
四、第四层|ThreadLocal 的障眼法——你以为它会自首,其实它在赖账
面试官追问到这里,才是真正的 P7 分水岭:
"ThreadLocal 的 Key 是 WeakReference,GC 的时候不就被回收了吗?为什么还会泄漏?"
很多人在这个问题上翻车,包括我那个哥们。来看这条引用链:
当前线程(强引用)
→ ThreadLocalMap(强引用)
→ Entry(Key=null, Value=你的数据)
解释一下这个犯罪过程:
- 第一步:ThreadLocalMap.Entry的 Key(也就是ThreadLocal对象本身)确实是弱引用。下一次 GC 一扫,Key 没了,变成null。
- 第二步:但是!Value 是我们存进去的业务数据,它是强引用!GC 一看——虽然 Key 已经 null 了,但 Value 还被Entry引着,Entry被ThreadLocalMap引着,Map被线程引着。
- 第三步:线程池的线程是不会死的,它被反复复用。这条强引用链就永远断不掉。Key 虽然没了你也访问不到这笔数据了,但 GC 不敢收它——因为它还被活着的线程间接拴着。
结论一句话:必须在finally块里显式调用ThreadLocal.remove(),没有第二条路。这是切断引用链的唯一方式。
ThreadLocal<UserContext> tl = new ThreadLocal<>();
try {
tl.set(context);
// ... 业务逻辑
} finally {
tl.remove(); // 这一行不写,就是在给 OOM 埋雷
}
💡 金句:WeakReference 管杀不管埋——Key 可以自动消失,但 Value 必须你亲手清理。

五、第五层|面试标准答案——"止血-保留-分析"三步法
以后再被问到线上 OOM,别再提"加内存"了,把下面这套 SOP 背下来:
1. 第一档:防御性配置(上线前必做)
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/data/logs/oom.hprof
这是给你的服务买的保险,出事了自动拍照,不花钱。
2. 第二档:现场保护(事故发生后)
|
步骤 |
操作 |
禁忌 |
|
① 摘流 |
从注册中心摘除故障节点 |
绝对不要直接重启 |
|
② 确认 Dump |
检查自动Dump是否生成 |
不要在流量节点上执行jmap |
|
③ 手动 Dump |
摘流后再jmap |
不加:live参数 |
3. 第三档:根因分析(拿到快照后)
- MAT 打开 →Dominator Tree,按 Retained Heap 倒序
- 锁定最大对象 →Path to GC Roots(排除虚/弱/软引用)
- 顺链找元凶 → 通常是ThreadLocal.remove()没写、静态集合无界增长、Excel 流式导出写成一次性加载
💡 金句:面试官要的不是"加内存"——他要的是你能在凌晨两点独自扛住事故的眼神。

写在最后
OOM 这玩意儿,早晚会找上你。
区别在于——有的人只会重启然后祈祷,有的人知道怎么拿到快照然后五分钟锁定根因。面试官要的是第二种人,公司愿意给 50K 月薪的也是第二种人。
©本文为清一色官方代发,观点仅代表作者本人,与清一色无关。清一色对文中陈述、观点判断保持中立,不对所包含内容的准确性、可靠性或完整性提供任何明示或暗示的保证。本文不作为投资理财建议,请读者仅作参考,并请自行承担全部责任。文中部分文字/图片/视频/音频等来源于网络,如侵犯到著作权人的权利,请与我们联系(微信/QQ:1074760229)。转载请注明出处:清一色财经

微信扫码打赏
支付宝扫码打赏
