共计 2158 个字符,预计需要花费 6 分钟才能阅读完成。
一. 景象
前段时间公司线上环境的一个 Java 利用因为 OOM 的异样报警,导致整个服务不可用被拉出集群,本地模仿重现的景象如下:
过后的解决方案是减少 metaspace 的容量:-XX:MaxMetaspaceSize=500m
,从原来默认的 256m 改为 500m,尽管没有再呈现 oom,但这个只是长期解决方案,通过公司的监控零碎察看 metaspace 的应用状况还是在回升,而且前面随着业务访问量越来越大还是有可能达到阈值。
二. 剖析
Metaspace 元空间次要是存储类的元数据信息,咱们的利用里加载的各种类形容信息,比方类名、属性、办法、拜访限度等,依照肯定的构造存储在 Metaspace 里。
由此可知 metaspace 空间增长是因为反射类加载,动静代理生成的类加载等导致的,也就是说 Metaspace 的大小和加载类的数据有关系,加载的类越多 metaspace 占用的内存也就越大。
因为理解过后的业务场景是因为有个邮件服务拜访订单详情接口的访问量忽然回升,以及查看 log 的 eroor 日志发现大部分都是订单详情接口先报出的这个问题:java.lang.OutOfMemoryError: Metaspace
这里我在测试环境 Java 利用的 jvm 里减少 -XX:+TraceClassLoading
-XX:+TraceClassUnloading
记录下类的加载和卸载状况,而后通过 jmeter 多个线程调用订单详情接口模仿 metaspace 溢出的景象,发现在 catalina.out 文件里输入的除了业务上用到的类外还有大量的反射类,如下:
这些反射类被频繁的加载和卸载是不失常的,通过 Arthas 诊断工具(Java 在线诊断利器之 Arthas)察看调用链发现每次调用接口都是通过反射的形式实现的。
目前咱们的我的项目都是基于 SOA 框架对外提供拜访的,从上图 sun.reflect
的调用者也能看进去
通过上图能够看出在调用底层接口时都是通过反射的形式获取类的实例,查看框架底层代码实现能够确认
同样对底层接口返回的 json 数据反序列化时也会用到反射
持续跟代码能够看到这些反射的实现都会用到 java.lang.Class
里的 ReflectionData
对象 ReflectionData
是个外部动态类被缓存起来,外面的属性就是咱们做反射操作时须要用的属性 Field
,办法Method
和构造函数等。然而有个问题 reflectionData
是被 SoftReference 软援用润饰的,如下图
如果是软援用的话在内存空间有余时就可能会被回收掉,如果回收掉那下次再应用的话只能从新通过反射获取。
而 SoftReference 是否被回收又跟 SoftRefLRUPolicyMSPerMB
参数的值有关系,查看咱们线上 JVM 的配置发现 XX:SoftRefLRUPolicyMSPerMB
这个参数设置的是 0SoftRefLRUPolicyMSPerMB
这个参数大略意思是每 1M 闲暇空间可放弃的 SoftReference 对象的生存时长(单位是 ms 毫秒),LRU 是 Least Recently Used 的缩写,最近起码应用的。
这个值 jvm 默认是 1000ms,如果被设置为 0,就会导致软援用对象马上被回收掉,进而会导致从新频繁的生成新的类,而无奈达到复用的成果。
上图里大量的 sun.reflect.GeneratedSerializationConstructorAccessor,GeneratedMethodAccessor
就是这样产生的。
我把这个参数改回默认值 -XX:SoftRefLRUPolicyMSPerMB=1000
(1 秒),公布到生产环境验证了下,公布后就降下来了,到明天为止基本上趋于稳定
调整后基本上没有再呈现稳定
三. 总结
- 目前次要是通过批改 JVM 的
-XX:SoftRefLRUPolicyMSPerMB
值来解决 metaspace 回升问题,后续会继续察看变动,适当调整参数。至于这个参数之前为什么会被设置成 0, 还须要找 ops 确认下。 - 咱们的利用须要大量 RPC 交互,属于 I / O 密集型业务,应用 SOA,Dubbo 都会遇到相似的问题,通过下面的源码剖析能够看出这个是无奈防止的 (除非是换一种序列化协定,比方
hessian
,不走办法反射的形式来赋值) 包含自身应用的 Spring 框架很多中央也是通过反射实现的比方 AOP,还有咱们埋点常常应用的JsonUtils
工具,通过 dump 文件也能看进去存在大量的属性拷贝和反射操作。
所以咱们在平时的业务代码开发中如果遇到两个对象赋值的操作尽量少用反射的形式实现,比方上面的代码:
这里做的对象拷贝操作应用的是 apache common-beanutils.jar 中的BeanUtils
,这个类底层采纳 javabeans+ 反射实现,性能比拟差,内存开销比拟大,当零碎高并发的状况容易导致 Metaspace 空间增长过快,不倡议这样应用。
如果字段少的话间接赋值就行了,多的话能够应用 Cglib 的 BeanCopier
类,BeanCopier
类底层是采纳 asm 字节码操作形式来进行对象拷贝操作,性能损耗和内存开销都比拟小。
或者应用 MapStruct 这种帮你生成 set
、get
办法的工具,成果会更好。
文章起源:http://javakk.com/160.html