由于经常在面经中刷到如果oom如果频繁fullgc后的排查流程是什么所以我简单的用codex弄了个场景模拟。一共四种模式/** * 场景一分配大对象迫使大对象直接进入老年代。 * p * 每次调用分配 2MB 的 byte[] 并保持引用。 * 频繁调用此接口会使老年代快速填满触发 Full GC。 */ GetMapping(/large-object) public MapString, Object allocateLargeObject() { log.info(接收到请求分配大对象); memoryStressService.allocateLargeObject(); MapString, Object result new HashMap(); result.put(场景, 大对象分配 - 迫使对象直接进入老年代); result.put(状态, memoryStressService.getLargeObjectStats()); result.put(建议工具, jstat -gcutil pid 1000 | jmap -histo pid | jvisualvm); return result; } /** * 场景二线程风暴 —— 启动多个线程反复分配临时对象。 * p * 大量临时对象导致 Yong GC 频繁部分对象晋升到老年代 * 晋升速率快于老年代回收速率时触发 Full GC。 * * param n 启动的线程数量默认 10 */ GetMapping(/thread-storm) public MapString, Object threadStorm(RequestParam(defaultValue 10) int n) { log.info(接收到请求启动线程风暴线程数 {}, n); if (n 50) { n 50; // 限制最大线程数防止系统过载 } memoryStressService.startThreadAllocationStorm(n); MapString, Object result new HashMap(); result.put(场景, 线程风暴 - 多线程高频分配加速对象晋升); result.put(启动线程数, n); result.put(当前总活跃, memoryStressService.getThreadStormCount()); result.put(建议工具, jstat -gcutil pid 1000 | jstack pid 查看线程); return result; } /** * 场景三String.intern() 滥用 —— 字符串常量池膨胀。 * p * 大量不同的字符串被 intern() 后进入字符串常量池 * 常量池中的字符串无法被 GC 回收导致老年代持续增长。 * * param n 要 intern 的字符串数量默认 50000 */ GetMapping(/string-intern) public MapString, Object stringIntern(RequestParam(defaultValue 50000) int n) { log.info(接收到请求String.intern 滥用数量 {}, n); if (n 200000) { n 200000; // 防止立即 OOM } memoryStressService.abuseStringIntern(n); MapString, Object result new HashMap(); result.put(场景, String.intern() 滥用 - 字符串常量池膨胀); result.put(intern 数量, n); result.put(累计 intern 数, memoryStressService.getInternedCount()); result.put(建议工具, jmap -histo:live pid | jhat / jvisualvm 分析 char[] 实例); return result; } /** * 场景四自定义 ClassLoader 泄漏 —— 模拟 Metaspace元空间持续增长。 * p * 每次调用使用新的 ClassLoader 加载类并保持 Class 引用 * 导致 Metaspace 中的 Klass 元数据无法卸载。 * * param n 要加载的类数量默认 1000 */ GetMapping(/classloader) public MapString, Object classLoaderLeak(RequestParam(defaultValue 1000) int n) { log.info(接收到请求ClassLoader 泄漏模拟数量 {}, n); if (n 10000) { n 10000; } memoryStressService.leakClassLoader(n); MapString, Object result new HashMap(); result.put(场景, ClassLoader 泄漏 - Metaspace 持续增长); result.put(本次加载类数, n); result.put(累计加载类数, memoryStressService.getLoadedClassCount()); result.put(建议工具, jstat -gcmetacapacity pid | jmap -clstats pid 查看类加载器统计); return result; }排查流程oom先说一下oom吧其实不管是oom还是频繁fullgc我觉得参数都是很重要的我用到的-Xms256m -Xmx256m # 初始堆 最大堆避免动态扩容-XX:HeapDumpOnOutOfMemoryError # OOM 时自动导出堆快照-XX:HeapDumpPath./heapdump.hprof # dump 文件路径-Xlog:gc*:filegc.log:time # JDK 9 GC 日志就会在我们指定路径下导出hprof文件然后我们使用mat来进行分析当然可以用其他的。点击这个dominantor tree就可以根据这个去找到频繁fullgc其实oom和频繁fullgc的区别就是oom系统已经停止运行了而fullgc还是在运行的可以说频繁fullgc就是oom的前期。我总结以下流程jps -l → 找到 Java 进程 PIDjstat -gcutil PID 1000 → 实时监控 GC 状况分析 gc.log → 判断 Full GC 原因jmap -dump → 导出堆快照泄漏时MAT 分析 → 定位具体泄漏代码注意不是一下子就导出快照而是要判断到底是不是泄漏然后如果是真实的运行项目这个导出快照的操作其实是很慢的很消耗性能所以说不能直接导出最好等凌晨这种人少的时候。这是整个排查流程中最关键的一步。gc.log能把 Full GC 的触发原因、回收效果、频率全部记录下来。日志关键词含义诊断结论Pause Full后老年代大幅下降如900M→200MFull GC 回收效果很好堆内存太小对象本身可回收调大-XmxPause Full后老年代几乎不降如255M→255MFull GC 回收不掉内存泄漏需导出 dump 用 MAT 分析Metadata GC Threshold元空间不足触发 Full GCMetaspace 泄漏用jmap -clstats排查 ClassLoaderEvacuation Failure: AllocationG1 没有空闲 Region 来拷贝存活对象对象晋升风暴Survivor 区太小或并发太高Humongous Allocation频繁创建超大对象大对象问题用jmap -histo看对象分布GC Overhead Limit ExceededGC 占用 98% CPU 但回收不到 2%JVM 放弃挣扎直接 OOM判断逻辑先看Pause Full前后的老年代变化降得多是压力大降不下是泄漏。再看触发原因Metadata GC Threshold→ Metaspace 问题Evacuation Failure→ 晋升风暴Humongous Allocation→ 大对象。如果 Full GC 后老年代降下来了但很快又涨满 → 堆太小调大-Xmx。如果 Full GC 后老年代纹丝不动 → 泄漏导 dump 用 MAT。总结最终我总结了一下线上频繁 Full GC 的排查最终都会收敛到两条路上要么是“压力太大”要么是“内存泄漏”。然后还有特殊情况现在就是有两种。根因GC日志特征快速定位临时止血根治压力大Full GC后老年代大幅下降jmap -histo看对象分布调大-Xmx优化代码减少对象创建内存泄漏Full GC后老年代几乎不降jmap -dump MAT分析Leak Suspects重启丢现场修复泄漏代码Metaspace泄漏Metadata GC Thresholdjmap -clstats查ClassLoader调大Metaspace缓存代理对象避免重复创建G1疏散失败Evacuation Failure结合jstat -gcutil看堆使用率调大堆、调大G1ReservePercent减少大对象调整Region大当然我这只是简单模拟一下对流程有个比较清晰的认识实际情况肯定本我这复杂的多也难以排查的多。还是要有实际遇到过才能真正累计经验。如果你有不同的见解欢迎讨论留言如果我有错的地方一定要指出来感谢。