JVM Metaspace OOM 问题分析与解决
环境:OpenJDK 1.8
通过 Arthas 的memory命令查看内存使用情况,发现 Metaspace 的使用率达到 90.98%。Metaspace 主要用于存储类的元数据(如类定义、方法字节码等),使用率过高极易引发OutOfMemoryError: Metaspace错误。
进一步使用classloader命令查看类加载情况,发现共有 3397 个类是由sun.reflect.DelegatingClassLoader加载的:
1 | sun.reflect.DelegatingClassLoader 3397 3397 |
通过sc命令搜索已加载的类,查找常见代理类命名模式:
1 | sc *$$* |
发现存在海量动态生成的类,主要分成两部分:
- Spring CGLIB 代理,例如:
.service.business.OrderService$$EnhancerBySpringCGLIB$$7d98eea.goods.admin.AdminGoodsController$$EnhancerBySpringCGLIB$$4ac43550
- JDK Lambda 表达式:大量以 $$Lambda$ 结尾的类。例如:
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration$RabbitConnectionFactoryCreator$$Lambda$627/1613514326java.util.stream.Collectors$$Lambda$1207/1482677722
为什么会有这么多代理类?
Spring 使用 CGLIB 为 Bean 创建代理的主要原因是为了支持 AOP(面向切面编程)。当 Bean 被 AOP 切面(如@Transactional、@Async、@Cacheable或自定义@Aspect)增强时,Spring 默认会为其创建一个 CGLIB 代理子类。
常见的情况是 AOP 切面定义过于宽泛,比如切点表达式如* com..service.*.*(..))匹配了大量 Service 和 Controller,导致几乎所有相关类都被代理。频繁使用声明式事务(@Transactional)也会显著增加代理类的数量。虽然这是正常机制,但结合 Lambda 类的问题,会进一步加剧 Metaspace 的压力。
Lambda 表达式在 JVM 中会被编译成特殊的私有静态方法,并生成对应的$$Lambda$...类。在正常情况下,这些类会被缓存和复用,数量有限。但如果使用不当,如在循环中频繁生成 Lambda,就容易导致类数量激增。
常见的诱因包括:
- 在循环体内使用 Stream API 的 Lambda 表达式(如
.map()、.filter()),每次迭代都可能生成新的 Lambda 类; - 在频繁调用的方法中重复使用方法引用或 Lambda 表达式;
- 动态生成函数式接口实例(如 Function、Supplier、Consumer),而未将其提取为静态常量。
解决方案:
- 调整 Metaspace 大小(临时缓解)
通过设置 JVM 参数限制 Metaspace 的上限,避免系统因内存耗尽而崩溃:
1 | # 设置上限 |
- 识别并优化高频Lambda代码
尽量避免在循环或高频调用中动态生成 Lambda,可将其提取为静态常量:
1 | // 反例:每次循环都可能生成新的 Lambda 类 |
- 优化Spring AOP配置(减少代理数量)
检查 AOP 切面定义,避免过于宽泛的切点表达式。如果使用的是 Spring Boot,可考虑如下配置:
1 | # 强制使用 JDK 动态代理(仅对接口有效),减少 CGLIB 的使用 |
如果确实需使用 CGLIB,可尝试缩小切面范围,或结合@Scope(proxyMode = ...)明确指定代理方式。

