记一次Bean Copy导致Metaspace OOM

记一次Metaspace OOM问题分析

1. 问题背景

线上环境和测试环境均发现过应用卡死,频繁Full GC,原因是因为Metaspace空间(JVM参数为-XX:MaxMetaspaceSize为256m)不足导致OOM,一段时间后服务自动重启,重启后服务正常。因为有配置OOM dump文件生成记录,所以获取到了当时OOM dump文件。

2. 问题分析

2.1 OOM dump文件分析

从MAT Leak Suspects分析来看:

  • 不存在占用内存大的对象。
  • 整体看下来无明显异常情况。

2.2 结合资料分析

通过网上收集资料,分析得到以下结论:

Metaspace空间使用

  • Metaspace 空间通过 mmap 来从操作系统申请内存,申请的内存会分成一个一个 Metachunk,以 Metachunk 为单位将内存分配给类加载器,每个 Metachunk 对应唯一一个类加载器,一个类加载器可以有多个 Metachunk。
  • 通过监控可以发现,Metaspace空间使用率为:87%。可能的原因是给类加载器分配的chunk使用率太低,也就是创建了很多类加载器,而每个类加载器又加载了很少的类。

参考地址:heapdump.cn/article/192...

类加载器情况

  • 通过arthas查看类加载器使用情况,命令:classloader
  • 对应用程序执行情况如下,发现DelegatingClassLoader类加载器数量比较多。

参考连接:cloud.tencent.com/developer/a...

DelegatingClassLoader分析

对dump文件进行分析

  • 发现dump文件中有较多的DelegatingClassLoader,且持有GeneratedMethodAccessorXXXX,该类是反射用于加载生成的Method类时,使用的加载器。
  • 通过MAT工具查看GeneratedMethodAccessorXXXX对象(List objects with incoming refenrece),发现大部分都是业务DTO字段的set方法。

参考链接:juejin.cn/post/711451...

反射原理

JVM对反射调用分两种情况:

  1. 默认使用native方法(JNI)进行反射操作,生成NativeMethodAccessorImpl类。
  2. 一定条件下会生成GeneratedMethodAccessor类,它是一个反射调用方法的包装类,代理不同的方法,类后缀序号递增。

JVM对于反射调用会首先使用JNI,当调用次数达到sun.reflect.inflationThreshold时(默认为15),会为调用的method生成一个class。而通过Java字节码生成的类,需要用于它自己的Java类和类加载器(sun/reflect/GeneratedMethodAccessorXXXX类和sun/reflect/DelegatingClassLoader)

当JVM从JNI获取改为生成class的行为,称为Inflation(膨胀)。Inflation机制提高了反射的性能,但对于使用反射项目存在隐患:动态加载的字节码导致Metaspace持续增长。可以通过参数-Dsun.reflect.inflationThreshold=N或者-Dsun.reflect.noInflation=true(默认false)控制

参考链接:juejin.cn/post/711451...blog.csdn.net/a807719447/...

结合业务

业务中使用了大量的BeanUtil.copy(jodd.bean框架),在service返回给controller的对象中。底层的copy使用了反射机制。

通过一下程序简单验证:

ini 复制代码
 public static void main(String[] args) throws InterruptedException {
        PartySimpleDTO partySimpleDTO = new PartySimpleDTO();
        partySimpleDTO.setEmail("12312");
        int i = 1;
        String name = ManagementFactory.getRuntimeMXBean().getName();
        int index = name.indexOf('@');
        String pid = name;
        if (index > 0) {
            pid = name.substring(0,index);
        }
        System.out.println(pid);

        while (true){
            TimeUnit.SECONDS.sleep(3);
            PartySimpleDTO newObj = BeanUtil.copy(partySimpleDTO, PartySimpleDTO.class);
            System.out.println(i);
            i++;
        }
    }
  • 当执行次数小于15时,才3个类加载器。
  • 当执行次数大于15时,数量膨胀到了50

参考资料:blog.csdn.net/a807719447/...

3. 总结

  • 项目中使用了比较多的BeanUtil.copy(jodd.bean框架),底层用了反射。但反射调用达到一定次数,会对method生成class和单独的类加载器,对于类加载器的内存分配是一块一块分配。大量的copy以及重复的调用,会导致Metaspace空间持续增长。
  • 当前调整-XX:MaxMetaspaceSize为512m可以解决问题,但后续要慎用copy,或者使用cglib方式的bean copy(以类为单位,而不是method)

4. 参考资料

相关推荐
MY_TEUCK1 小时前
【Java 后端】SpringBoot 登录认证与会话跟踪实战(JWT + Filter/Interceptor)
java·开发语言·spring boot
今天长肉了吗1 小时前
银行风控项目踩坑实录:指标跑了6小时,风险评分全挂了
java
随读手机1 小时前
多式联运信息交互平台完整方案(2026版)
java·ai·eclipse·云计算·区块链
许彰午2 小时前
03-二叉树——从递归遍历到非递归实现
java·算法
nj01282 小时前
Spring 循环依赖详解:三级缓存、早期引用、AOP 代理与懒加载
java·spring·缓存
野生技术架构师2 小时前
2026年最全Java面试题及答案汇总(建议收藏,面试前看这篇就够了)
java·开发语言·面试
程序员飞哥2 小时前
重构 AI 思维(一):Prompt Engineering,如何下达不可违抗的指令?
人工智能·后端
一只叫煤球的猫3 小时前
ThreadForge 源码解读一:ThreadScope 如何把并发任务放进清晰边界?
java·面试·开源
皮皮林5513 小时前
@Autowired 和 @Resource 注解有啥区别?你这项目怎么还混着用呢?
后端
洛_尘4 小时前
Python 5:使用库
java·前端·python