记一次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使用率太低,也就是创建了很多类加载器,而每个类加载器又加载了很少的类。
类加载器情况
- 通过arthas查看类加载器使用情况,命令:classloader
- 对应用程序执行情况如下,发现DelegatingClassLoader类加载器数量比较多。
DelegatingClassLoader分析
对dump文件进行分析
- 发现dump文件中有较多的DelegatingClassLoader,且持有GeneratedMethodAccessorXXXX,该类是反射用于加载生成的Method类时,使用的加载器。
- 通过MAT工具查看GeneratedMethodAccessorXXXX对象(List objects with incoming refenrece),发现大部分都是业务DTO字段的set方法。
反射原理
JVM对反射调用分两种情况:
- 默认使用native方法(JNI)进行反射操作,生成NativeMethodAccessorImpl类。
- 一定条件下会生成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)控制
结合业务
业务中使用了大量的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
3. 总结
- 项目中使用了比较多的BeanUtil.copy(jodd.bean框架),底层用了反射。但反射调用达到一定次数,会对method生成class和单独的类加载器,对于类加载器的内存分配是一块一块分配。大量的copy以及重复的调用,会导致Metaspace空间持续增长。
- 当前调整-XX:MaxMetaspaceSize为512m可以解决问题,但后续要慎用copy,或者使用cglib方式的bean copy(以类为单位,而不是method)
4. 参考资料
- 又一个beanCopy引发的血案metaspace溢出: blog.csdn.net/a807719447/...
- 一次Metaspace OutOfMemoryError问题排查记录: juejin.cn/post/711451...
- 大量类加载器创建导致诡异FullGC: heapdump.cn/article/192...
- cloud.tencent.com/developer/a...
- juejin.cn/post/712761...
- javakk.com/160.html