Arthas memory outputArthas memory output

环境:OpenJDK 1.8

通过 Arthas 的memory命令查看内存使用情况,发现 Metaspace 的使用率达到 90.98%。Metaspace 主要用于存储类的元数据(如类定义、方法字节码等),使用率过高极易引发OutOfMemoryError: Metaspace错误。

进一步使用classloader命令查看类加载情况,发现共有 3397 个类是由sun.reflect.DelegatingClassLoader加载的:

Arthas classloader outputArthas classloader output

1
sun.reflect.DelegatingClassLoader    3397    3397

通过sc命令搜索已加载的类,查找常见代理类命名模式:

1
2
3
4
sc *$$*
sc *ByCGLIB*
sc *BySpringCGLIB*
sc *$Proxy*

发现存在海量动态生成的类,主要分成两部分:

  1. Spring CGLIB 代理,例如:
  • .service.business.OrderService$$EnhancerBySpringCGLIB$$7d98eea
  • .goods.admin.AdminGoodsController$$EnhancerBySpringCGLIB$$4ac43550
  1. JDK Lambda 表达式:大量以 $$Lambda$ 结尾的类。例如:
  • org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration$RabbitConnectionFactoryCreator$$Lambda$627/1613514326
  • java.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),而未将其提取为静态常量。

解决方案:

  1. 调整 Metaspace 大小(临时缓解)

通过设置 JVM 参数限制 Metaspace 的上限,避免系统因内存耗尽而崩溃:

1
2
3
4
# 设置上限
-XX:MaxMetaspaceSize=512M
# 初始大小
-XX:MetaspaceSize=256M
  1. 识别并优化高频Lambda代码

尽量避免在循环或高频调用中动态生成 Lambda,可将其提取为静态常量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 反例:每次循环都可能生成新的 Lambda 类
for (Order order : orders) {
List<String> names = order.getItems().stream()
.map(item -> item.getName()) // 在循环内生成 Lambda
.collect(Collectors.toList());
}

// 正例:提取为静态方法引用,避免重复生成
private static final Function<Item, String> ITEM_NAME_EXTRACTOR = Item::getName;

for (Order order : orders) {
List<String> names = order.getItems().stream()
.map(ITEM_NAME_EXTRACTOR) // 复用静态实例
.collect(Collectors.toList());
}
  1. 优化Spring AOP配置(减少代理数量)

检查 AOP 切面定义,避免过于宽泛的切点表达式。如果使用的是 Spring Boot,可考虑如下配置:

1
2
# 强制使用 JDK 动态代理(仅对接口有效),减少 CGLIB 的使用
spring.aop.proxy-target-class=false

如果确实需使用 CGLIB,可尝试缩小切面范围,或结合@Scope(proxyMode = ...)明确指定代理方式。