从一块儿GC血案谈到反射原理

前言

首先回答一下提问者的问题。这主要是因为存在大量反射而产生的临时类加载器和 ASM 临时生成的类,这些类会被保留在 Metaspace,一旦 Metaspace 即将满的时候,就会触发 FullGc,已达到回收再也不被使用的类对象的目的。具体问题请参考接下来的内容,更好的了解反射的实现原理。java

正文

概述

公司以前有个大内存系统(70G以上)一直使用CMS GC,不过由于该系统对时间很敏感,偶尔会由于gclocker致使remark特别长(虽然加了-XX:+CMSScavReengeBeforeRemark参数,可是gclocker会致使remark前的YGC被delay),没法忍受这么长的暂停就只好迁移到了G1,通过一系列的调优以后算比较稳定了,这套参数便推到了所有机器上缓存

但是就在上周忽然有机器出现了Full GC,原本G1设计出来就是但愿Full GC不在出现,出现Full GC通常是不正常,GC日志以下:数据结构

gc.jpg

从上面日志不难发现是由于Perm触发的Full GC,而且Full GC以后Perm就降下去了,不过须要提一下的是JDK7下正常的G1 GC是不会作类卸载的,只有Full GC的时候才会卸载,但JDK8下是提供了相关参数的能够在G1 GC某些阶段作类卸载多线程

因而要业务方先作了coredump,保存好现场再重启系统,而后再针对coredump作了heap dump,不过heapdump有40G这么大,能够经过jmap -permstat <executable java> core.xxx来看看究竟perm里有什么东西并发

这篇文章相对来讲比较长,涉及到的知识点比较多,若是实在忍不住看下去,能够跳到最后看下我对这个问题的描述再反过来看这篇文章或许让你有更清晰的认识app

Perm里究竟塞了什么

既然是Perm满了,那咱们得看Perm里究竟放了什么,咱们知道Perm里主要存的是类的原始数据,好比咱们加载了一个类,那这个类的信息会在Perm里分配内存来存储它的一些数据结构,因此大部分状况下,Perm的使用量和加载的类个数是关系很大的,固然Perm里在低版本的时候还会存一些其余的数据,好比String(String.intern()的状况)。框架

另外经验告诉咱们若是真的是Perm溢出,那有地方动态构建一个类加载器加载一个类的可能性会很大,经过上面的jmap命令,咱们能够统计下sun.reflect.DelegatingClassLoader的个数竟然达到了415737个jvm

那基本能够锁定是反射类加载器致使Perm溢出的缘由了,那究竟为何会有这么多反射类加载器呢,反射类加载器又是什么,接下来先简单说下反射的原理ide

反射的原理

反射你们用起来很方便,因为性能其实也比较不错了,所以用得挺广的,咱们一般这么用反射性能

Method method = XXX.class.getDeclaredMethod(xx,xx);method.invoke(target,params)

不过这里我不许备用大量的代码来描述其原理,而是讲几个关键的东西,而后将他们串起来

获取Method

要调用首先要获取Method,而获取Method的逻辑是经过Class这个类来的,而关键的几个方法和属性以下:

class.jpg

在Class里有个关键的属性叫作reflectionData,这里主要存的是每次从jvm里获取到的一些类属性,好比方法,字段等,大概长这样

3.jpg

这个属性主要是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方法:

4.jpg

因而可知,咱们每次经过调用getDeclaredMethod方法返回的Method对象其实都是一个新的对象,因此不宜多调哦,若是调用频繁最好缓存起来。不过这个新的方法对象都有个root属性指向reflectionData里缓存的某个方法,同时其methodAccessor也是用的缓存里的那个Method的methodAccessor。

Method调用

有了Method以后,那就能够调用其invoke方法了,那先看看Method的几个关键信息

5.jpg

root属性其实上面已经说了,主要指向缓存里的Method对象,也就是当前这个Method对象实际上是根据root这个Method构建出来的,所以存在一个root Method派生出多个Method的状况。

methodAccessor这个很关键了,其实Method.invoke方法就是调用methodAccessor的invoke方法,methodAccessor这个属性若是root自己已经有了,那就直接用root的methodAccessor赋值过来,不然的话就建立一个

MethodAccessor的实现

MethodAccessor自己就是一个接口

6.jpg

其主要有三种实现

  • DelegatingMethodAccessorImpl

  • NativeMethodAccessorImpl

  • GeneratedMethodAccessorXXX

