字节二面:服务半夜炸了,我说加内存,面试官当场黑脸

推荐服务突然OOM挂掉。运维重启之后,过了四十分钟又挂了。这道题表面考 JVM,实际考的是生产事故应急能力和内存现场分析能力。

推荐服务突然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)。转载请注明出处:清一色财经

(0)
打赏 微信扫码打赏 微信扫码打赏 支付宝扫码打赏 支付宝扫码打赏
清一色的头像清一色管理团队
一天上市7款车:大家嘴上说不愿卷,脚却不愿停
上一篇 2026年7月19日 01:17
APEC进入“成都时间”,看懂“市值密码”背后的产业跃迁
下一篇 2026年7月19日 01:17

相关推荐

发表回复

登录后才能评论

联系我们

在线咨询:1643011589-QQbutton

手机:13798586780

QQ/微信:1074760229

QQ群:551893940

工作时间:工作日9:00-18:00,节假日休息

关注微信