前言
反射是Spring、mybatis等框架的基础,对于常写业务逻辑的同学应该算是最熟悉的陌生人,今天我们就聊聊Java的反射机制,把不熟悉变成熟悉。
结论在前:
1:Class对象包含类的所有信息,可以通过该对象获取到构造方法,成员变量,成员方法和接口等信息,这些信息在JVM中同样以类的形式存在
2:Class对象有三种获取方法,字面量XXX.class,Object.getClass(),Class.forName()
3:从Class对象中获取的Method、Field等类的组成元素的时候获取到的实际上是该Class对象内部的一个缓存中存储的Method、Field的拷贝。
4:调用Method的invoke方法底层借助了一个叫做MethodAccessor的工具,这个工具又两种形态,一种是native,一种是字节码生成加载后的Magic形态,前者初始化快执行慢,后者初始化慢执行快,JVM权衡之后决定前15次用前者,超过15次用后者(inflation机制)。
5:Class对象中的缓存创建使用了CAS+Volatile+死循环这种无锁形式
一、Class对象知多少
Class对象可以说是反射的源泉,但是Class对象存放在哪,什么时候存放,存放了哪些信息你真的了解吗?下面我们就来揭开Class对象的神秘面纱。
1、类生命周期知多少
这个比较简单,栈、堆、方法区、其他,栈存放引用,堆存放对象,方法区存放类信息和静态变量以及字面量,实际上Class对象就存放在方法区中。
1.1、类的生命周期
加载,连接(验证,准备,解析),初始化,使用,卸载;解析阶段与初始化阶段顺序不一定。
1.2、类的初始化时机
以下几种情况虚拟机必须对类进行初始化(加载,验证,准备自然必须在此之前)
- 首先:遇到new,getstatic,putstatic或invokestatic这4条字节码指令时,一般为实例化对象,读取或设置一个类的静态字段(final的除外),调用一个类的静态方法的时候
- 然后:使用java.lang.reflect包的方法对类进行反射调用的时候
- 再次:初始化一个类的时候首先初始化其父类
然后:虚拟机启动的时候首先会初始化主类(main方法的类) - 最后:java.lang.invoke.MethodHandler实例最后的解析结果为Ref_getstatic,REF_putStatic,REF_invokeStatic的时候,这个方法对应的类需要触发初始化
从上面可以看出,当反射调用类的时候会触发类的加载。
1.3、类的加载过程
加载阶段需要完成的3件事情
- 首先:通过一个类的全限定名来获取定义此类的二进制字节流,实际上就是获取你已编译好的.class字节码文件
- 然后:将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 最后:在内存中生成一个代表这个类的java.lang.Class对象,作为方法区中这个类的各种数据的访问入口,这个对象存储在方法区中
重点就在这,Java的字节码Class信息会以Class对象的形式存放在方法区中!
二、类的结构信息知多少
让我们好好想想类有哪些信息组成,继承/实现的接口、全路径名、类名称、类访问限制、构造方法、成员变量、成员方法、静态变量等等。Class对象包含了所有获取这些信息的方法。这些信息中,反射最常用的是类的构造方法、成员变量、成员方法、成员方法参数这几个重要的属性,而且每个属性都有对应的类实现。
2.1、类信息的组成
类继承体系@2x.png- AnnotatedElement接口:该接口提供了获取注解信息的一些方法,即所有的参数、方法、成员变量、构造方法都能通过反射获取到注解的相关信息。
- package类:对应Java包的一些信息
- Parameter类:对应构造方法或者成员方法的参数信息,如参数访问限制,参数类型等等信息
- AccessibleObject类:该类是成员变量、成员方法、构造方法们的父类,提供了一些public、private等修饰符的入口检查(是否可以访问、设置访问权限等)
- GenericDeclaration类:提供泛型的一些信息
- Field类:这个不用说了,对应成员变量
- Executable类:顾名思义,可执行的,由两个子类,成员方法和构造方法,提供了两种方法公共的必要进行的一些操作,比如获取方法参数、获取方法名称、获取方法访问修饰符等等。
- Method类:对应方法的类,提供了类的一些方法信息
- Constructor类:对应构造方法的类,提供了构造方法的一些信息。
- Class类:包含类的所有信息
其实使用反射关注最多的就是上面的Field、Method、Constructor三个类。
三、反射
解释了这么多类的信息,实际上都是为了反射做铺垫。那么究竟什么是反射呢?反射其实是在JVM运行的时候通过Class对象动态的获取类信息,这与我们平时编码有所差异的地方是反射不需要事先写好代码及用编译器编译,而是直接在JVM运行过程中拿到类的方法等信息直接执行。
3.1、如何获取Class对象
- 方法一:通过字面量直接获取,如XXX.class,值得注意的是这种字面量不会触发类的初始化,但此时方法区中肯定已经有了XXX类的Class对象,因此可以推断XXX类已经被加载到方法区,只是没有完成初始化,实际上初始化就是给类中定义的类变量和成员变量及静态代码块进行初始化赋值和执行操作,初始化完的类程序员才真正可以new出来。
- 方法二:通过Object类的getClass方法,如xxxObject.getClass(),这种方法会触发类的初始化哦
- 方法三:通过Class的静态方法forName(),这种方式也会触发类的初始化。
3.2、反射获取Field、Constructor、Method等
有了Class对象就可以为所欲为吗?抱歉,有Class对象真的可以为所欲为!
public class SingletonTest {
// 私有构造方法
private SingletonTest(){
System.out.println("无参数---构造----");
}
// 私有构造方法
private SingletonTest(String a){
System.out.println("有参数---构造----参数值:" + a);
}
//定义私有类型的变量
private static volatile SingletonTest instance;
//定义一个静态共有方法
public static SingletonTest getInstance(){
if(instance == null){
synchronized(SingletonTest.class){
if(instance == null){
return new SingletonTest();
}
}
}
return instance;
}
public static void main(String[] args) throws Exception{
Class clazz = SingletonTest.class;
/*以下调用无参的、私有构造函数*/
Constructor c0= clazz.getDeclaredConstructor();
c0.setAccessible(true);
SingletonTest po=(SingletonTest)c0.newInstance();
System.out.println("无参构造函数\t"+po);
/*以下调用带参的、私有构造函数*/
Constructor c1=clazz.getDeclaredConstructor(new Class[]{String.class});
c1.setAccessible(true);
SingletonTest p1=(SingletonTest)c1.newInstance(new Object[]{"我是参数值"});
System.out.println("有参的构造函数\t"+p1);
}
}
执行结果:
无参数---构造----
无参构造函数 com.huo.demos.test.SingletonTest@15db9742
有参数---构造----参数值:我是参数值
有参的构造函数 com.huo.demos.test.SingletonTest@6d06d69c
具体的API这里就不一一调用了,自己去参考Java的API吧,上面很详细。这里要说的是反射技术可以拿到类的所有信息,并通过这些信息做你想做的事情,无论信息声明是私有的还是公有的。
四、反射原理
一个Class对象可以同时被多个线程同时反射,这里面有没有线程安全问题?多个线程同时反射会不会出现性能下降问题?带着这些疑问我们一起探索一下反射的原理。
首先,我们自己新建一个用于发射的类,反射获取该类的成员变量,构造方法,成员方法,如下
public class ReflectionMechanism {
public static void main(String[] args) throws Exception {
ClassNeedReflect target = new ClassNeedReflect();
Class<ClassNeedReflect> clazz = ClassNeedReflect.class;
Field field = clazz.getDeclaredField("fieldReflected");
Constructor<ClassNeedReflect> constructor = clazz.getDeclaredConstructor();
Method method = clazz.getDeclaredMethod("methodReflected");
method.invoke(target);
}
static class ClassNeedReflect {
private String fieldReflected;
private ClassNeedReflect(){
System.out.println("构造方法");
}
public void methodReflected() {
System.out.println("反射的方法");
}
}
}
然后,无论getXXX()方法(XXX表示成员变量,成员方法或者构造方法如getDeclaredMethod方法),内部都调用了privateGetXXX这个方法,该方法内部的前三行又不约而同的调用了下面这行代码。
ReflectionData<T> rd = reflectionData();
然后,rd是个缓存对象,缓存的内容实际上是反射需要获取的成员变量、成员方法、构造方法、类接口等可以由每个线程获取到的变量。为什么要这么做呢?缓存嘛,肯定是可以提高效率啊,多线程同时读取的时候就不用每次都去底层拿这些变量了,具体缓存内容如下。
private static class ReflectionData<T> {
volatile Field[] declaredFields;
volatile Field[] publicFields;
volatile Method[] declaredMethods;
volatile Method[] publicMethods;
volatile Constructor<T>[] declaredConstructors;
volatile Constructor<T>[] publicConstructors;
// Intermediate results for getFields and getMethods
volatile Field[] declaredPublicFields;
volatile Method[] declaredPublicMethods;
volatile Class<?>[] interfaces;
final int redefinedCount;
ReflectionData(int redefinedCount) {
this.redefinedCount = redefinedCount;
}
}
重点来了,敲黑板!!!,Class用软引用持有了一个缓存对象,在JVM内存吃紧的情况下防止OOM会把这个缓存对象回收,所以其失效时间是内存不足的时候。细心的同学可以发现,这个缓存数据是个volatile内存可见的变量,结合CAS操作刚好可以实现线程安全。
我们来分析reflectionData方法,假设现在多个线程同时调用了这个方法,那么在Class内部已经持有该缓存对象的情况下不会出现线程安全问题(因为都是读操作),但是在Class对象内部没持有该对象的时候多个线程同时创建这个reflectionData可能会出现创建多个对象的问题,我们来看下大师们是如何解决这个问题的。
当缓存对象不存在的时候会调用newReflectionData,注意到该方法实际上用了一个死循环+CAS操作,由于reflectionData变量是volatile内存可见的,死循环+cas+volatile是保证线程安全的常用手法(可以参见我前面concurrent包的博客),Atomic.casReflectionData保证线程安全的创建了缓存对象。值得注意的是新创建的内存内部只有成员变量而没有成员方法和构造方法供反射获取!!!
ReflectionData<T> rd = reflectionData();
private volatile transient SoftReference<ReflectionData<T>> reflectionData;
private ReflectionData<T> reflectionData() {
SoftReference<ReflectionData<T>> reflectionData = this.reflectionData;
int classRedefinedCount = this.classRedefinedCount;
ReflectionData<T> rd;
if (useCaches &&
reflectionData != null &&
(rd = reflectionData.get()) != null &&
rd.redefinedCount == classRedefinedCount) {
return rd;
}
return newReflectionData(reflectionData, classRedefinedCount);
}
private ReflectionData<T> newReflectionData(SoftReference<ReflectionData<T>> oldReflectionData,
int classRedefinedCount) {
if (!useCaches) return null;
while (true) {
ReflectionData<T> rd = new ReflectionData<>(classRedefinedCount);
if (Atomic.casReflectionData(this, oldReflectionData, new SoftReference<>(rd))) {
return rd;
}
oldReflectionData = this.reflectionData;
classRedefinedCount = this.classRedefinedCount;
if (oldReflectionData != null &&
(rd = oldReflectionData.get()) != null &&
rd.redefinedCount == classRedefinedCount) {
return rd;
}
}
}
再次,缓存对象创建完成之后成员方法类型和构造方法类型是什么时候加到缓存里面去的呢?这里我们以Method成员方法为例,在某个线程调用getDeclaredMethod方法获取/创建了缓存对象之后,接着首先会从缓存中获取该方法,如果获取不到才会调用Reflection.filterMethods方法从VM虚拟机中获取,然后如果有缓存的话更新缓存。
private Method[] privateGetDeclaredMethods(boolean publicOnly) {
checkInitted();
Method[] res;
ReflectionData<T> rd = reflectionData();
if (rd != null) {
res = publicOnly ? rd.declaredPublicMethods : rd.declaredMethods;
if (res != null) return res;
}
// 没有缓存的条件下从虚拟机中获取
res = Reflection.filterMethods(this, getDeclaredMethods0(publicOnly));
if (rd != null) {
if (publicOnly) {
rd.declaredPublicMethods = res;
} else {
rd.declaredMethods = res;
}
}
return res;
}
然后,上面获取到的是一个数组,而要想获取到指定名称的那个还经历了一个searchMethods方法。方法代码如下,该方法在匹配到了指定的Method之后会通过getReflectionFactory().copyMethod(res)底层调用copy方法,该方法返回的是一个新的Method对象,且对象持有一个root引用指向Method的this对象。所以如果是多线程调用这个方法实际上是每个线程都new了一个对象,这样做的好处类似于Spring中的原型,不会出现线程之间同时更改同一个Method对象的线程安全问题。Field、Constructor的方法与Method方法除了名字不同其他一模一样,连代码顺序都不变!所以原理都是相同的。
private static Method searchMethods(Method[] methods,
String name,
Class<?>[] parameterTypes)
{
Method res = null;
String internedName = name.intern();
for (int i = 0; i < methods.length; i++) {
Method m = methods[i];
if (m.getName() == internedName
&& arrayContentsEq(parameterTypes, m.getParameterTypes())
&& (res == null
|| res.getReturnType().isAssignableFrom(m.getReturnType())))
res = m;
}
return (res == null ? res : getReflectionFactory().copyMethod(res));
}
Method copy() {
if (this.root != null)
throw new IllegalArgumentException("Can not copy a non-root Method");
Method res = new Method(clazz, name, parameterTypes, returnType,
exceptionTypes, modifiers, slot, signature,
annotations, parameterAnnotations, annotationDefault);
res.root = this;
// Might as well eagerly propagate this if already present
res.methodAccessor = methodAccessor;
return res;
}
4.1、反射调用
以上反射获取对应对象过程结束,下面是对对象的使用,这里同样以方法为例,获取到方法之后会调用invoke方法执行方法。方法代码如下,该方法首先进行了一系列的检测,然后通过MethodAccessor调用invoke方法执行Method,开始时MethodAccessor为空,需要获取。
@CallerSensitive
public Object invoke(Object obj, Object... args)
throws IllegalAccessException, IllegalArgumentException,
InvocationTargetException
{
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, obj, modifiers);
}
}
MethodAccessor ma = methodAccessor; // read volatile
if (ma == null) {
ma = acquireMethodAccessor();
}
return ma.invoke(obj, args);
}
然后,看下获取MethodAccessor方法,首先也是从缓存中获取,如果获取不到就调用reflectionFactory.newMethodAccessor方法获取一个并设置缓存
private MethodAccessor acquireMethodAccessor() {
MethodAccessor tmp = null;
if (root != null) tmp = root.getMethodAccessor();
if (tmp != null) {
methodAccessor = tmp;
} else {
tmp = reflectionFactory.newMethodAccessor(this);
setMethodAccessor(tmp);
}
return tmp;
}
然后,具体看下newMethodAccesor方法,该方法可以使用inflation机制创建accessor,初始的时候noInfalation值为false,所以开始的时候会调用NativeMethodAccessorImpl的方法invoke。
private static boolean noInflation = false;
private static int inflationThreshold = 15;
public MethodAccessor newMethodAccessor(Method method) {
checkInitted();
if (noInflation) {
return new MethodAccessorGenerator().
generateMethod(method.getDeclaringClass(),
method.getName(),
method.getParameterTypes(),
method.getReturnType(),
method.getExceptionTypes(),
method.getModifiers());
} else {
NativeMethodAccessorImpl acc =
new NativeMethodAccessorImpl(method);
DelegatingMethodAccessorImpl res =
new DelegatingMethodAccessorImpl(acc);
acc.setParent(res);
return res;
}
}
再次,看下NativeMethodAccessorImpl的实现,这里的++numInvocations就是统计次数的,有源码可以看出,前15次会调用native的invoke0方法,这种方法初始化比较快,但性能不够好。而过了15次之后都是底层调用asm给Method生成字节码然后加载到内存形成对象进行调用的,这种方式初始化较慢,但性能较好。
class NativeMethodAccessorImpl extends MethodAccessorImpl {
private Method method;
private DelegatingMethodAccessorImpl parent;
private int numInvocations;
NativeMethodAccessorImpl(Method method) {
this.method = method;
}
public Object invoke(Object obj, Object[] args)
throws IllegalArgumentException, InvocationTargetException
{
if (++numInvocations > ReflectionFactory.inflationThreshold()) {
MethodAccessorImpl acc = (MethodAccessorImpl)
new MethodAccessorGenerator().
generateMethod(method.getDeclaringClass(),
method.getName(),
method.getParameterTypes(),
method.getReturnType(),
method.getExceptionTypes(),
method.getModifiers());
parent.setDelegate(acc);
}
return invoke0(method, obj, args);
}
void setParent(DelegatingMethodAccessorImpl parent) {
this.parent = parent;
}
private static native Object invoke0(Method m, Object obj, Object[] args);
}
最后,来看下asm生成字节码方法的实现,这个方法的最后返回了一个MagicAccesorImpl(开始是没有实现的空类),其中的AccessController.doPrivileged(new PrivilegeAction)相当于执行一个任务,该任务调用ClassDefiner的defineClass方法,该方法内部同样也是执行任务,但内部的方法每次执行的时候都会返回一个类加DelegatingClassLoader类加载器,然后把类的一些字节码信息连同这个类加载器一起交给unsafe让他去把字节码通过类加载器加载到内存里面来以MagicAccessorImpl的类形式存在与内存中。
return (MagicAccessorImpl) AccessController.doPrivileged(new PrivilegedAction() {
public MagicAccessorImpl run() {
try {
return (MagicAccessorImpl) ClassDefiner
.defineClass(arg12, arg16, 0, arg16.length, arg0.getClassLoader()).newInstance();
} catch (IllegalAccessException | InstantiationException arg1) {
throw new InternalError(arg1);
}
}
});
class MagicAccessorImpl {
}
class ClassDefiner {
static final Unsafe unsafe = Unsafe.getUnsafe();
static Class<?> defineClass(String arg, byte[] arg0, int arg1, int arg2, final ClassLoader arg3) {
ClassLoader arg4 = (ClassLoader) AccessController.doPrivileged(new PrivilegedAction() {
public ClassLoader run() {
return new DelegatingClassLoader(arg3);
}
});
return unsafe.defineClass(arg, arg0, arg1, arg2, arg4, (ProtectionDomain) null);
}
}
4.2、多线程情况下的反射
我们回到最开始的方法执行,假如有多个线程同时执行反射方法的调用,上面有说过每个线程持有一个自己的Method对象,当调用invoke方法后,每个线程都调用了acquireMethodAccessor()方法。
Method method = clazz.getDeclaredMethod("methodReflected");
method.invoke(target);
@CallerSensitive
public Object invoke(Object obj, Object... args)
throws IllegalAccessException, IllegalArgumentException,
InvocationTargetException
{
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, obj, modifiers);
}
}
MethodAccessor ma = methodAccessor; // read volatile
if (ma == null) {
ma = acquireMethodAccessor();
}
return ma.invoke(obj, args);
}
看下面两个图片,reflectionFactory是Method、Field、Constructor等类的父类AccessibleObject中的静态变量,在AccessibleObject初始化的时候就已经存在了,是由多线程共享的一个变量,所以在下图执行到reflectionFactory.newMethodAccessor(this)的时候可能会出现多线程同时执行的该方法,好在的是该方法我们上面分析过,内部都是无状态的变量。现在假如有1000个线程同时调用了该方法,线程执行到该方法的时候就会在内存中通过asm生成1000份字节码和1000个MagicAccessorImpl的类及类加载器。这些类会占用方法区哦,不同的虚拟机回收策略可能会有所不同,所以这块可能会有性能问题。这里不要被1000个线程误导,如果缓存的是NativeAccessorImpl即使是一个线程执行1000次也会出现同样的结果!
image.png
image.png
总结:
- 1、反射获取的Field、Method和Constructor等除了第一次都是从一个软引用的缓存中获取的拷贝
- 2、invoke方法内部也使用了缓存技术缓存执行invoke方法的Accessor对象,而且内部有种机制叫inflation,即在前15次执行invoke的时候会调用native,之后通过字节码技术创建MagicAccessorImpl对象执行invoke。inflation的执行次数阈值是可以设置的。
- 3、以上!
网友评论