密封类动态生成大量许可子类会同时耗尽metaspace和compressed class space;需通过jvm参数启用监控,并用jcmd、jmx及-verbose:class定位子类爆炸源头。

密封类(sealed classes)在 JDK 15+ 引入,配合 permits 子句定义允许的直接子类。当框架(如 Spring、Lombok 编译插件、或自定义代码生成器)基于密封类动态生成大量许可子类(尤其是匿名类、隐藏类或代理类)时,每个子类都会产生完整的元数据 + 对应的 Klass 结构体,从而同时消耗 Metaspace 和 Compressed Class Space —— 这正是监控的关键双路径。
重点关注两个独立内存池指标
密封类许可子类属于“新加载的类”,其内存开销不是单一分区问题:
- Metaspace:存储类完整元数据(方法表、常量池、注解、签名等),每个多态子类都有一份冗余副本
-
Compressed Class Space:存储每个子类对应的
Klass*结构体(x64 下典型占 2KB+),子类数量越多,此处越先见顶
二者默认不联动扩容,-XX:MaxMetaspaceSize 调大无法缓解 OutOfMemoryError: Compressed class space。
启用精准监控的 JVM 参数组合
启动时加入以下参数,确保两类空间使用率可采集:
-
-XX:+UseCompressedClassPointers(默认开启,但显式声明更稳妥) -
-XX:CompressedClassSpaceSize=512m(设为合理上限,便于观察是否逼近耗尽) -
-XX:MaxMetaspaceSize=512m(限制 Metaspace,避免掩盖真实瓶颈) -
-Xlog:gc*:gc.log:time,uptime,level,tags(JDK9+ 推荐,替代旧版-XX:+PrintGCDetails) -
-XX:+UnlockDiagnosticVMOptions -XX:+PrintNMTStatistics(启用本机内存追踪,验证 Class 区 committed 值)
运行时实时验证方法
不依赖日志,直接查运行态指标:
- 用
jcmd <pid> VM.native_memory summary scale=MB</pid>查 “Class” 行的committed值 → 对应 Compressed Class Space 实际占用 - 用 JMX 连接(如 JConsole 或 Prometheus + jmx_exporter)读取:
java.lang:type=MemoryPool,name=Metaspace→ 看Usage.usedjava.lang:type=MemoryPool,name=Compressed Class Space→ 看Usage.used / Usage.max - 若发现后者使用率 >90% 且持续上涨,而前者仍宽松,基本锁定是密封子类爆炸导致 Klass 数量超限
定位具体哪类密封结构在“产子”
结合类加载行为分析:
- 加
-verbose:class启动,过滤输出中含sealed关键字的父类,再观察其后密集出现的$1、$Hidden、$$EnhancerBySpringCGLIB等命名子类 - 用
jcmd <pid> VM.class_hierarchy</pid>(JDK21+)或jstack -l <pid> | grep -A5 "ClassLoader"</pid>辅助识别高频加载的类加载器实例 - 若使用
Lookup.defineHiddenClass或Unsafe.defineAnonymousClass,检查调用栈是否集中在密封类的反射适配逻辑中











