Java元空间Metaspace泄漏排查:jstat+jcmd+Arthas三重定位法

发布时间:2026/10/10 9:27:10
Java元空间Metaspace泄漏排查:jstat+jcmd+Arthas三重定位法 线上Java服务如果反反复复出现重启重启前日志里飘着一句java.lang.OutOfMemoryError: Metaspace监控面板上Metaspace的committed一路爬升used却低得像在嘲笑你那基本可以断定元空间正在泄漏。这种事我遇到不止一次每次排查都感觉像在拆一个嵌套的盲盒。用jstat看现象、用jcmd挖对象、用Arthas锁代码这套三重定位法是我这几年排查Metaspace泄漏用得最顺手的组合今天把完整思路和实操过程展开讲清楚适合正在被Java进程内存问题折磨的后端开发、运维和SRE同学参考。先说结论Metaspace泄漏不是堆内存泄漏它藏在本地内存Native Memory里常规的堆dump分析经常失效必须从类加载器回收这个根子上入手。全文不堆概念直接按真实排查顺序来你跟着操作就能复现并解决同类问题。1. 先搞清楚Metaspace为什么会泄漏1.1 元空间的内存模型committed与used的差距从哪来JDK 8之后永久代PermGen被移除类元数据搬到了Metaspace。它使用的是进程本地内存不再受-Xmx管理默认情况下最大容量只受物理内存和操作系统限制。这带来一个很直观的后果堆内存不够了JVM会按部就班地触发GC但Metaspace如果无节制增长进程可能直接被操作系统杀掉连OOM日志都来不及写完整。Metaspace里存的是什么类结构元信息Klass、方法字节码、常量池、注解、字段描述、方法签名等说白了就是JVM用来描述这个类长什么样的C对象。每一个被加载的类都要在这块内存里占一份位置。类卸载之后这块内存才可能被回收。关键点在于Metaspace的内存分配方式是chunk级别的。JVM会为类加载器按需分配chunkchunk内部再用free block来管理。类加载器释放之后部分chunk并不会立即归还操作系统大的chunk会返还给虚拟内存小的chunk则被放进Metaspace的全局空闲列表里供后续分配复用。这个机制带来的直接现象就是committed内存居高不下而used只占其中一小部分。所以你在监控里看到committed远大于used时先别急着下结论说是泄漏它可能是正常的碎片化也可能确实是类加载器没有被回收需要进一步区分。另外还有一个容易忽略的CCSCompressed Class Space也就是压缩类空间。启用-XX:UseCompressedClassPointers之后Klass指针被压缩成32位最大容量默认1GB。CCS的committed和used同样值得关注它和Metaspace主空间是分开统计的排查时两个都要看。1.2 泄漏的本质类加载器无法回收Metaspace里的类元数据要想被回收前提是它对应的类加载器变得不可达同时该类加载器加载的所有类都不存在存活实例和引用。这两个条件只要有一个不满足类的元数据就会一直挂在Metaspace里。实际生产中最常见的泄漏原因就是自定义类加载器被长持有。典型场景包括每个请求或每次任务都new一个自定义ClassLoader加载完类之后丢进静态列表、缓存或者ThreadLocal里不清理。频繁使用CGLIB、ASM、ByteBuddy生成代理类或增强类尤其是配合Spring、Hibernate这类框架做缓存时。脚本引擎反复编译脚本例如Groovy每次执行都生成新的类。Web容器热部署老应用的老类加载器没有被完全释放多个版本叠加。反射调用配合自定义类加载器在一个循环里不断加载新类。这一类泄漏和堆内存泄漏有个明显差异堆里对象多GC日志能看到Old区涨dump文件直接分析引用链就行Metaspace泄漏的堆往往是干净的Old区甚至很空闲但本地内存和类加载器数量一直在涨。所以排查工具和方法论必须换个思路不能再指望一份heap dump解决所有问题。2. 工具选型jstat、jcmd、Arthas怎么分工2.1 为什么要用这三个组合单个工具都有局限。jstat属于速览型能快速给出Metaspace使用量、类加载数量和GC频率但它看不到代码层面也看不出是哪个类加载器在作祟。jcmd属于对象型是JDK自带的诊断命令能深入Metaspace内部查看chunk管理也能输出类统计信息但它操作起来偏静态没法动态追踪热点代码。Arthas属于在线手术刀能挂载到运行中的Java进程实时看类加载器树形结构、反编译字节码、抓CPU火焰图是定位到具体代码行的关键。我把这套组合定位成三个递进阶段先用jstat确认是不是在泄漏再用jcmd锁定是哪一类对象在增长最后用Arthas揪出是哪段代码在反复创建类加载器。每走一步问题范围就从进程级缩小到对象级再到代码级排查效率高很多。2.2 jstat盯住MC、MU和加载数量jstat是JDK自带的零成本直接对目标进程执行即可。用它干两件事第一件是看Metaspace的使用量和容量第二件是看类加载数量。jstat -gc pid 1s 10这条命令每秒输出一次连续10次。重点看MC和MU两列MC是Metaspace当前容量committedMU是Metaspace实际使用量单位都是KB。CCSC和CCSU对应压缩类空间的容量和使用量同样需要关注。判定趋势的时候不能只看单次值要看曲线。如果MU持续增长比如每隔几秒刷新就上一个台阶而且YGC/FGC还回收不下来说明类元数据在被持续创建且无法释放。这时候再配合jstat -class pid看一下Loaded和Unloaded的数量。正常稳定运行的Java进程Loaded数量应该基本持平如果Loaded每秒都在涨Unloaded几乎为0基本可以判定类加载器泄漏了。这里有个实操细节jstat的PID要和你自己在命令行top里看到的Java进程PID对应如果是容器部署先找到宿主机上对应的Java进程别对着Pod的PID去执行。2.3 jcmd深入Metaspace内部和类加载器统计jcmd同样是JDK自带的命令比jstat更细。需要先解锁诊断选项最好在JVM启动时就加上-XX:UnlockDiagnosticVMOptions排查时依次执行两条命令。第一条是看Metaspace的整体内存分配情况jcmd pid VM.metaspace输出里能看到Usage、Committed、Virtual space reserved、Chunk manager、GC threshold这些信息。重点看Committed和Used的比例以及Chunk manager里的chunk数量。如果chunk数量非常多、碎片很严重说明存在大量短命类加载器。第二条是看类的统计信息jcmd pid GC.class_stats这命令会输出所有类的元数据统计包含类名、加载器、字节数、方法数等。执行之后最好再按加载器聚合一下看看哪个类加载器实例占用的元空间最多。通常你会看到某些自定义ClassLoader出现几百上千个实例每个实例都带着一批类这就是泄漏的最直接证据。注意GC.class_stats在某些JDK版本上会触发较长的安全点停顿生产环境如果比较敏感尽量在低峰期执行或者先确认是否开启诊断选项。负载过高的核心交易链路不建议贸然使用。2.4 Arthas在线反编译、类加载器树和火焰图Arthas的威力在动态和在线。你不需要重启进程不需要预先埋点直接attach上去就能看。启动Arthas非常简单java -jar arthas-boot.jar选择目标Java进程编号后先执行dashboard看一眼全局内存里面会显示Metaspace的使用情况。然后重点用三个能力。第一个是classloader命令。执行classloader -t会按树形结构展示当前进程里所有类加载器的父子关系和实例数量。如果你看到某个类加载器几十上百个实例平铺在树里而它们的parent都是同一个系统类加载器那基本就是从同一个代码路径new出来没有被回收的。还可以指定classloader -l看每个类加载器的URLClassPath判断它是加载了哪个目录或jar包下的类。第二个是sc/jad命令。用sc -d 类名可以查看类的详细信息包括类加载器、类路径、注解等。用jad 全限定类名可以直接反编译目标类的字节码看到现场跑的实际代码这对于确认是不是某个框架生成的动态代理类特别有用。第三个是profiler命令。执行profiler start profiler stop --format html会生成一份火焰图能看到CPU热点。Metaspace泄漏往往伴随着类加载和反射调用的高CPU开销火焰图里会有一个高频调用链顺着调用链就能找到反复创建类加载器的方法。新版Arthas内置的async-profiler多数支持--event alloc之类的事件采样可以用来观察内存分配热点不过不同版本支持度不一样用之前先查一下当前版本的profiler help。3. 实战复现构造一个Metaspace泄漏现场3.1 写一个可复现的泄漏Demo理论说再多不如直接跑一遍。我准备了一个非常简单的Demo模拟线上最常见的循环创建类加载器并持有引用的泄漏模式。新建两个Java文件。package com.demo; /** * 一个简单的业务类用来被自定义类加载器反复加载。 */ public class SampleLogic { public static void hello() { System.out.println(sample logic executed); } }package com.demo; import java.lang.reflect.Method; import java.net.URL; import java.net.URLClassLoader; import java.util.ArrayList; import java.util.List; /** * 模拟Metaspace内存泄漏 * 每个循环都new一个URLClassLoader加载同一个类并把类加载器保存到静态List中 * 使类加载器永远无法被GC回收类元数据也就一直留在Metaspace里。 */ public class MetaSpaceLeakDemo { // 关键用静态List持有类加载器引用阻止回收 private static final ListClassLoader HOLDER new ArrayList(); public static void main(String[] args) throws Exception { URL url MetaSpaceLeakDemo.class.getProtectionDomain() .getCodeSource().getLocation(); while (true) { URLClassLoader cl new URLClassLoader(new URL[]{url}, null); Class? cls cl.loadClass(com.demo.SampleLogic); // 反射调用模拟真实业务场景里的反射热点 Method method cls.getMethod(hello); method.invoke(null); // 持有类加载器引用模拟线上长期缓存 HOLDER.add(cl); Thread.sleep(1); if (HOLDER.size() % 1000 0) { System.out.println(holder size: HOLDER.size()); } } } }启动时加上这些JVM参数java -XX:UnlockDiagnosticVMOptions \ -XX:MaxMetaspaceSize256m \ -XX:MetaspaceSize32m \ -XX:MaxMetaspaceExpandRatio5 \ -Xmx256m -Xms256m \ -Xloggc:gc.log \ com.demo.MetaSpaceLeakDemo这里的-XX:MaxMetaspaceSize256m很关键。如果不设上限泄漏会一直吞噬本地内存直到进程被OS杀掉排查窗口很短。设了上限后Metaspace增长到256m时会触发频繁Full GC最后抛出OOM给你留出观察空间。-XX:MetaspaceSize32m是初始阈值意思是Metaspace用到32m先触发第一次GC方便早点看到趋势。3.2 第一重定位jstat快速确认泄漏趋势Demo启动后找到它的PIDps -ef | grep MetaSpaceLeakDemo然后执行jstat -gc pid 1s 10几秒后你会看到类似下面的输出S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT 5120.0 5120.0 0.0 0.0 31744.0 31744.0 174848.0 13744.2 33792.0 33088.0 4096.0 3560.0 5 0.123 3 0.456 0.579注意MC和MU在持续变大。刚启动时MC可能是几MB几十秒后到了几十MB再过一会儿逼近-XX:MaxMetaspaceSize256m的限制。同时FGC的次数在不断增加但Metaspace的使用量并不下降说明GC回收不了这些类元数据。再执行jstat -class pid会看到Loaded数字不断上升比如从1000涨到5000、10000而Unloaded数量几乎不动。到这一步已经可以明确存在类加载器泄漏Metaspace正在被持续消耗。第一阶段的目标达成接下来要找具体的元凶对象。3.3 第二重定位jcmd锁定类加载器层级继续对同一个进程执行jcmd先看Metaspace内部细节jcmd pid VM.metaspace输出中能看到类似下面的信息Metaspace: Usage: 96.00 MB [Used: 92.40 MB, Committed: 96.00 MB] Virtual space: reserved: 512.00 MB committed: 96.00 MB Chunk manager: blocks: 224 KB, chunks: 1102 free chunks: ...重点看chunks数量。如果chunks数量在反复增长并且有很多free chunks无法被合并使用说明存在大量短命类加载器留下的碎片。这里你会体会到committed远大于used是怎么来的——很多chunk明明已经空了但因为碎片化严重无法整体归还给操作系统。接着用类统计命令看类加载器jcmd pid GC.class_stats输出非常长建议在本地先输出到文件再分析jcmd pid GC.class_stats class_stats.txt然后按类加载器名称聚合统计awk {print $3} class_stats.txt | sort | uniq -c | sort -nr由于不同JDK版本输出列顺序有差异实际执行时先head看下字段含义再调整。不过你大概率会看到两个特征一是java.net.URLClassLoader类名大量出现二是对应的地址各不相同代表多个实例。每个URLClassLoader下面都挂着一批com.demo.SampleLogic的类元数据。还可以用jmap的堆直方图做交叉验证。运行jmap -histo pid | head -30会看到类似num #instances #bytes class name 1: 10315 412600 java.net.URLClassLoader 2: 10315 288820 java.net.URLClassLoader$1 3: 10315 165040 com.demo.SampleLogic实例数量破万而且还在增加这就是类加载器层层叠叠的实锤。到这步问题已经缩小到了代码里某处在不断new URLClassLoader并持有不释放。剩下的工作就是找出那行代码。3.4 第三重定位Arthas揪出创建类加载器的代码attach Arthas到目标进程java -jar arthas-boot.jar输入进程编号进入交互控制台。第一步执行classloader -t你会看到输出里出现大量重复的URLClassLoader节点实例数量还在跳动。这个结果和jcmd里看到的一致但Arthas的优势在于可以直接操作对象。第二步是直接搜类加载器对应的类路径。执行classloader -l查看到URLClassLoader的URLClassPath指向的路径如果指向的都是同一个地方说明这些实例是从同一份代码路径new出来的。第三步是抓CPU火焰图。执行profiler start让它跑几十秒然后profiler stop --format html浏览器打开生成的火焰图你会看到一条很宽的调用栈MetaSpaceLeakDemo.main一路调用URLClassLoader的构造方法和loadClass、getMethod。火焰图上反复出现的高塔就是泄漏代码的入口。第四步在线反编译确认代码。执行jad com.demo.MetaSpaceLeakDemoArthas会直接把当前运行的字节码反编译出来你会看到new URLClassLoader这一行的真实上下文甚至能看到HOLDER这个静态List。现场代码和Demo代码一一对上之后修复方案就变得很直接了。到这里三重定位的全部链路已经走通jstat给出现象jcmd给出对象证据Arthas给出代码位置。生产环境排查时这套流程完全可以平移只是把Demo换成真实业务而已。4. 常见问题与排查技巧实录4.1 线上疑似Metaspace泄漏先做这4件事遇到线上Metaspace OOM或指标异常我建议先花5分钟完成四步快速检查再去跑上面的三重定位。第一件事确认是不是真泄漏。看Metaspace的used曲线是否只涨不落。如果涨到某个水位就稳定可能只是业务高峰期类加载多而不是泄漏如果持续爬坡且每次Full GC都兜不住就是泄漏。第二件事看FGC频率和回收效果。执行jstat -gc pid观察FGC和FGCT的变化。Metaspace接近-XX:MaxMetaspaceSize时JVM会频繁Full GC这时候的GC日志会有大量Metaspace标记需要注意区分。第三件事确认MaxMetaspaceSize有没有设置。如果没设置泄漏进程不会报OutOfMemoryError: Metaspace而是直接耗尽本地内存表现为容器OOMKilled或宿主机负载异常排查难度更大。所以线上必须显式设置Metaspace上限宁可设大一点再监控也不要完全不设。第四件事检查是否存在热部署或动态脚本。询问业务方最近有没有频繁发布、回滚、动态加载脚本或配置。基于经验这类操作是Metaspace泄漏最高发的来源能在排查前锁定重点方向。4.2 三个必看监控指标和它们的预警阈值监控系统里建议对每个Java进程都放上Metaspace相关指标至少包含四个Metaspace Used、Metaspace Committed、Loaded类数量、Unloaded类数量。预警阈值可以参考Metaspace Used持续超过-XX:MaxMetaspaceSize的70%或者连续多个GC周期未见下降就触发P2告警Loaded类数量在小时级别增长超过20%且无稳定趋势就触发检查Unloaded数量长期为0结合Loaded增长就是类加载器泄漏的典型信号。另外要区分GC后used下降不等于回收了Metaspace。类卸载本身是延迟的卸载后的类元数据要等对应的chunk被清理或复用后committed才会下降。所以不能用一次GC后的committed变化来判断是否泄漏至少要看10个GC周期的趋势。4.3 常见问题速查表现象可能原因处理方向Metaspace OOM堆内存正常类加载器泄漏元数据无法回收用jstat看趋势jcmd看类加载器Arthas定位代码committed远大于used且chunk碎片多大量短命类加载器chunk无法合并归还检查创建类加载器的调用链修复持有引用问题FGC频繁Metaspace使用量不降动态代理/脚本引擎/热部署检查CGLIB、Groovy、容器热加载类容器OOMKilledJava堆还有余量Metaspace或NativeMemory超限设置MaxMetaspaceSize开启NMT分析类加载器数量正常Metaspace仍涨单个大类的元数据体积异常jcmd GC.class_stats查哪个类占字节数最大4.4 修复方案与预防建议定位到代码之后修复方向通常有四个层次。第一层是别持有类加载器。把静态List、缓存、ThreadLocal里的ClassLoader引用清掉让GC能回收它。这里是治本的思路也是绝大多数问题的根源。第二层是复用而非重建。如果业务必须反复加载同一批类不要每次new ClassLoader把已经加载好的Class放到缓存里按类名或版本复用。像Groovy这类脚本引擎官方都建议用GroovyClassLoader的缓存机制而不是每次独立创建。第三层是限制和预警。设置合理的-XX:MaxMetaspaceSize打开-XX:TraceClassLoading -XX:TraceClassUnloading配合监控告警在泄漏刚冒头时就发现。脚本引擎和动态代理库升级到能控制类缓存和回收的版本。第四层是隔离和快速恢复。对于短时间内无法完全修复的历史遗留服务可以通过调整-XX:MaxMetaspaceExpandRatio降低扩容幅度或者配置JVM参数在Metaspace接近上限时自动dump现场-XX:HeapDumpOnOutOfMemoryError \ -XX:HeapDumpPath/tmp/metaspace_oom.hprof不过要注意Metaspace OOM的hprof文件主要包含堆对象dump意义有限真正有价值的还是GC日志和类加载日志。4.5 实际排查中容易踩的坑这个部分是我最想说的。排查Metaspace泄漏时几个坑几乎每次都会遇到提前避开会省下大量时间。第一个坑是GC.class_stats在生产上造成的停顿。低版本JDK执行这条命令非常容易触发长STW我见过一次线上执行直接卡了十几秒的情况。所以一定要带到低峰期执行并且确认业务能接受短暂暂停。实在担心可以只用jcmd VM.metaspace和jstat做初步判断把GC.class_stats放到最后确认环节。第二个坑是Arthas attach不上。常见原因是对目标进程没有权限或/tmp目录空间不足。Arthas默认会写临时文件到/tmp空间满了一连串功能都不可用。先清理/tmp或用-C指定临时目录。另外容器环境要确认你是否和Java进程在同一个PID namespace。第三个坑是火焰图只给出CPU热点没给出分配热点。Metaspace泄漏的链路往往伴随着类加载和反射调用CPU火焰图上确实能看到。但如果你的Metaspace增长是由定时任务批量加载造成的CPU热点可能不明显。这时候别死磕profiler回到classloader命令和sc命令上去找增长点。第四个坑是误把正常的Metaspace增长当泄漏。Java的类加载是按需的很多大型系统在运行初期类数量会持续攀升一段时间直到业务路径都走一遍才稳定。判断泄漏至少要观察两三个小时而不是看几分钟就下结论。第五个坑是只修代码不清理残留。线上已经泄漏过的进程哪怕你修复了代码Metaspace碎片和已加载的类数据也不会自动消失。修复后需要通过重启或灰度发布来释放残留内存别指望Metaspace会在下次GC里自动降到正常水位。我在实际排查里还有一个深有体会的点Metaspace泄漏的根因往往不难找难的是把那一堆看似正常的框架代码和业务代码区分开。比如CGLIB代理和反射调用这类机制本身没错但配合每次请求都生成新代理的写法就变成了泄漏。排查时多问一句这段代码的调用频率是什么比单纯盯内存指标更有用。希望这套三重定位法能帮你少走几步弯路。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询