java执行反射代码导致频繁创建奇怪的类从而发生元数据区被占满FUllGc案例

查看 JVM GC 日志

没有一丝耽搁,老王立刻调出了线上GC日志,在日志里,看到了一个“Metadata GC Threshold”的字样,类似于如下日志:

【Full GC(Metadata GC Threshold)xxxxx, xxxxx】

通过GC日志,老王立即定位到这次频繁的Full GC,实际上是JDK 1.8以后的Metadata元数据区导致的。

奇怪了,Metadata区域一般是放一些加载到JVM里去的类,为什么会被频繁的塞满,进而触发Full GC呢?

老王陷入了沉思,一旁的小猛和彪子只是静静的看着,大气也不敢出。。。

查看 Metaspace 内存占用情况

对啊,怎么把这茬给忘了”。老王突然的自言自语,把两个小弟吓了一跳。

思考中的老王根本没注意到两个小弟的反应,他迅速通过jstat,观察了一下Metaspace内存区域占用的波动曲线图,发现图像类似下面这样:
java执行反射代码导致频繁创建奇怪的类从而发生元数据区被占满FUllGc案例_第1张图片
看起来Metaspace区域的内存呈现一个波动的状态,总是会先不断增加,达到一个顶点之后,就会把Metaspace区域给占满

占满之后,自然就会触发一次Full GC,Full GC会带着Metaspace区域的垃圾回收,所以接下来Metaspace区域的内存占用又变得很小了。

看着屏幕上的图像,老王心里一丝明悟。很明显系统在运行过程中,不停的有新的类产生被加载到Metaspace区域里,然后不停的把Metaspace区域占满。接着触发一次Full GC,回收掉Metaspace区域中的部分类。

然后这个过程不断循环,进而造成Metaspace区域反复被占满,然后反复导致Full GC的发生。

老王一边思考,一边在纸上画出了一张示意图:
java执行反射代码导致频繁创建奇怪的类从而发生元数据区被占满FUllGc案例_第2张图片

到底是什么类在不停的加载?

那问题又来了,到底是什么类不停的被加载到JVM的Metaspace区域里去?老王又一次陷入了沉思。

不多会,他回过神来,在JVM启动参数中加入如下两个参数了:

-XX:TraceClassLoading 
-XX:TraceClassUnloading

“老大,这两个参数有什么作用啊?”彪子忍不住好奇问道。

“这两个参数,顾名思义,就是追踪类加载和类卸载的情况,他会通过日志打印出来JVM中加载了哪些类,卸载了哪些类”,老王一边紧盯屏幕,一边解释道。

果然,加入这两个参数之后,在Tomcat的catalina.out日志文件中输出了一堆日志,里面显示类似如下的内容:

【Loaded sun.reflect.GeneratedSerializationConstructorAccessor from __JVM_Defined_Class】

明显可以看到,JVM在运行期间不停的加载了大量的名字叫做“GeneratedSerializationConstructorAccessor”类到Metaspace区域里去
看着这个莫名其妙的类,老王在之前的草图上又补充了几笔:
java执行反射代码导致频繁创建奇怪的类从而发生元数据区被占满FUllGc案例_第3张图片
虽然不知道这是什么类,但是老王相信就是它的一直加载,导致 Metaspace 区域不停被占满,进而不停的引发Full GC

直觉告诉他,已经慢慢接近真相了!

为什么会频繁加载奇怪的类?

“彪子,你马上百度一下这个类是什么?”老王指着刚才日志追踪到的这个奇怪的类名说道。

“好的好的”,彪子哪敢怠慢,三下五除二就百度了出来

“老大,这个类是Java执行反射机制时加载出的一个类,JVM会在反射调用一定次数之后动态生成一些类,刚才这个类就是其中之一,这个是JVM的一个底层优化机制”

彪子念完上面一段话,一脸懵逼,自己都不知道自己在说啥。。。

不停执行反射代码后动态生成的类。。。老王陷入沉思,边思考边在纸上又画出了一张图:
java执行反射代码导致频繁创建奇怪的类从而发生元数据区被占满FUllGc案例_第4张图片

JVM 创建的奇怪类的玄机

