简述
生产环境一个项目出现了频繁重启,运维兄弟反馈过来是项目OOM了。通过Grafana上观察,随着时间推移,Metaspace趋势上升状态。这就奇怪了,一般情况下Metaspace不会一直增长。
原因分析
一般Metaspace增长原因是类加载导致的,比方说反射类加载,动态代理等。加载的类越多Metaspace占用的内存也就越大,所以Metaspace OOM可以从类加载问题进行分析。
添加-XX:+TraceClassLoading -XX:+TraceClassUnloading
VM配置后,观察日志中输出类加载以及卸载的信息,发现ma.glasnost.orika.generated.
包下的类不停加载,所以分析为是orika框架错误使用导致的。
代码分析
public class BeanUtils {
public static <S, D> D copyProperties(S source, Class<D> target) {
MapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();
return mapperFactory.getMapperFacade().map(source, target);
}
public static <S, D> List<D> copyProperties(List<S> source, Class<D> target) {
MapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();
return mapperFactory.getMapperFacade().mapAsList(source, target);
}
}
很简单的一段代码,作用就是对象的copy,使用了orika框架。查看orika官网指导的例子orika使用例子也没发现什么问题。
添加jvm配置-XX:+TraceClassLoading -XX:+TraceClassUnloading -XX:MetaspaceSize=20m -XX:MaxMetaspaceSize=20m
,循环调用copyProperties
方法,结果如下:
jvisualvm观察到的结果:
很明显得出结论,orika框架错误使用,导致UserAccountDTO类不停加载,最终导致了Metaspace OOM。
查看orika源码,发现使用了Javassist
对字节码进行增强。orika原理是类映射会生成字节码文件,字节码文件是存到Metaspace中的。因为频繁new DefaultMapperFactory()
(每次new都会使用新类名),导致每次new都会生成字节码文件(即使是同两个类进行转换)。所以我们需要对代码进行修复,把DefaultMapperFactory
声明为静态成员变量,每次进行转换的时候都是使用同一个DefaultMapperFactory
对象(里面会缓存映射类之间的关系),就不会导致Metaspace OOM了。
代码修复
public class BeanUtils {
private static final MapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();
public static <S, D> D copyProperties(S source, Class<D> target) {
return mapperFactory.getMapperFacade().map(source, target);
}
public static <S, D> List<D> copyProperties(List<S> source, Class<D> target) {
return mapperFactory.getMapperFacade().mapAsList(source, target);
}
}
总结
本次出现的OOM问题是orika的错误使用导致Metaspace OOM,orika官网也没有细节告诉我们每次调用都new一下回导致OOM。所以我们在开发过程中使用别人的框架,除了要看框架文档使用,还要注意下自己使用过程中可能出现的问题。
评论区