性能文章>JVM Metaspace内存溢出排查与总结>

JVM Metaspace内存溢出排查与总结原创

1年前
781618

现象

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

OOM异常报警本地模拟重现 - JVM Metaspace内存溢出排查与总结 - HeapDump性能社区

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

监控系统观察metaspace使用情况在上升 - JVM Metaspace内存溢出排查与总结 - HeapDump性能社区

分析

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

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

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

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

模拟metaspace溢出的现象发现了大量的反射类 - JVM Metaspace内存溢出排查与总结 - HeapDump性能社区

这些反射类被频繁的加载和卸载是不正常的,通过Arthas诊断工具观察调用链发现每次调用接口都是通过反射的方式实现的。

Arthas诊断观察发现调用接口是通过反射的方式实现 - JVM Metaspace内存溢出排查与总结 - HeapDump性能社区

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

调用底层接口都是通过反射的方式获取类的实例 - JVM Metaspace内存溢出排查与总结 - HeapDump性能社区

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

调用底层接口都是通过反射的方式获取类的实例 - JVM Metaspace内存溢出排查与总结 - HeapDump性能社区

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

对底层接口返回的json数据反序列化也会用到反射 - JVM Metaspace内存溢出排查与总结 - HeapDump性能社区

对底层接口返回的json数据反序列化也会用到反射 - JVM Metaspace内存溢出排查与总结 - HeapDump性能社区

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

反射的实现都会用到java.lang.Class ReflectionData对象 - JVM Metaspace内存溢出排查与总结 - HeapDump性能社区

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

reflectionData被SoftReference软引用修饰 - JVM Metaspace内存溢出排查与总结 - HeapDump性能社区

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

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

线上JVM配置XX:SoftRefLRUPolicyMSPerMB参数设置 - JVM Metaspace内存溢出排查与总结 - HeapDump性能社区

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

这个值jvm默认是1000ms,如果被设置为0,就会导致软引用对象马上被回收掉,进而会导致重新频繁的生成新的类,而无法达到复用的效果

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

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

-XX:SoftRefLRUPolicyMSPerMB=1000 (1秒)参数改回默认值 - JVM Metaspace内存溢出排查与总结 - HeapDump性能社区

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

总结

目前主要是通过修改JVM的-XX:SoftRefLRUPolicyMSPerMB值来解决metaspace上升问题,后续会持续观察变化,适当调整参数。至于这个参数之前为什么会被设置成0, 还需要找ops确认下。

我们的应用需要大量RPC交互,属于I/O密集型业务,使用SOA,Dubbo都会遇到类似的问题,通过上面的源码分析可以看出这个是无法避免的(除非是换一种序列化协议,比如hessian,不走方法反射的方式来赋值)

包括本身使用的Spring框架很多地方也是通过反射实现的比如AOP,还有我们埋点经常使用的JsonUtils工具,通过dump文件也能看出来存在大量的属性拷贝和反射操作。

所以我们在平时的业务代码开发中如果遇到两个对象赋值的操作尽量少用反射的方式实现,比如下面的代码:

对象赋值的操作尽量少用反射的方式实现 - JVM Metaspace内存溢出排查与总结 - HeapDump性能社区

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

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

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

本文来自公众号:Java老K,互联网一线java开发老兵,工作10年有余,梦想敲一辈子代码,以梦为码,不负韶华。

 

相关阅读

解决服务器进程退出问题(metaspace溢出)实战

JVM源码分析之Metaspace解密

深入理解堆外内存 Metaspace

由「Metaspace容量不足触发CMS GC」从而引发的思考

请先登录,再评论

这个值设置为0只会快速增长,可以gc调吧?

1年前

为你推荐

关于内存溢出,咱再聊点有意思的?
概述 上篇文章讲了JVM在GC上的一个设计缺陷,揪出一个导致GC慢慢变长的JVM设计缺陷,可能有不少人还是没怎么看明白的,今天准备讲的大家应该都很容易看明白 本文其实很犹豫写不写,因为感觉没有
字符串字面量长度是有限制的
前言 偶然在一次单元测试中写了一个非常长的字符串字面量。 正文 在一次单元测试中,我写了一个很长的字符串字面量,大概10万个字符左右,编译时,编译器给出了异常告警 `java: constant
多次字符串相加一定要用StringBuilder而不用-吗?
今天在写一个读取Java class File并进行分析的Demo时,偶然发现了下面这个场景(基于oracle jdk 1.8.0_144): ``` package test; public c
如何通过反射获得方法的真实参数名(以及扩展研究)
前段时间,在做一个小的工程时,遇到了需要通过反射获得方法真实参数名的场景,在这里我遇到了一些小小的问题,后来在部门老大的指导下,我解决了这个问题。通过解决这个问题,附带着我了解到了很多新的知识,我觉得
谨防JDK8重复类定义造成的内存泄漏
概述 如今JDK8成了主流,大家都紧锣密鼓地进行着升级,享受着JDK8带来的各种便利,然而有时候升级并没有那么顺利?比如说今天要说的这个问题。我们都知道JDK8在内存模型上最大的改变是,放弃了Perm
高吞吐、低延迟 Java 应用的 GC 优化实践
本篇原文作者是 LinkedIn 的 Swapnil Ghike,这篇文章讲述了 LinkedIn 的 Feed 产品的 GC 优化过程,虽然文章写作于 April 8, 2014,但其中的很多内容和
「每日五分钟,玩转 JVM」:久识你名,初居我心
聊聊 JVMJVM,一个熟悉又陌生的名词,从认识Java的第一天起,我们就会听到这个名字,在参加工作的前一两年,面试的时候还会经常被问到JDK,JRE,JVM这三者的区别。JVM可以说和我们是老朋友了
JVM垃圾回收与一次线上内存泄露问题分析和解决过程
本文转载自:花椒技术微信公众号 前言内存泄漏(Memory Leak)是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。Ja