经过短暂的思考后,老王一丝恍悟。想到了自己在公众号儒猿技术窝的专栏《从零开始带你成为JVM实战高手》里学过的如下知识:

JVM在反射过程中动态生成的类的Class对象,他们都是SoftReference软引用的,而软引用正常情况下不会回收,但是如果内存比较紧张的时候就会回收这些对象。

那SoftReference对象到底在GC的时候要不要回收,是怎么判断的呢?老王大脑继续飞速运转

对了,就是这个!老王一拍大腿,在纸上迅速写下了一个等式。

clock - timestamp <= freespace * SoftRefLRUPolicyMSPerMB

看着老大写出的这个公式,两个小弟的反应完全不同。小猛一脸懵逼,而彪子则一脸淡然。

彪子看到好哥们小猛的反应,主动给他解释道:“clock - timestamp”代表了一个软引用对象有多久没被访问过了。freespace代表JVM中的空闲内存空间,SoftRefLRUPolicyMSPerMB代表每一MB空闲内存空间可以允许SoftReference对象存活多久。”

“举个例子,假如现在JVM创建了一大堆奇怪的类出来,这些类本身的Class对象都是被SoftReference软引用的。”

“然后现在JVM里的内存空间有3000MB,SoftRefLRUPolicyMSPerMB的默认值是1000毫秒,那么就意味着,此时那些奇怪的SoftReference软引用的Class对象,可以存活3000 * 1000 = 3000秒,就是50分钟左右。”

“是的,小彪说的没错”,老王一边思考,一边评价着彪子刚才的一番解答

得到了老大的认可,彪子心里一阵小得意,而小猛则是一脸不可思议,心想:“这货跟我合租一套房,白天一块上班,晚上一块开黑,这些东西哪里看的”

其实他不知道的是,彪子游戏玩的很渣,每局都早早的挂掉了,所以在等待下一局的时候,就会无聊逛逛技术博客啥的

刚才说的这些,就是在他等待玩下一把游戏之前的空隙,无意中get到的

但是,正是因为彪子get到的这个他引以为傲的“黑科技”,导致了今天这次线上事故。

怎么回事儿呢?我们接着看!

话说老王在肯定了彪子对自己写下的这个等式的解释后,又继续说道:

“当然小彪上面那个例子,仅仅是举例而已。一般来说,发生GC时,其实JVM内部或多或少总有一些空间内存的,所以基本上如果不是快要发生OOM内存溢出了,一般软引用也不会被回收。”

“所以按理说,JVM应该会随着反射代码的执行,动态的创建一些奇怪的类,他们的Class对象都是软引用的,正常情况下不会被回收,也不应该快速增长。”

问题解决,水落石出

分析到这里,老王感觉自己离解决方案就差一层窗户纸,一捅就破。

眼睛紧紧盯着纸上的公式,他忽然想到了什么,打开了线上的JVM参数设置,找到刚才公式里的参数:SoftRefLRUPolicyMSPerMB

“果然是它”,老王自言自语道,随后他快速敲击着键盘,重新设置了JVM参数,重启之后,系统稳定运行了

“神了。。。老大,你怎么解决的?”小猛和彪子一脸的崇拜和不可思议。

老王并没有理会两个小弟的问题,反问道:“这个SoftRefLRUPolicyMSPerMB参数,是谁设置为0的?”

“额。。。是我设置的!”彪子额头上顿时冷汗岑岑。

不过他马上补充道:“是我没事的时候在网上逛技术博客,偶然看到了关于这个JVM参数的讲解,以及软引用什么时候被JVM垃圾回收的判断依据”。

说到这里,彪子指了指老王写在纸上的公式,“就是老大你写的这个等式”。

“然后我想着把SoftRefLRUPolicyMSPerMB这个参数设置为0,任何软引用对象就可以尽快释放掉,不用留存,尽量给内存释放空间出来,这样就可以提高内存利用效率了!”

老王听完一阵苦笑,缓缓的吐出5个字:“很傻很天真。。。”
稍微顿了顿,他给彪子解释了起来:“实际上一旦这个参数设置为0之后,直接导致clock-timestamp<=freespace*SoftRefLRUPolicyMSPerMB这个公式的右半边是0,就导致所有的软引用对象,比如JVM生成的那些奇怪的Class对象,刚创建出来就可能被一次Young GC给带着立马回收掉一些。”

