生产环境 JVM Metaspace 溢出让服务反复重启:我用 jcmd + GC 日志 5 分钟定位了类加载泄漏
2026/4/25 20:11:15 网站建设 项目流程

说实话,这次真被折腾得够呛。服务上线前跑得好好的,一部署到生产环境就开始疯狂重启。运维报障说 CPU 打满,K8s 显示 OOMKilled——但问题是,Java 进程被 kill 之前,Metaspace 已经悄悄飙到上限了。

如果你也遇到类似情况,这篇文章可能能帮你省下至少 2 小时排查时间。

问题:服务反复重启,Pod 被 OOMKilled

周一早上 9 点,运维群炸了。

「某某服务一直在重启,K8s 显示 OOMKilled。」

第一反应是内存不够,调大 limit 呗。512M → 1G → 2G。结果还是一样,过几个小时又炸了。

这就离谱了。内存给了这么多,为什么还是 OOM?

登录机器看了一下 Java 进程的启动参数:

psaux|grepjava

发现启动脚本里只配了-Xmx(堆内存),Metaspace 用的默认无限制。但 Java 11 默认的 MetaspaceSize 其实很小,稍微多加载点类就容易出问题。

先别急着重启服务。让我先用jcmd看看运行时状态。

排查:用 jcmd + GC 日志 5 分钟定位根因

第一步:查看 Metaspace 使用情况

# 找到 Java 进程 PIDjps-l# 查看 Metaspace 详情jcmd<PID>VM.native_memory summary

输出里有这么一段:

Native Memory Tracking: Total: reserved=524MB, committed=524MB - Metaspace: reserved=256MB, committed=256MB

256MB 的 Metaspace 已经 committed 满了。而这个服务是个老 Java 8 项目升级到 Java 11,里面有大量的动态类生成——主要是 RPC 框架的序列化类和各种脚本引擎。

第二步:打开 GC 日志看类加载趋势

加上这几个参数重启服务(不用停服,下个版本发版时带上):

-XX:+UseG1GC\-XX:+PrintGCDetails\-XX:+PrintGCDateStamps\-Xloggc:/var/log/java/gc.log\-XX:+UnlockDiagnosticVMOptions\-XX:+LogCompilation\-XX:MetaspaceSize=128M\-XX:MaxMetaspaceSize=256M

然后用jcmd实时监控:

# 每 5 秒输出一次类加载统计watch-n5'jcmd <PID> GC.class_stats | head -20'

跑了半小时后,看到一个吓人的数字:类加载数量一直在涨,没有下降趋势。

正常 JVM 类卸载是有条件的:类加载器不可达 + 所有实例被回收。但这里的业务代码里有个问题——自定义的 GroovyClassLoader 一直没释放,导致它加载的类永远无法卸载。

第三步:定位泄漏点

看 GC 日志里有没有类卸载记录:

grep"class unloading"/var/log/java/gc.log

输出是空的。说明这半小时内,一个类都没卸载过。

再用jmap导出堆快照(生产慎用,建议先引流):

jmap-histo:live<PID>|head-50

看对象分布,发现GroovyClassLoader的实例数量是 0,说明主类加载器没问题。但业务代码里还有一层委托的URLClassLoader,它的引用链是这样的:

Root → Spring Context → SomeService → CustomClassLoader → 大量动态生成的类

SomeService 是个单例 bean,持有了一个一直没清理的 ClassLoader 引用。每次 RPC 调用返回新的类实例,这个 ClassLoader 就加载更多类,而且永远不会被 GC。

解决:修复类加载器泄漏 + 限制 Metaspace

方案一:修复代码(推荐)

在 SomeService 销毁时手动清理 ClassLoader:

@PreDestroypublicvoiddestroy(){if(customClassLoader!=null){// 清理引用customClassLoader.clearAssertionStatus();customClassLoader=null;}}

但更根本的问题是业务逻辑——每次 RPC 调用都创建新的类实例,这本身就是设计缺陷。重构为复用类实例才是正解。

方案二:限制 Metaspace 上限(治标)

-XX:MetaspaceSize=256M\-XX:MaxMetaspaceSize=512M\-XX:+HeapDumpOnOutOfMemoryError\-XX:HeapDumpPath=/var/log/java/oom_dump.hprof

这样即使泄漏,也会先 OOM 而不是把系统拖垮。

方案三:切到 Java 11 的默认 G1 GC

Java 8 默认是 Parallel GC,Metaspace 回收策略比较保守。切到 G1 后,Metaspace GC 会更积极:

-XX:+UseG1GC\-XX:MaxGCPauseMillis=200\-XX:+PrintGCDetails

验证:修复后 Metaspace 稳定在 150M

上线修复版本后,继续监控:

# 监控 Metaspace 趋势whiletrue;dojcmd<PID>VM.native_memory summary|grepMetaspacesleep30done

跑了 24 小时,Metaspace 稳定在 150M 左右,不再持续增长。服务也没再重启。

再看 GC 日志,终于出现了:

[class unloading: 1234 classes, 5678KB]

类卸载终于生效了。

写在最后

这次踩坑给我的教训是:Java 内存不只是 Heap,Metaspace 也要管。

以前写 Java 8 代码时,Metaspace 几乎不用操心,因为默认无限制。但升级到 Java 11+ 后,默认的 MetaspaceSize 变小了,而且 G1 GC 的回收策略也不同了。

几个建议:

  1. 启动参数里一定要配 MetaspaceSize 和 MaxMetaspaceSize,不要依赖默认值
  2. 自定义 ClassLoader 用完一定要清理,尤其是结合脚本引擎使用时
  3. GC 日志一定要开,这玩意儿出问题时不看日志基本没法排查
  4. 监控要加 Metaspace 指标,别只盯着 Heap Used

最后,如果你的服务也遇到反复重启、内存给够了还是 OOM 的情况,不妨先jcmd看看 Metaspace。说不定根因就在这儿。


有问题欢迎评论区交流。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询