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

相关推荐
IT古董1 小时前
第四章:大模型(LLM)】06.langchain原理-(3)LangChain Prompt 用法
java·人工智能·python
东阳马生架构3 小时前
生成订单链路中的技术问题说明文档
后端
轻抚酸~4 小时前
小迪23年-32~40——java简单回顾
java·web安全
程序员码歌6 小时前
【零代码AI编程实战】AI灯塔导航-总结篇
android·前端·后端
Sirius Wu6 小时前
Maven环境如何正确配置
java·maven
java坤坤6 小时前
GoLand 项目从 0 到 1:第八天 ——GORM 命名策略陷阱与 Go 项目启动慢问题攻坚
开发语言·后端·golang
元清加油6 小时前
【Golang】:函数和包
服务器·开发语言·网络·后端·网络协议·golang
健康平安的活着7 小时前
java之 junit4单元测试Mockito的使用
java·开发语言·单元测试
bobz9657 小时前
GPT-4.1 对比 GPT-4o
后端
Java小白程序员7 小时前
Spring Framework :IoC 容器的原理与实践
java·后端·spring