用codex模拟oom和频繁fullgc
由于经常在面经中刷到如果oom,如果频繁fullgc后的排查流程是什么,所以我简单的用codex弄了个场景模拟。一共四种模式
/**
* 场景一:分配大对象,迫使大对象直接进入老年代。
* <p>
* 每次调用分配 2MB 的 byte[] 并保持引用。
* 频繁调用此接口会使老年代快速填满,触发 Full GC。
*/
@GetMapping("/large-object")
public Map<String, Object> allocateLargeObject() {
log.info("接收到请求:分配大对象");
memoryStressService.allocateLargeObject();
Map<String, 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 Map<String, Object> threadStorm(@RequestParam(defaultValue = "10") int n) {
log.info("接收到请求:启动线程风暴,线程数 = {}", n);
if (n > 50) {
n = 50; // 限制最大线程数,防止系统过载
}
memoryStressService.startThreadAllocationStorm(n);
Map<String, 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 Map<String, Object> stringIntern(@RequestParam(defaultValue = "50000") int n) {
log.info("接收到请求:String.intern 滥用,数量 = {}", n);
if (n > 200000) {
n = 200000; // 防止立即 OOM
}
memoryStressService.abuseStringIntern(n);
Map<String, 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 Map<String, Object> classLoaderLeak(@RequestParam(defaultValue = "1000") int n) {
log.info("接收到请求:ClassLoader 泄漏模拟,数量 = {}", n);
if (n > 10000) {
n = 10000;
}
memoryStressService.leakClassLoader(n);
Map<String, 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*:file=gc.log:time # JDK 9+ GC 日志

就会在我们指定路径下导出hprof文件
然后我们使用mat来进行分析当然可以用其他的。点击这个dominantor tree


就可以根据这个去找到

频繁fullgc
其实oom和频繁fullgc的区别就是oom系统已经停止运行了,而fullgc还是在运行的,可以说频繁fullgc就是oom的前期。
我总结以下流程
jps -l → 找到 Java 进程 PID
jstat -gcutil PID 1000 → 实时监控 GC 状况
分析 gc.log → 判断 Full GC 原因
jmap -dump → 导出堆快照(泄漏时)
MAT 分析 → 定位具体泄漏代码
注意不是一下子就导出快照而是要判断到底是不是泄漏,然后如果是真实的运行项目,这个导出快照的操作其实是很慢的,很消耗性能,所以说不能直接导出,最好等凌晨这种人少的时候。
这是整个排查流程中最关键的一步。gc.log 能把 Full GC 的触发原因、回收效果、频率全部记录下来。
| 日志关键词 | 含义 | 诊断结论 |
|---|---|---|
Pause Full 后,老年代大幅下降(如 900M→200M) |
Full GC 回收效果很好 | 堆内存太小,对象本身可回收,调大 -Xmx |
Pause Full 后,老年代几乎不降(如 255M→255M) |
Full GC 回收不掉 | 内存泄漏,需导出 dump 用 MAT 分析 |
Metadata GC Threshold |
元空间不足触发 Full GC | Metaspace 泄漏,用 jmap -clstats 排查 ClassLoader |
Evacuation Failure: Allocation |
G1 没有空闲 Region 来拷贝存活对象 | 对象晋升风暴,Survivor 区太小或并发太高 |
Humongous Allocation |
频繁创建超大对象 | 大对象问题,用 jmap -histo 看对象分布 |
GC Overhead Limit Exceeded |
GC 占用 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 Threshold |
jmap -clstats 查ClassLoader |
调大Metaspace | 缓存代理对象,避免重复创建 |
| G1疏散失败 | Evacuation Failure |
结合jstat -gcutil看堆使用率 |
调大堆、调大G1ReservePercent | 减少大对象,调整Region大 |
当然我这只是简单模拟一下,对流程有个比较清晰的认识,实际情况肯定本我这复杂的多,也难以排查的多。还是要有实际遇到过才能真正累计经验。如果你有不同的见解欢迎讨论,留言,如果我有错的地方一定要指出来感谢。
更多推荐

所有评论(0)