其中DelegatingMethodAccessorImpl是最终注入给Method的methodAccessor的,也就是某个Method的全部的invoke方法都会调用到这个DelegatingMethodAccessorImpl.invoke,正如其名同样的,是作代理的,也就是真正的实现能够是下面的两种 7.jpg 若是是NativeMethodAccessorImpl,那顾名思义,该实现主要是native实现的,而GeneratedMethodAccessorXXX是为每一个须要反射调用的Method动态生成的类,后的XXX是一个数字,不断递增的 而且全部的方法反射都是先走NativeMethodAccessorImpl,默认调了15次以后,才生成一个GeneratedMethodAccessorXXX类,生成好以后就会走这个生成的类的invoke方法了 那如何从NativeMethodAccessorImpl过分到GeneratedMethodAccessorXXX呢,来看看NativeMethodAccessorImpl的invoke方法 8.jpg 其中我上面说的是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究竟长什么样呢,大概这样了 9.jpg 其实就是直接调用目标对象的具体方法了,和正常的方法调用没什么区别

GeneratedMethodAccessorXXX的类加载器

那加载GeneratedMethodAccessorXXX的类加载器是什么呢,在生成好了字节码以后会调用下面的方法作类定义 10.jpg

因此GeneratedMethodAccessorXXX的类加载器实际上是一个DelegatingClassLoader类加载器

之因此搞一个新的类加载器,是为了性能考虑,在某些状况下能够卸载这些生成的类,由于类的卸载是只有在类加载器能够被回收的状况下才会被回收的,若是用了原来的类加载器,那可能致使这些新建立的类一直没法被卸载,从其设计来看自己就不但愿他们一直存在内存里的,在须要的时候有就好了,在内存紧俏的时候能够释放掉内存

并发致使垃圾类建立

看到这里不知道你们是否发现了一个问题,上面的NativeMethodAccessorImpl.invoke其实都是不加锁的,那意味着什么?若是并发很高的时候,是否是意味着可能同时有不少线程进入到建立GeneratedMethodAccessorXXX类的逻辑里,虽说最终使用的其实只会有一个,可是这些开销是否是已然存在了,假若有1000个线程都进入到建立GeneratedMethodAccessorXXX的逻辑里,那意味着多建立了999个无用的类,这些类会一直占着内存,直到能回收Perm的GC发生才会回收

那到底是什么方法在不断反射呢

有了上面对反射原理的了解以后,咱们知道了在反射执行到必定次数以后,其实会动态构建一个类,在这个类里会直接调用目标对象的对应的方法,咱们从heap dump里看到了有大量的DelegatingClassLoader类加载器加载了GeneratedMethodAccessorXXX类,那这些类究竟是调用了什么方法呢,因而咱们不得不作一件事,那就是将内存里的这些类都dump下来,而后对字节码作一个统计分析一下

运行时Dump类字节码

咱们能够利用SA的接口从coredump里或者live进程里将对应的类dump下来,为了dump下来咱们特定的类,首先咱们写一个Filter类

11.jpg

使用SA的jar($JAVA_HOME/lib/sa-jdi.jar)编译好类以后,而后咱们在编译好的类目录下调用下面的命令进行dump

12.jpg

这样咱们就能够将全部的GeneratedMethodAccessor给dump下来了,这个时候咱们再经过javap -verbose GeneratedMethodAccessor9随便看一个类的字节码

12.jpg

看到上面关键的bci为36的那行,这里的方法即是咱们反射调用的方法了,好比上面的那个反射调用的方法就是org/codehaus/xfire/util/ParamReader.readCode

定位到具体的反射类及方法

dump出这些字节码以后,咱们对这些全部的类的字节码作一个统计,就找出了全部的反射调用方法,而后发现某些model类(package都是相同的)竟然产生了20多万个类,这意味着有很是多的这些model类作反射

13.jpg

有了这个线索以后就去看代码究竟哪里会有调用这些model方法的反射逻辑,可是惋惜没有找到,可是这种model对象极有可能在某种状况下出现,那就是rpc反序列化的时候,最终询问业务方是使用的Xfire的服务,而凭借我多年框架开发积累的经验,肯定Xfire就是经过反射的方式来反序列化对象的,具体代码以下(org.codehaus.xfire.aegis.type.basic.BeanType.writeProperty):

郑州哪家不孕不育医院好:http://www.zztjby.com/

14.jpg

而javabean的PropertyDeor里的get/set方法,其实自己就是SoftReference包装的

14.jpg

看到这里或许你们都明白了吧,前面也已经说了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才得以释放。

郑州不孕不育正规医院:http://jbk.39.net/yiyuanfengcai/tsyl_zztjyy/3029/

相关文章
相关标签/搜索