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

相关推荐
盖世英雄酱581367 分钟前
java 深度调试【第一章:堆栈分析】
java·后端
lastHertz24 分钟前
Golang 项目中使用 Swagger
开发语言·后端·golang
渣哥25 分钟前
面试高频:Spring 事务传播行为的核心价值是什么?
javascript·后端·面试
调试人生的显微镜30 分钟前
iOS 代上架实战指南,从账号管理到使用 开心上架 上传IPA的完整流程
后端
本就一无所有 何惧重新开始34 分钟前
Redis技术应用
java·数据库·spring boot·redis·后端·缓存
低音钢琴1 小时前
【SpringBoot从初学者到专家的成长11】Spring Boot中的application.properties与application.yml详解
java·spring boot·后端
越千年1 小时前
用Go实现类似WinGet风格彩色进度条
后端
蓝色汪洋1 小时前
string字符集
java·开发语言
卿言卿语1 小时前
CC1-二叉树的最小深度
java·数据结构·算法·leetcode·职场和发展
淳朴小学生1 小时前
静态代理和动态代理
后端