
凌晨两点手机突然疯狂震动。杨工线上服务挂了OutOfMemoryError: Java heap space 实习生小刘的声音带着哭腔。我叹了口气披上外套打开电脑。这已经是我这个月第三次半夜被叫醒了。第一幕案发现场——先分清死因我别慌先把错误日志截图发我。小刘发了就是Java heap space。我好先记住一件事OOM 不是只有一种。就像医生看病得先分清楚是哪种病。我给他列了一张急诊分诊表错误信息案发区域常见凶手Java heap space堆内存内存泄漏、一次性加载大对象查表没加 limit、集合只增不减GC overhead limit exceeded堆内存JVM 花了 98% 的时间做 GC却只回收了不到 2% 的内存——这是堆快满了的前兆Metaspace元空间动态代理/CGLIB/反射生成类过多、类加载器泄漏、热部署频繁Direct buffer memory堆外内存NIO / Netty 申请直接内存堆外空间耗尽unable to create new native thread本地内存线程数超过系统限制ulimit或代码里无节制地创建线程StackOverflowError虚拟机栈递归太深、方法循环调用常和 OOM 一起被面试官问我我们是Java heap space说明问题出在堆上。接下来按步骤来。第二幕保留现场——别让证据消失小刘那我重启服务我重启可以止血但别忘了保留现场。你确认 JVM 启动参数里有这两行吗-XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/tmp/小刘呃……好像没有。我这就是教训。加了这两个参数OOM 发生时 JVM 会自动 dump 一份内存快照相当于案发现场的监控录像。这次没配只能手动取了。我让他执行jmap -dump:formatb,fileheap.hprof 12345小刘文件好大……有 8GB我正常。dump 会暂停整个 JVMSTW生产高峰期慎用。如果只是想快速判断方向可以用更轻量的命令先看一眼jmap -histo 12345 | head -20这会列出对象数量和大小排行不用等几十 GB 的文件生成。第三幕法医鉴定——MAT 分析我们把 dump 文件拖到 Eclipse Memory AnalyzerMAT里打开。我MAT 打开后第一眼先看什么小刘看 Leak Suspects 报告我对。MAT 会自动帮你标出可疑的大对象相当于法医初步判断这里有问题。但别全信它还得自己验证。我点开Dominator Tree支配树按Retained Heap从大到小排序。小刘等等Shallow Heap 和 Retained Heap 有什么区别面试官爱问这个。我好问题。打个比方Shallow Heap一个 HashMap 对象自身占的内存可能就几十字节。Retained Heap这个 HashMap 被回收后能连带释放的所有内存——包括它引用的几百万个 Entry、每个 Entry 里的 key 和 value。加起来可能几个 GB。排查内存泄漏永远看Retained Heap不看 Shallow Heap。小刘找到了一个static ConcurrentHashMapRetained Heap 占了 6GB我 bingo。这就是经典泄漏模式之一。第四幕锁定真凶——常见泄漏模式我让小刘顺着 GC Roots 引用链往下追果然找到了业务代码// 罪魁祸首 private static final ConcurrentHashMapString, byte[] CACHE new ConcurrentHashMap(); public void process(String key, byte[] data) { CACHE.put(key, data); // 只 put从不 remove }我看到了吗static修饰的 Map当缓存用但没设上限、没设过期时间。数据只进不出迟早把堆吃光。我把常见的泄漏模式给他总结了一遍让他背下来static集合只增不减用ConcurrentHashMap当缓存却不设上限和过期策略。ThreadLocal用完不remove()线程池复用线程value 越积越多。无界缓存应该换成 Caffeine设maximumSizeexpireAfterWrite。数据库查询没加 limit一条 SQL 把整张表捞进ArrayList。连接/流没关连接池泄漏同时表现为获取连接超时。小刘那怎么判断是泄漏还是堆配置太小我看 dump 里的对象分布如果是大量该死没死的活对象业务对象被引用着GC 收不掉→内存泄漏改代码。如果对象分布正常只是堆确实太小 →配置不足加堆只是临时手段长期还是要优化。第五幕亡羊补牢——怎么防修复完代码后我给小刘列了一份防 OOM 清单上线前压测观察堆内存曲线看是否持续上涨不回落。缓存一律限界用 Caffeine 或 Guava Cache设最大容量和过期时间。慢查询日志兜底数据库查询必须加 limit大结果集分页处理。OOM 自动 dump 进启动模板-XX:HeapDumpOnOutOfMemoryError写进所有服务的启动脚本别再忘。dump 文件太大打不开调大 MAT 的启动内存修改MemoryAnalyzer.ini或先用jmap -histo缩小范围也可以用 Arthas 的heapdump分区导出。尾声天快亮了服务恢复正常。小刘杨工下次面试要是问 OOM 排查我知道怎么答了。我记住排查 OOM 就像破案分类型→留现场→取 dump→MAT 分析→顺引用链→定位代码。套路熟了半夜就不慌了。他点点头补了一句还有以后 JVM 启动参数我一定检查三遍。我笑了这才是今晚最大的收获。