边说老王还边给彪子画了一张图:
java执行反射代码导致频繁创建奇怪的类从而发生元数据区被占满FUllGc案例_第5张图片
“举个例子,比如JVM好不容易给你弄出来100个这种奇怪的类,结果因为你瞎设置软引用的参数,导致突然一次GC就给你回收掉几十个类”

“接着JVM在反射代码执行的过程中,就会继续创建这种奇怪的类,在JVM的机制之下,会导致这种奇怪类越来越多。”

“也许下一次gc又会回收掉一些奇怪的类,但是马上JVM还会继续生成这种类,最终就会导致Metaspace区域被放满了,一旦Metaspace区域被占满了,就会触发Full GC,然后回收掉很多类,接着再次重复上述循环”

老王一边说,一边又画出了一张图:

java执行反射代码导致频繁创建奇怪的类从而发生元数据区被占满FUllGc案例_第6张图片
老王紧接着继续说道:“其实解决方案很简单,在有大量反射代码的场景下,只要把-XX:SoftRefLRUPolicyMSPerMB这个参数设置大一些即可,千万别设置为0,可以设置个1000,2000,3000,或者5000毫秒,都可以。”

“提高这个数值,就是让反射过程中JVM自动创建的软引用的一些类的Class对象不要被随便回收,你看我优化这个参数之后,系统是不是就稳定运行了,然后Metaspace区域的内存占用也是稳定的,不会来回大幅度波动了”

两个小弟听得入了迷。。。

还是小猛先回过神来:“老大,虽然最后解决方案看起来很简单,但是我觉得整个过程中的问题定位和分析才是重点,可以教教我吗?”小猛一脸的求知若渴

“是的是的”,彪子赶紧附和着说道。

老王似乎并没有打算回答两个小弟的问题,相反他脸上显现出一丝小小的失望。

叹了口气,他反问小猛和彪子:“这个月1号,我给你们布置过什么任务?”

看了看日子,1号是周五,小猛和彪子面面相觑。唯一记得的,就是那天他俩下班后,一起吃了个饭,然后就相约回出租屋里,开黑玩王者,一直到深夜!

见两个小弟陷入沉思,老王打破沉默:“我是不是给你们推荐了一个专栏《从零开始带你成为JVM实战高手》,并且交待你们用几周的时间,好好消化里面的内容!

顿了顿,老王又继续说道:“今天能这么迅速的解决这个线上事故,不是我多神,而是因为这个案例正是专栏里讲的一个案例。

我把整个专栏反复阅读了3遍,甚至可以告诉你们,这个案例在专栏的第59讲”

小猛和彪子闻言大惊,纷纷掏出手机,退出之前还在的王者荣耀界面,进入公众号儒猿技术窝的知识店铺,翻到第59讲,标题赫然在目:

java执行反射代码导致频繁创建奇怪的类从而发生元数据区被占满FUllGc案例_第7张图片
老王继续说道:“不论是平时工作,还是跳槽面试,JVM一直是咱们Java程序员的一大痛点。以前我自己也看过不少JVM书籍,理论知识掌握了不少,但是对于线上实战和性能优化,总感觉心里很虚”

而《从零开始带你成为JVM实战高手》专栏,可以说填补了国内 JVM 实战这块技术资料的空白,它深入讲解了下面三块实战内容:

1.线上系统的JVM参数优化实战:基于百万交易量的支付系统、上亿用户量的电商系统实战

2.线上系统的JVM GC调优实战:基于每秒10万并发的BI系统、日百亿数据量的数据处理系统进行实战

3.线上系统的OOM内存溢出优化实战:基于百万连接网关系统、10万并发量的秒杀系统,进行实战

他在专栏里详细剖析了自己亲身经历的34个真实线上JVM生产案例,非常适用于我们这种中小型的创业公司

除了实战,专栏也有非常详细的对JVM原理性知识的讲解,而且写作方式很接地气,一步一图,循序渐进、通俗易懂

你们看看这个专栏大纲,也能体会到作者设计大纲的用心良苦了

你可能感兴趣的:(java虚拟机,业务案例)