记一次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. 参考资料

相关推荐
我是福福大王4 分钟前
MyBatis源码学习总结
后端·mybatis
玄明Hanko13 分钟前
生产环境到底能用Docker部署MySQL吗?
后端·mysql·docker
sayornottt15 分钟前
Rust中的动态分发
后端·rust
创码小奇客17 分钟前
MongoDB 时间序列:解锁数据时光机的终极指南
java·mongodb·trae
黯_森17 分钟前
Java面向对象
java·后端
小厂永远得不到的男人17 分钟前
WebSocket深度剖析:实时通信的终极解决方案实践指南
后端·websocket
代码小侦探19 分钟前
Java中以Maven方式引入Oracle JDBC Driver依赖的详解
java·oracle·maven
不畏惧的少年19 分钟前
AQS的底层实现原理
java
289792400320 分钟前
理解volatile
后端·面试
斜月21 分钟前
Springboot wechatpay-java 微信支付实践
spring boot·后端