由于经常在面经中刷到如果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

判断逻辑

  1. 先看 Pause Full 前后的老年代变化:降得多是压力大,降不下是泄漏。

  2. 再看触发原因:Metadata GC Threshold → Metaspace 问题;Evacuation Failure → 晋升风暴;Humongous Allocation → 大对象。

  3. 如果 Full GC 后老年代降下来了,但很快又涨满 → 堆太小,调大 -Xmx

  4. 如果 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大

当然我这只是简单模拟一下,对流程有个比较清晰的认识,实际情况肯定本我这复杂的多,也难以排查的多。还是要有实际遇到过才能真正累计经验。如果你有不同的见解欢迎讨论,留言,如果我有错的地方一定要指出来感谢。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