我晕,又来GC血案!这下我算是懂啥是“反射原理”了
yuyutoo 2024-10-12 00:27 11 浏览 0 评论
前言
首先回答一下提问者的问题。这主要是由于存在大量反射而产生的临时类加载器和 ASM 临时生成的类,这些类会被保留在 Metaspace,一旦 Metaspace 即将满的时候,就会触发 FullGc,已达到回收不再被使用的类对象的目的。具体问题请参考接下来的内容,更好的了解反射的实现原理。
概述
公司之前有个大内存系统(70G以上)一直使用CMS GC,不过因为该系统对时间很敏感,偶尔会因为gclocker导致remark特别长(虽然加了-XX:+CMSScavReengeBeforeRemark参数,但是gclocker会导致remark前的YGC被delay),无法忍受这么长的暂停就只好迁移到了G1,经过一系列的调优之后算比较稳定了,这套参数便推到了全部机器上
可是就在上周突然有机器出现了Full GC,本来G1设计出来就是希望Full GC不在出现,出现Full GC一般是不正常,GC日志如下:
从上面日志不难发现是因为Perm触发的Full GC,并且Full GC之后Perm就降下去了,不过需要提一下的是JDK7下正常的G1 GC是不会做类卸载的,只有Full GC的时候才会卸载,但JDK8下是提供了相关参数的可以在G1 GC某些阶段做类卸载
于是要业务方先做了coredump,保存好现场再重启系统,然后再针对coredump做了heap dump,不过heapdump有40G这么大,可以通过jmap -permstat core.xxx来看看究竟perm里有什么东西
这篇文章相对来说比较长,涉及到的知识点比较多,如果实在忍不住看下去,可以跳到最后看下我对这个问题的描述再反过来看这篇文章或许让你有更清晰的认识
Perm里究竟塞了什么
既然是Perm满了,那我们得看Perm里究竟放了什么,我们知道Perm里主要存的是类的原始数据,比如我们加载了一个类,那这个类的信息会在Perm里分配内存来存储它的一些数据结构,所以大部分情况下,Perm的使用量和加载的类个数是关系很大的,当然Perm里在低版本的时候还会存一些其他的数据,比如String(String.intern()的情况)。
另外经验告诉我们如果真的是Perm溢出,那有地方动态构建一个类加载器加载一个类的可能性会很大,通过上面的jmap命令,我们可以统计下sun.reflect.DelegatingClassLoader的个数居然达到了415737个
那基本可以锁定是反射类加载器导致Perm溢出的原因了,那究竟为什么会有这么多反射类加载器呢,反射类加载器又是什么,接下来先简单说下反射的原理
反射的原理
反射大家用起来很方便,由于性能其实也比较不错了,因此用得挺广的,我们通常这么用反射
Method method = XXX.class.getDeclaredMethod(xx,xx);method.invoke(target,params)
不过这里我不准备用大量的代码来描述其原理,而是讲几个关键的东西,然后将他们串起来
获取Method
要调用首先要获取Method,而获取Method的逻辑是通过Class这个类来的,而关键的几个方法和属性如下:
在Class里有个关键的属性叫做reflectionData,这里主要存的是每次从jvm里获取到的一些类属性,比如方法,字段等,大概长这样
这个属性主要是SoftReference的,也就是在某些内存比较苛刻的情况下是可能被回收的,不过正常情况下可以通过-XX:SoftRefLRUPolicyMSPerMB这个参数来控制回收的时机,一旦时机到了,只要GC发生就会将其回收,那回收之后意味着再有需求的时候要重新创建一个这样的对象,同时也需要从JVM里重新拿一份数据,那这个数据结构关联的Method,Field字段等都是重新生成的对象。如果是重新生成的对象那可能有什么麻烦?讲到后面就明白了
getDeclaredMethod方法其实很简单,就是从privateGetDeclaredMethods返回的方法列表里复制一个Method对象返回。而这个复制的过程是通过searchMethods实现的
如果reflectionData这个属性的declaredMethods非空,那privateGetDeclaredMethods就直接返回其就可以了,否则就从JVM里去捞一把出来,并赋值给reflectionData的字段,这样下次再调用privateGetDeclaredMethods时候就可以用缓存数据了,不用每次调到JVM里去获取数据,因为reflectionData是Softreference,所以存在取不到值的风险,一旦取不到就又去JVM里捞了
searchMethods将从privateGetDeclaredMethods返回的方法列表里找到一个同名的匹配的方法,然后复制一个方法对象出来,这个复制的具体实现,其实就是Method.copy方法:
由此可见,我们每次通过调用getDeclaredMethod方法返回的Method对象其实都是一个新的对象,所以不宜多调哦,如果调用频繁最好缓存起来。不过这个新的方法对象都有个root属性指向reflectionData里缓存的某个方法,同时其methodAccessor也是用的缓存里的那个Method的methodAccessor。
Method调用
有了Method之后,那就可以调用其invoke方法了,那先看看Method的几个关键信息
root属性其实上面已经说了,主要指向缓存里的Method对象,也就是当前这个Method对象其实是根据root这个Method构建出来的,因此存在一个root Method派生出多个Method的情况。
methodAccessor这个很关键了,其实Method.invoke方法就是调用methodAccessor的invoke方法,methodAccessor这个属性如果root本身已经有了,那就直接用root的methodAccessor赋值过来,否则的话就创建一个
MethodAccessor的实现
MethodAccessor本身就是一个接口
其主要有三种实现
- DelegatingMethodAccessorImpl
- NativeMethodAccessorImpl
- GeneratedMethodAccessorXXX
其中DelegatingMethodAccessorImpl是最终注入给Method的methodAccessor的,也就是某个Method的所有的invoke方法都会调用到这个DelegatingMethodAccessorImpl.invoke,正如其名一样的,是做代理的,也就是真正的实现可以是下面的两种
如果是NativeMethodAccessorImpl,那顾名思义,该实现主要是native实现的,而GeneratedMethodAccessorXXX是为每个需要反射调用的Method动态生成的类,后的XXX是一个数字,不断递增的 并且所有的方法反射都是先走NativeMethodAccessorImpl,默认调了15次之后,才生成一个GeneratedMethodAccessorXXX类,生成好之后就会走这个生成的类的invoke方法了 那如何从NativeMethodAccessorImpl过度到GeneratedMethodAccessorXXX呢,来看看NativeMethodAccessorImpl的invoke方法
其中我上面说的是15次就是ReflectionFactory.inflationThreshold()这个方法返回的,这个15当然也不是一尘不变的,我们可以通过-Dsun.reflect.inflationThreshold=xxx来指定,我们还可以通过-Dsun.reflect.noInflation=true来直接绕过上面的15次NativeMethodAccessorImpl调用,和-Dsun.reflect.inflationThreshold=0的效果一样的 而GeneratedMethodAccessorXXX都是通过new MethodAccessorGenerator().generateMethod来生成的,一旦创建好之后就设置到DelegatingMethodAccessorImpl里去了,这样下次Method.invoke就会调到这个新创建的MethodAccessor里了。
那生成的GeneratedMethodAccessorXXX究竟长什么样呢,大概这样了
其实就是直接调用目标对象的具体方法了,和正常的方法调用没什么区别
GeneratedMethodAccessorXXX的类加载器
那加载GeneratedMethodAccessorXXX的类加载器是什么呢,在生成好了字节码之后会调用下面的方法做类定义
所以GeneratedMethodAccessorXXX的类加载器其实是一个DelegatingClassLoader类加载器
之所以搞一个新的类加载器,是为了性能考虑,在某些情况下可以卸载这些生成的类,因为类的卸载是只有在类加载器可以被回收的情况下才会被回收的,如果用了原来的类加载器,那可能导致这些新创建的类一直无法被卸载,从其设计来看本身就不希望他们一直存在内存里的,在需要的时候有就行了,在内存紧俏的时候可以释放掉内存
并发导致垃圾类创建
看到这里不知道大家是否发现了一个问题,上面的NativeMethodAccessorImpl.invoke其实都是不加锁的,那意味着什么?如果并发很高的时候,是不是意味着可能同时有很多线程进入到创建GeneratedMethodAccessorXXX类的逻辑里,虽然说最终使用的其实只会有一个,但是这些开销是不是已然存在了,假如有1000个线程都进入到创建GeneratedMethodAccessorXXX的逻辑里,那意味着多创建了999个无用的类,这些类会一直占着内存,直到能回收Perm的GC发生才会回收
那究竟是什么方法在不断反射呢
有了上面对反射原理的了解之后,我们知道了在反射执行到一定次数之后,其实会动态构建一个类,在这个类里会直接调用目标对象的对应的方法,我们从heap dump里看到了有大量的DelegatingClassLoader类加载器加载了GeneratedMethodAccessorXXX类,那这些类到底是调用了什么方法呢,于是我们不得不做一件事,那就是将内存里的这些类都dump下来,然后对字节码做一个统计分析一下
运行时Dump类字节码
我们可以利用SA的接口从coredump里或者live进程里将对应的类dump下来,为了dump下来我们特定的类,首先我们写一个Filter类
使用SA的jar($JAVA_HOME/lib/sa-jdi.jar)编译好类之后,然后我们在编译好的类目录下调用下面的命令进行dump
这样我们就可以将所有的GeneratedMethodAccessor给dump下来了,这个时候我们再通过javap -verbose GeneratedMethodAccessor9随便看一个类的字节码
看到上面关键的bci为36的那行,这里的方法便是我们反射调用的方法了,比如上面的那个反射调用的方法就是org/codehaus/xfire/util/ParamReader.readCode
定位到具体的反射类及方法
dump出这些字节码之后,我们对这些所有的类的字节码做一个统计,就找出了所有的反射调用方法,然后发现某些model类(package都是相同的)居然产生了20多万个类,这意味着有非常多的这些model类做反射
有了这个线索之后就去看代码究竟哪里会有调用这些model方法的反射逻辑,但是可惜没有找到,但是这种model对象极有可能在某种情况下出现,那就是rpc反序列化的时候,最终询问业务方是使用的Xfire的服务,而凭借我多年框架开发积累的经验,确定Xfire就是通过反射的方式来反序列化对象的,具体代码如下(org.codehaus.xfire.aegis.type.basic.BeanType.writeProperty):
而javabean的PropertyDeor里的get/set方法,其实本身就是SoftReference包装的
看到这里或许大家都明白了吧,前面也已经说了SoftReference是可能被GC回收掉的,时间一到在下次GC里就会被回收,如果被回收了,那就要重新获取,然后相当于是调用的新的Method对象的invoke方法,那调用次数一多,就会产生新的动态构建的类,而这份类会一直存到直到可以回收Perm的GC
G1回收Perm
注意下业务系统使用的是JDK7的G1,而JDK7的G1对perm其实正常情况下是不会回收的,只有在Full GC的时候才会回收Perm,这就解释了经过了多次G1 GC之后,那些Softreference的对象会被回收,但是新产生的类其实并不会被回收,所以G1 GC越频繁,那意味着SoftReference的对象越容易被回收(虽然正常情况下是时间到了,但是如果gc不频繁,即使时间到了,也会留在内存里的),越容易被回收那就越容易产生新的类,直到Full GC发生
解决方案
- 升级到jdk8,可以在G1 GC过程中对类做卸载
- 换一个序列化协议,不走方法反射的,比如hessian
- 调整SoftRefLRUPolicyMSPerMB这个参数变大,不过这个不能治本
总结
上面涉及的内容非常多,如果不多读几遍可能难以串起来,我这里将这个问题发生的情况大致描述一下:
这个系统在JDK7下使用G1,而这个版本的G1只有在Full GC的时候才会对Perm里的类做卸载,该系统因为大量的请求导致G1 GC发生很频繁,同时该系统还设置了-XX:SoftRefLRUPolicyMSPerMB=0,那意味着SoftReference的生命周期不会跨GC周期,能很快被回收掉,这个系统存在大量的RPC调用,走的Xfire协议,对返回结果做反序列化的时候是走的Method.invoke的逻辑,而相关的method因此被SoftReference引用,因此很容易被回收,一旦被回收,那就创建一个新的Method对象,再调用其invoke方法,在调用到一定次数(15次)之后,就构建一个新的字节码类,伴随着GC的进行,同一个方法的字节码类不断构建,直到将Perm充满触发一次Full GC才得以释放
作者:PerfMa
链接:https://juejin.im/post/5e8ed12bf265da47d00a58f8
来源:掘金
相关推荐
- 墨尔本一华裔男子与亚裔男子分别失踪数日 警方寻人
-
中新网5月15日电据澳洲新快网报道,据澳大利亚维州警察局网站消息,22岁的华裔男子邓跃(Yue‘Peter’Deng,音译)失踪已6天,维州警方于当地时间13日发布寻人通告,寻求公众协助寻找邓跃。华...
- 网络交友须谨慎!美国犹他州一男子因涉嫌杀害女网友被捕
-
伊森·洪克斯克(图源网络,侵删)据美国广播公司(ABC)25日报道,美国犹他州一名男子于24日因涉嫌谋杀被捕。警方表示,这名男子主动告知警局,称其杀害了一名在网络交友软件上认识的25岁女子。雷顿警...
- 一课译词:来龙去脉(来龙去脉 的意思解释)
-
Mountainranges[Photo/SIPA]“来龙去脉”,汉语成语,本指山脉的走势和去向,现比喻一件事的前因后果(causeandeffectofanevent),可以翻译为“i...
- 高考重要考点:range(range高考用法)
-
range可以用作动词,也可以用作名词,含义特别多,在阅读理解中出现的频率很高,还经常作为完形填空的选项,而且在作文中使用是非常好的高级词汇。...
- C++20 Ranges:现代范围操作(现代c++白皮书)
-
1.引言:C++20Ranges库简介C++20引入的Ranges库是C++标准库的重要更新,旨在提供更现代化、表达力更强的方式来处理数据序列(范围,range)。Ranges库基于...
- 学习VBA,报表做到飞 第二章 数组 2.4 Filter函数
-
第二章数组2.4Filter函数Filter函数功能与autofilter函数类似,它对一个一维数组进行筛选,返回一个从0开始的数组。...
- VBA学习笔记:数组:数组相关函数—Split,Join
-
Split拆分字符串函数,语法Split(expression,字符,Limit,compare),第1参数为必写,后面3个参数都是可选项。Expression为需要拆分的数据,“字符”就是以哪个字...
- VBA如何自定义序列,学会这些方法,让你工作更轻松
-
No.1在Excel中,自定义序列是一种快速填表机制,如何有效地利用这个方法,可以大大增加工作效率。通常在操作工作表的时候,可能会输入一些很有序的序列,如果一一录入就显得十分笨拙。Excel给出了一种...
- Excel VBA入门教程1.3 数组基础(vba数组详解)
-
1.3数组使用数组和对象时,也要声明,这里说下数组的声明:'确定范围的数组,可以存储b-a+1个数,a、b为整数Dim数组名称(aTob)As数据类型Dimarr...
- 远程网络调试工具百宝箱-MobaXterm
-
MobaXterm是一个功能强大的远程网络工具百宝箱,它将所有重要的远程网络工具(SSH、Telnet、X11、RDP、VNC、FTP、MOSH、Serial等)和Unix命令(bash、ls、cat...
- AREX:携程新一代自动化回归测试工具的设计与实现
-
一、背景随着携程机票BU业务规模的不断提高,业务系统日趋复杂,各种问题和挑战也随之而来。对于研发测试团队,面临着各种效能困境,包括业务复杂度高、数据构造工作量大、回归测试全量回归、沟通成本高、测试用例...
- Windows、Android、IOS、Web自动化工具选择策略
-
Windows平台中应用UI自动化测试解决方案AutoIT是开源工具,该工具识别windows的标准控件效果不错,但是当它遇到应用中非标准控件定义的UI元素时往往就无能为力了,这个时候选择silkte...
- python自动化工具:pywinauto(python快速上手 自动化)
-
简介Pywinauto是完全由Python构建的一个模块,可以用于自动化Windows上的GUI应用程序。同时,它支持鼠标、键盘操作,在元素控件树较复杂的界面,可以辅助我们完成自动化操作。我在...
- 时下最火的 Airtest 如何测试手机 APP?
-
引言Airtest是网易出品的一款基于图像识别的自动化测试工具,主要应用在手机APP和游戏的测试。一旦使用了这个工具进行APP的自动化,你就会发现自动化测试原来是如此简单!!连接手机要进行...
- 【推荐】7个最强Appium替代工具,移动App自动化测试必备!
-
在移动应用开发日益火爆的今天,自动化测试成为了确保应用质量和用户体验的关键环节。Appium作为一款广泛应用的移动应用自动化测试工具,为测试人员所熟知。然而,在不同的测试场景和需求下,还有许多其他优...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- mybatis plus (70)
- scheduledtask (71)
- css滚动条 (60)
- java学生成绩管理系统 (59)
- 结构体数组 (69)
- databasemetadata (64)
- javastatic (68)
- jsp实用教程 (53)
- fontawesome (57)
- widget开发 (57)
- vb net教程 (62)
- hibernate 教程 (63)
- case语句 (57)
- svn连接 (74)
- directoryindex (69)
- session timeout (58)
- textbox换行 (67)
- extension_dir (64)
- linearlayout (58)
- vba高级教程 (75)
- iframe用法 (58)
- sqlparameter (59)
- trim函数 (59)
- flex布局 (63)
- contextloaderlistener (56)