OOM 治理与排查:从体系建设到命令实战
发布时间:2026/11/3 10:10:51 锦皓数字建站

一、问题与主线我会从两个层次处理 OOM体系治理一次事故发生后怎样补监控、改规范、做工具让团队不再重复踩坑。现场排查线上机器内存异常时具体执行哪些命令怎样从现象定位到代码。一句话概括现场排查要把这一次事故查明白体系治理要把排查经验变成团队默认能力。二、体系治理把一次事故变成三项具体改进治理不能只停留在“建设体系”必须说清楚发现了什么缺口、做了什么、最后怎样证明有效。以无界缓存导致堆 OOM 为例我会推动三项改进。1. 先统一运行基线让事故发生时有现场过去的问题是每个服务的 JVM 参数由开发自己配置。有的没有 GC 日志有的 OOM 后没有 Heap Dump事故发生后只能重启和猜测。我会把以下配置放进统一启动模板-Xlog:gc*:file/data/logs/gc.log:time,uptime,level,tags:filecount10,filesize100M-XX:HeapDumpOnOutOfMemoryError-XX:HeapDumpPath/data/dump-XX:ExitOnOutOfMemoryError同时检查 dump 目录的磁盘空间、访问权限和敏感数据管理避免“参数配了但事故时写不出来”。这项改进的验收很直接抽查核心服务确认 GC 日志可查询、dump 目录可写并通过测试进程验证 OOM 后能够生成文件。2. 再把无界资源变成 Code Review 红线这次事故的直接原因是静态 Map 只写不删但只修一个 Map 没有意义其他服务仍可能存在相同问题。我会在 Review 模板中加入四个问题这个集合、缓存、队列或线程池会不会随请求增长 最大容量是多少 数据什么时候删除或过期 达到上限后怎样降级对于本地缓存统一使用带容量和过期策略的组件不允许用裸HashMap、ConcurrentHashMap承担无界缓存职责。这不是说项目中不能使用 Map而是长生命周期容器必须有明确的数据边界。3. 最后补上能够提前发现问题的告警只监控“堆使用率超过 80%”不够。业务流量升高时堆短暂达到 80% 可能是正常现象真正危险的是 Full GC 后仍然降不下来。我会增加三组观察指标趋势Full GC 后存活集是否连续上涨代价Full GC 次数、停顿时间和接口 P99增长源缓存条目数、队列深度、线程数等业务资源指标。告警后不是立刻通知所有人而是把服务名、机器、PID、Heap 趋势、最近一次发布和排查手册一起带出来减少临时找信息的时间。4. 怎样证明这些工作有价值我不会用“规范已经发布”作为结果而会检查核心服务是否全部接入 GC 日志和 OOM dump新增缓存是否都声明容量和过期策略告警能否在 OOM 前发现存活集持续上涨同类问题的平均发现时间和平均恢复时间是否下降后续是否再次出现无界缓存导致的 OOM。治理的价值不是泛泛地谈平台和流程而是把一次事故沉淀为启动模板、Review 卡点和可观测指标。三、现场实战登录机器后如何用命令排查下面以 Linux 机器上的订单服务为例。现象是接口越来越慢进程内存持续上涨最后出现java.lang.OutOfMemoryError: Java heap space。排查顺序是确认机器和进程 → 判断堆是否异常 → 观察 GC 后能否回落 → 保存低成本证据 → 生成 Heap Dump → MAT 找到持有者 → 回到代码证明增长机制1. 先判断是不是 Java 进程占用内存先看整机内存free-hvmstat15重点看可用内存和是否发生持续 swap。然后找出高内存进程top-o%MEM也可以使用ps-eopid,ppid,%mem,rss,vsz,cmd--sort-rss|head这里先得到两个结论是不是 Java 进程占用了主要内存进程 RSS 是持续上涨还是只有某个瞬时峰值。RSS是进程当前驻留在物理内存中的部分不等于 Java Heap。RSS 高只能证明进程占内存不能直接证明堆泄漏。2. 找到 PID确认 JVM 参数和运行时间jps-lv假设订单服务 PID 是12345先查看启动参数jcmd12345VM.command_line jcmd12345VM.flags jcmd12345VM.uptime重点确认-Xms、-Xmx是多少使用什么垃圾收集器进程运行了多久是否配置 GC 日志和 OOM dump。如果jcmd无法附加需要确认执行用户、权限、容器隔离和 JDK 工具是否匹配不能为了方便直接修改线上安全策略。3. 用jstat判断是不是堆持续上涨jstat-gcutil12345100020这表示每秒采样一次共采样 20 次。重点观察O老年代使用比例FGCFull GC 次数FGCTFull GC 累计耗时。如果看到O 持续接近 100% FGC 不断增加 Full GC 后 O 仍然维持高位说明大量对象在 Full GC 后仍然存活优先怀疑长期对象积累、无界缓存或引用未释放。但 20 秒采样只能看到短期状态。判断慢性增长还要结合一段时间的 GC 日志和监控曲线。4. 先获取 Class Histogram缩小对象范围如果进程仍能响应可以先获取对象直方图jcmd12345GC.class_histogram/tmp/order-histogram.txt或者jmap-histo12345|head-50输出可能类似num #instances #bytes class name 1 3200000 900000000 [B 2 3100000 400000000 com.example.OrderDetail 3 3300000 105000000 java.util.HashMap$NodeHistogram 只能说明哪些对象数量多、浅大小大不能证明谁在持有它们。另外在线上执行 Class Histogram 可能有明显开销尤其是对象很多时。核心机器上应先移除业务流量或在低峰操作不要把诊断命令当成零成本操作。5. 生成 Heap Dump先检查磁盘空间df-h/data/dump确保空间足够、目录权限正确后再生成 dumpjcmd12345GC.heap_dump /data/dump/order-12345.hprof也可以使用jmap-dump:formatb,file/data/dump/order-12345.hprof12345生产环境注意三件事Heap Dump 可能接近当前堆大小dump 过程可能造成停顿和额外 I/Odump 中可能包含订单、手机号、Token 等敏感数据。所以要先评估业务影响、磁盘空间和数据安全。若进程已经频繁 Full GC、几乎无法响应应优先使用 OOM 时自动生成的 dump避免临时命令把服务彻底压垮。6. 在 MAT 中完成可达性分析把.hprof导入 MAT 后按以下顺序查看第一步Histogram确认OrderDetail、HashMap$Node等对象数量是否异常。第二步Dominator Tree查看哪个对象的 Retained Heap 最大。案例中一个静态 Map 保留了大部分订单对象。第三步Path to GC Roots对某个OrderDetail执行Path to GC Roots并优先排除弱引用路径得到System Class → OrderCacheService.class → static LOCAL_CACHE → ConcurrentHashMap.table → Node.value → OrderDetail这条路径说明OrderCacheService.class属于 GC Roots 可达范围静态字段LOCAL_CACHE是普通强引用Map 节点继续强引用OrderDetail因此订单对象经历 Full GC 后仍然可达不能被回收。这里不是 GC 回收失败而是应用通过强引用告诉 GC这些对象仍然有效。7. 回到代码证明对象为什么持续增加发现引用链后还不能立刻宣布根因。必须回到代码验证增长机制privatestaticfinalMapLong,OrderDetailLOCAL_CACHEnewConcurrentHashMap();publicOrderDetailget(longorderId){OrderDetailvalueLOCAL_CACHE.get(orderId);if(value!null){returnvalue;}valuerepository.findById(orderId);LOCAL_CACHE.put(orderId,value);returnvalue;}需要确认是否只有put没有删除是否没有最大容量和过期策略key 的业务基数是否持续增长是否有多个类似缓存分别保存同一份数据。最终因果链是订单 ID 持续增长 → 新订单不断写入静态 Map → Map 没有容量和淘汰 → Map 通过强引用持有 OrderDetail → 对象从 GC Roots 始终可达 → Full GC 后老年代无法回落 → Java heap space只有“监控趋势 Histogram Dominator Tree GC Roots 引用链 代码增长机制”相互吻合根因才算成立。8. 修复后怎样验证修复不是简单换成 Caffeine而是明确缓存契约最大容量或最大权重过期时间缓存对象的数据范围并发加载和热点保护未命中后的数据库承载能力命中、淘汰、加载失败和当前容量指标。灰度后继续观察jstat-gcutil12345100060同时验证Full GC 后老年代是否形成稳定平台Full GC 次数和停顿是否下降缓存容量是否被硬限制数据库 QPS、连接池和慢查询是否正常接口 P99 和错误率是否恢复。验证重点不是“一小时内没有 OOM”而是原来的内存增长机制已经消失。四、3 分钟口述版我处理 OOM 会分成体系治理和现场排查两个层次。体系治理上我不会泛泛地说建设稳定性体系而是把一次事故落实成三项改进。第一把 GC 日志、OOM 自动 dump 和退出策略放进统一 JVM 启动模板确保事故发生时有现场第二把缓存、集合、队列和线程池是否有界加入 Code Review标准缓存必须声明容量、过期和回源策略第三补充 Full GC 后存活集、缓存容量和接口 P99 告警争取在 OOM 前发现问题。最后通过基线覆盖率、发现时间、恢复时间和同类事故复发率验证效果。现场排查时我会先用free、top和ps确认机器内存以及高 RSS 进程再用jps和jcmd确认 PID、堆参数和运行时间。接着用jstat -gcutil观察老年代和 Full GC。如果 Full GC 不断发生但老年代仍降不下来说明长期存活对象在积累。然后我先用jcmd GC.class_histogram看异常对象类型在评估停顿、磁盘和敏感数据风险后生成 Heap Dump。MAT 中通过 Histogram 找到大量订单对象通过 Dominator Tree 找到保留内存最大的静态 Map再通过 Path to GC Roots 证明OrderDetail沿static LOCAL_CACHE的强引用链始终可达。最后回到代码确认缓存只有写入没有容量和淘汰而订单 ID 基数持续增长。修复时重新定义缓存容量、过期、并发加载和数据库回源策略。上线后继续用 GC 后存活集、缓存容量、数据库负载和接口 P99 验证确认消失的是增长机制而不只是暂时没有再次 OOM。五、命令速记# 1. 看机器和进程内存free-hvmstat15top-o%MEMps-eopid,ppid,%mem,rss,vsz,cmd--sort-rss|head# 2. 找 Java PID 和参数jps-lvjcmd12345VM.command_line jcmd12345VM.flags jcmd12345VM.uptime# 3. 看 GC 趋势jstat-gcutil12345100020# 4. 看对象分布jcmd12345GC.class_histogram/tmp/order-histogram.txt jmap-histo12345|head-50# 5. 检查空间并生成 Heap Dumpdf-h/data/dump jcmd12345GC.heap_dump /data/dump/order-12345.hprof速记顺序不是“见命令就执行”而是先提出判断再选择能够验证判断、风险最小的命令。六、常见追问为什么不先增加-Xmx如果对象无界增长加堆只会推迟 OOM。应先判断是内存泄漏、容量不足还是非堆问题。确认没有异常增长后才根据业务工作集和 GC 表现调整堆大小。为什么 Histogram 不能直接定位根因Histogram 只能说明某类对象数量多、浅大小大不能说明它们为什么存活。还要通过 Dominator Tree 找主要持有者通过 Path to GC Roots 找到阻止回收的引用链。为什么静态 Map 中的对象无法回收普通字段和集合元素默认是强引用。只要类仍被加载就存在GC Root → Class → static Map → OrderDetail的强引用链因此对象保持可达。为什么不在代码里大量使用弱引用解决泄漏弱引用只改变回收语义不能替代容量设计。业务缓存需要可预测的容量、过期和回源策略通常应使用有明确边界的缓存组件。真正应修复的是错误的对象生命周期和无界资源。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。