JAVA反射原理
什么是反射?
反射,一种计算机处理方式。是程序可以访问、检测和修改它本身状态或行为的一种能力。java反射使得我们可以在程序运行时动态加载一个类,动态获取类的基本信息和定义的方法,构造函数,域等。除了检阅类信息外,还可以动态创建类的实例,执行类实例的方法,获取类实例的域值。反射使java这种静态语言有了动态的特性。
类的加载
java反射机制是围绕Class类展开的,在深入java反射原理之前,需要对类加载机制有一个大致的了解。jvm使用ClassLoader将字节码文件(class文件)加载到方法区内存中:
Class clazz = ClassLoader.getSystemClassLoader().loadClass("com.mypackage.MyClass");
可见ClassLoader根据类的完全限定名加载类并返回了一个Class对象,而java反射的所有起源都是从这个class类开始的。
ReflectionData
为了提高反射的性能,缓存显然是必须的。class类内部有一个useCaches静态变量来标记是否使用缓存,这个值可以通过外部配置项sun.reflect.noCaches进行开关。class类内部提供了一个ReflectionData内部类用来存放反射数据的缓存,并声明了一个reflectionData域,由于稍后进行按需延迟加载并缓存,所以这个域并没有指向一个实例化的ReflectionData对象。
//标记是否使用缓存,可以通过外部配置项sun.reflect.noCaches进行禁用。
private static boolean useCaches = true;
static class ReflectionData<T> {
volatile Field[] declaredFields;
volatile Field[] publicFields;
volatile Method[] declaredMethods;
volatile Method[] publicMethods;
volatile Constructor<T>[] declaredConstructors;
volatile Constructor<T>[] publicConstructors;
volatile Field[] declaredPublicFields;
volatile Method[] declaredPublicMethods;
final int redefinedCount;
ReflectionData(int redefinedCount) {
this.redefinedCount = redefinedCount;
}
}
//注意这是个SoftReference,在内存资源紧张的时候可能会被回收。volatile保证多线程环境下的读写的正确性
private volatile transient SoftReference<ReflectionData<T>> reflectionData;
//J主要用于和ReflectionData中的redefinedCount进行比较,如果两个值不相等,说明ReflectionData缓存的数据已经过期了。
private volatile transient int classRedefinedCount = 0;
获取类的构造函数
在获取一个类的class后,我们可以通过反射获取一个类的所有构造函数,class类内部封装了如下方法用于提取构造函数:
//publicOnly指示是否只获取public的构造函数,我们常用的getConstructors方法是只返回public的构造函数,而getDeclaredConstructors返回的是所有构造函数,由于java的构造函数不会继承,所以这里不包含父类的构造函数。
private Constructor<T>[] privateGetDeclaredConstructors(boolean publicOnly) {
checkInitted(); //主要是读取了sun.reflect.noCaches配置。
Constructor<T>[] res;
ReflectionData<T> rd = reflectionData();//这里缓存中读取reflectionData,如果还没有缓存,则创建一个reflectionData并设置到缓存。但是注意这个ReflectionData可能只是个空对象,里面并没有任何数据。
if (rd != null) {
res = publicOnly ? rd.publicConstructors : rd.declaredConstructors;
if (res != null) return res;//检查缓存中是否有数据
}
//没有缓存数据可用,从这里开始需要从jvm中去获取数据。
if (isInterface()) {
res = new Constructor[0];//接口没有构造函数
} else {
res = getDeclaredConstructors0(publicOnly);//native方法,从jvm中获取
}
//如果代码执行到了这里,说明需要更新缓存了,将之前从jvm中请求到的数据放置到缓存中。
if (rd != null) {
if (publicOnly) {
rd.publicConstructors = res;
} else {
rd.declaredConstructors = res;
}
}
return res;
}
上面的代码片段比较简单,主要就是读取配置,检查缓存中是否有有效数据,如果有,直接从缓存中返回,如果没有,调用native的方法从jvm中请求数据,然后设置到缓存。比较重要的是reflectionData()
这个调用。这个方法主要是用于延迟创建并缓存ReflectionData对象,注意是对象,里面并没有保存反射数据,这些数据只有在第一次执行相应的反射操作后才会被填充。下面是这个方法的实现代码:
private ReflectionData<T> reflectionData() {
SoftReference<ReflectionData<T>> reflectionData = this.reflectionData;
int classRedefinedCount = this.classRedefinedCount;
ReflectionData<T> rd;
//检查缓存是否有效,如果有效,从缓存中直接返回reflectionData。
if (useCaches &&
reflectionData != null &&
(rd = reflectionData.get()) != null &&
rd.redefinedCount == classRedefinedCount) {
return rd;
}
//无法使用到缓存,创建新的reflectionData
return newReflectionData(reflectionData, classRedefinedCount);
}
下面看一下newReflectionData()的实现:
private ReflectionData<T> newReflectionData(SoftReference<ReflectionData<T>> oldReflectionData,int classRedefinedCount) {
if (!useCaches) return null;
//这里使用while-cas模式更新reflectionData,主要是考虑多线程并发更新的问题,可能有另外一个线程已经更新了reflectionData,并且设置了有效的缓存数据,如果这里再次更新就把缓存数据覆盖了。
while (true) {
ReflectionData<T> rd = new ReflectionData<>(classRedefinedCount);
if (Atomic.casReflectionData(this, oldReflectionData, new SoftReference<>(rd))) {
return rd;//cas成功,那么我们swap的这个新对象是有效的。
}
//重新读取oldReflectionData,为下次重试cas做准备。
oldReflectionData = this.reflectionData;
classRedefinedCount = this.classRedefinedCount;
//先判断这个oldReflectionData是否有效。如果是无效的数据,需要去重试cas一个新的ReflectionData了。
if (oldReflectionData != null &&
(rd = oldReflectionData.get()) != null &&
rd.redefinedCount == classRedefinedCount) {
return rd;//reflectionData中已经是有效的缓存数据了,直接返回这个reflectionData
}
}
}
上面的几个代码片段是反射获取一个类的构造函数的主要方法调用。主要流程就是先从class内部的reflectionData缓存中读取数据,如果没有缓存数据,那么就从jvm中去请求数据,然后设置到缓存中,供下次使用。
执行构造函数
在通过反射获取到一个类的构造函数我们,一般我们会试图通过构造函数去实例化声明这个构造函数的类,如下:
MyClass myClass = (MyClass) constructor.newInstance();
下面来看看这个newInstance()到底发生了什么:
public T newInstance(Object... initargs)
throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException {
//首先判断语言级别的访问检查是否被覆盖了(通过setAccess(true)方法可以将private的成员变成public),如果没有被覆盖,需要进行访问权限检查。
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, null, modifiers);
}
}
//枚举无法通过反射创建实例
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
//创建ConstructorAccessor对象,并进行缓存
ConstructorAccessor ca = constructorAccessor;
if (ca == null) {
ca = acquireConstructorAccessor();
}
//通过ConstructorAccessor来执行newInstance
return (T) ca.newInstance(initargs);
}
上面代码主要就是做权限检查,如果权限通过,则通过执行acquireConstructorAccessor()获取一个ConstructorAccessor来真正执行newInstance,acquireConstructorAccessor()这个方法实现比较简单,真正工作的是ReflectionFactory.newConstructorAccessor(),这个方法的主要工作就是为一个Constructor动态生成针对ConstructorAccessor接口的实现ConstructorAccessorImpl,下面来看一下这个方法实现:
public ConstructorAccessor newConstructorAccessor(Constructor c) {
checkInitted();
Class declaringClass = c.getDeclaringClass();
//如果声明这个构造函数的类部是抽象类,则返回一个InstantiationExceptionConstructorAccessorImpl,这其实也是个ConstructorAccessor,只是它对newInstace的实现是直接抛出一个InstantiationException异常
if (Modifier.isAbstract(declaringClass.getModifiers())) {
return new InstantiationExceptionConstructorAccessorImpl(null);
}
//如果是Class,同上
if (declaringClass == Class.class) {
return new InstantiationExceptionConstructorAccessorImpl
("Can not instantiate java.lang.Class");
}
//如果是ConstructorAccessorImpl子类,这里会造成无限循环,所以直接通过native方式实例化这个类
if (Reflection.isSubclassOf(declaringClass,
ConstructorAccessorImpl.class)) {
return new BootstrapConstructorAccessorImpl(c);
}
//判断是否启用了Inflation机制,默认是启用了。
//如果没有启用Inflation机制,那么通过asm操作字节码的方式来生成一个ConstructorAccessorImpl类。
//如果启用了,那么在执行inflationThreshold(默认15次)次数之前,是通过navite调用来执行newInstance,超过这个次数之后,才会通过asm来生成类。
//Inflation机制主要是在执行时间和启动时间上做一个平衡,native方式执行慢但是第一次执行不耗费任何时间,asm生成代码的方式执行快(20倍),但是第一次生成需要耗费大量的时间。
//可以通过sun.reflect.noInflation和sun.reflect.inflationThreshold配置型来进行动态配置
if (noInflation) {
return new MethodAccessorGenerator().
generateConstructor(c.getDeclaringClass(),
c.getParameterTypes(),
c.getExceptionTypes(),
c.getModifiers());
} else {
NativeConstructorAccessorImpl acc =
new NativeConstructorAccessorImpl(c);
DelegatingConstructorAccessorImpl res =
new DelegatingConstructorAccessorImpl(acc);
acc.setParent(res);
return res;
}
}
由上面代码可见,在生成ConstructorAccessorImpl的操作上,一共提供多种版本的实现:直接抛出异常的实现,native执行的实现,asm生成代码的实现。native的实现比较简单,asm 版本的主要就是字节码操作了,比较复杂,暂时不做深入了。
对于Method
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
,正如其名一样的,是做代理的,也就是真正的实现可以是下面的两种
如果noInflation为false(默认值),方法newMethodAccessor都会返回DelegatingMethodAccessorImpl对象。
其实,DelegatingMethodAccessorImpl对象就是一个代理对象,负责调用被代理对象delegate的invoke方法,其中delegate参数就是目前的NativeMethodAccessorImpl对象,所以最终Method的invoke方法调用的是NativeMethodAccessorImpl对象invoke方法,这里用到了ReflectionFactory类中的inflationThreshold,此时当delegate调用了15次invoke方法之后,如果继续调用就通过MethodAccessorGenerator类的generateMethod()方法生成MethodAccessorImpl对象,并设置为delegate对象,这样下次执行Method.invoke时,就调用新建的MethodAccessor对象的invoke()方法了。
注意:generateMethod()
方法在生成MethodAccessorImpl
对象时,会在内存中生成对应的字节码,并调用ClassDefiner.defineClass
创建对应的class对象,在ClassDefiner.defineClass
方法实现中,每被调用一次都会生成一个DelegatingClassLoader
类加载器对象.
这里每次都生成新的类加载器,是为了性能考虑,在某些情况下可以卸载这些生成的类,因为类的卸载是只有在类加载器可以被回收的情况下才会被回收的,如果用了原来的类加载器,那可能导致这些新创建的类一直无法被卸载,从其设计来看本身就不希望这些类一直存在内存里的,在需要的时候有就行了。
并发导致垃圾类创建
看到这里不知道大家是否发现了一个问题,上面的NativeMethodAccessorImpl.invoke
其实都是不加锁的,那意味着什么?如果并发很高的时候,是不是意味着可能同时有很多线程进入到创建MethodAccessorImpl类的逻辑里,虽然说最终使用的其实只会有一个,但是这些开销是不是已然存在了,假如有1000个线程都进入到创建MethodAccessorImpl
的逻辑里,那意味着多创建了999个无用的类,这些类会一直占着内存,直到能回收Perm的GC发生才会回收
那究竟是什么方法在不断反射呢
有了上面对反射原理的了解之后,我们知道了在反射执行到一定次数之后,其实会动态构建一个类,在这个类里会直接调用目标对象的对应的方法,我们从heap dump里看到了有大量的DelegatingClassLoader
类加载器加载了MethodAccessorImpl
类,那这些类到底是调用了什么方法呢,于是我们不得不做一件事,那就是将内存里的这些类都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的PropertyDescriptor
里的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才得以释放