JVM Metaspace内存溢出排查与总结

一. 现象

前段时间公司线上环境的一个Java应用由于OOM的异常报警,致使整个服务不可用被拉出集群,本地模拟重现的现象以下:html

当时的解决方案是增长metaspace的容量:-XX:MaxMetaspaceSize=500m,从原来默认的256m改成500m,虽然没有再出现oom,但这个只是临时解决方案,经过公司的监控系统观察metaspace的使用状况仍是在上升,并且后面随着业务访问量愈来愈大仍是有可能达到阈值。java

二. 分析

Metaspace元空间主要是存储类的元数据信息,咱们的应用里加载的各类类描述信息,好比类名、属性、方法、访问限制等,按照必定的结构存储在Metaspace里。apache

由此可知metaspace空间增加是因为反射类加载,动态代理生成的类加载等致使的,也就是说Metaspace的大小和加载类的数据有关系,加载的类越多metaspace占用的内存也就越大。json

由于了解当时的业务场景是由于有个邮件服务访问订单详情接口的访问量忽然上升,以及查看log的eroor日志发现大部分都是订单详情接口先报出的这个问题:java.lang.OutOfMemoryError: Metaspace缓存

这里我在测试环境Java应用的jvm里增长-XX:+TraceClassLoading -XX:+TraceClassUnloading记录下类的加载和卸载状况,而后经过jmeter多个线程调用订单详情接口模拟metaspace溢出的现象,发如今catalina.out文件里输出的除了业务上用到的类外还有大量的反射类,以下:并发

这些反射类被频繁的加载和卸载是不正常的,经过Arthas诊断工具(Java在线诊断利器之Arthas)观察调用链发现每次调用接口都是经过反射的方式实现的。框架

目前咱们的项目都是基于SOA框架对外提供访问的,从上图sun.reflect的调用者也能看出来jvm

经过上图能够看出在调用底层接口时都是经过反射的方式获取类的实例,查看框架底层代码实现能够确认函数

一样对底层接口返回的json数据反序列化时也会用到反射高并发

继续跟代码能够看到这些反射的实现都会用到java.lang.Class里的ReflectionData对象

ReflectionData是个内部静态类被缓存起来,里面的属性就是咱们作反射操做时须要用的属性Field,方法Method和构造函数等。可是有个问题reflectionData是被SoftReference软引用修饰的,以下图

若是是软引用的话在内存空间不足时就可能会被回收掉,若是回收掉那下次再使用的话只能从新经过反射获取。

而SoftReference是否被回收又跟SoftRefLRUPolicyMSPerMB参数的值有关系,查看咱们线上JVM的配置发现XX:SoftRefLRUPolicyMSPerMB这个参数设置的是0

SoftRefLRUPolicyMSPerMB这个参数大概意思是每1M空闲空间可保持的SoftReference对象的生存时长(单位是ms毫秒),LRU是Least Recently Used的缩写,最近最少使用的。

这个值jvm默认是1000ms,若是被设置为0,就会致使软引用对象立刻被回收掉,进而会致使从新频繁的生成新的类,而没法达到复用的效果。

上图里大量的sun.reflect.GeneratedSerializationConstructorAccessor,GeneratedMethodAccessor就是这样产生的。

我把这个参数改回默认值-XX:SoftRefLRUPolicyMSPerMB=1000 (1秒),发布到生产环境验证了下,发布后就降下来了,到今天为止基本上趋于稳定

调整后基本上没有再出现波动

三. 总结

  1. 目前主要是经过修改JVM的-XX:SoftRefLRUPolicyMSPerMB值来解决metaspace上升问题,后续会持续观察变化,适当调整参数。至于这个参数以前为何会被设置成0, 还须要找ops确认下。
  2. 咱们的应用须要大量RPC交互,属于I/O密集型业务,使用SOA,Dubbo都会遇到相似的问题,经过上面的源码分析能够看出这个是没法避免的(除非是换一种序列化协议,好比hessian,不走方法反射的方式来赋值)包括自己使用的Spring框架不少地方也是经过反射实现的好比AOP,还有咱们埋点常用的JsonUtils工具,经过dump文件也能看出来存在大量的属性拷贝和反射操做。

因此咱们在平时的业务代码开发中若是遇到两个对象赋值的操做尽可能少用反射的方式实现,好比下面的代码:

这里作的对象拷贝操做使用的是apache common-beanutils.jar中的BeanUtils,这个类底层采用javabeans+反射实现,性能比较差,内存开销比较大,当系统高并发的状况容易致使Metaspace空间增加过快,不建议这样使用。

若是字段少的话直接赋值就好了,多的话可使用Cglib的BeanCopier类,BeanCopier类底层是采用asm字节码操做方式来进行对象拷贝操做,性能损耗和内存开销都比较小。

或者使用MapStruct这种帮你生成setget方法的工具,效果会更好。

文章来源:javakk.com/160.html

相关文章
相关标签/搜索