不可信——jcmd VM.metaspace仅显示已加载类的元数据用量,不包含未回收残留块及mmap分配但未归还的Native内存,易将碎片化误判为泄漏,须结合-XX:NativeMemoryTracking=detail和pmap/smaps等OS级工具综合分析。

直接看 jcmd VM.metaspace 输出是否可信
不可信——它只反映当前已加载类的元数据内存用量,不包含已卸载但尚未被 Metaspace 回收的“残留块”,更不统计 Native 层通过 mmap 分配但未归还给 Metaspace 的内存。你看到 used 是 256MB、committed 是 384MB,不代表真实堆外增长就止步于此。
常见误判场景:MetaspaceSize 设得太小(比如默认 21807104 字节 ≈ 20.8MB),触发频繁扩容 + 碎片化,jcmd 显示 committed 持续上涨,但 used 基本不动——这其实是 Metaspace 内部管理开销,不是泄漏,但容易被当成问题源头。
jcmd VM.metaspace 要结合 -XX:NativeMemoryTracking=detail 一起用
单独跑 jcmd pid VM.metaspace 只能看到类元数据层的汇总,看不出它底层是用 mmap 还是 brk 分配的,也看不到 ClassLoader 是否持有引用阻止卸载。
- 必须加 JVM 参数:
-XX:NativeMemoryTracking=detail,重启应用 - 再执行:
jcmd pid VM.native_memory summary scale=MB,重点看Metaspace和Internal两栏 - 如果
Metaspace栏增长缓慢,但Internal栏持续上涨(尤其伴随大量64MB匿名映射),基本可排除纯 Java 类加载泄漏,指向 Native 层(如 JNI、Netty 的Unsafe、JDBC 驱动)
查 Metaspace 泄漏,真正要盯的是 ClassLoader 生命周期
Metaspace 本身不会泄漏,泄漏的是“无法被 GC 的 ClassLoader”——只要 ClassLoader 活着,它加载的所有类元数据就一直占着 Metaspace 不放。
典型诱因:
- Web 应用热部署时,旧
WebAppClassLoader被新实例替换,但线程池/定时任务/静态缓存里还持有着旧 loader 的引用 - 使用了
ThreadLocal<class></class>或缓存了Class.forName("xxx")返回的Class对象,且没清理 - 第三方 SDK(如某些老版本 Druid、Logback)在 shutdown 时未释放其内部 ClassLoader
验证方法:用 jcmd pid VM.class_hierarchy -all 查看活跃 ClassLoader 数量;或 dump 后用 MAT 检查 java.lang.ClassLoader 实例数是否随请求量线性增长。
pmap -x <pid></pid> 和 /proc/<pid>/smaps</pid> 是绕过 JVM 层的底线手段
当 jcmd VM.metaspace 和 native_memory 都显示正常,但物理内存持续上涨,就得跳过 JVM 视角,直查 OS 内存映射。
关键信号:
-
pmap -x <pid> | grep "64M\|128M"</pid>出现大量相同大小的匿名映射(anon),且地址段不在jcmd VM.native_memory报告范围内 -
grep -i "mmap" /proc/<pid>/smaps | awk '{sum += $2} END {print sum/1024/1024 " GB"}'</pid>结果远超jcmd中Internal + Metaspace总和 - 这类内存通常来自 Netty 的
PooledByteBufAllocator、JDBC 驱动的 native buffer、或自定义 JNI 库——它们完全游离于 Metaspace 管理之外
Metaspace 只管类定义,不管字节码加载器背后的 native buffer、direct memory、甚至 mmap 文件映射。真要定位到根因,得从这些 64MB 块的 MemTotal 所在的 mmapped file 或 heap 标签反推调用栈,而不是盯着 VM.metaspace 输出反复刷新。










