本文介绍 Java 的反射机制,源码走读
什么是反射
先看正反的两个例子
正向的例子:已知一个类,直接进行实例化,使用类对象进行操作
1 | Dog dog = new Dog(); |
反射的例子:不知道是什么类,无法使用 new 关键字直接创建对象
1 | Class clz = Class.forName("org.example.Dog"); |
你可能会有疑问,这两个例子不就是写法不同吗,都是已知这个类是什么;在反射的例子中,只不过是用反射的形式创建对象和调用方法。看上去是这样的,不过在实际使用中,是在程序运行过程中通过字符串变量值,才知道是什么类
所以反射的概念可以理解为:运行时才知道要操作的类是什么,在运行时构造类的对象,调用其方法
反射的步骤
一般情况下,可以归纳为下面几个步骤:
获取类的 Class 对象实例
Class clz = Class.forName("...");
根据 Class 对象实例获取 Constructor 对象
Constructor constructor = clz.getConstructor();
使用 Constructor 对象的 newInstance 方法获取
Object obj = constructor.newInstance();
获取对象实例方法
Method method = clz.getMethod("eat", String.class);
调用对象实例方法
method.invoke(obj, "meat");
下面就这五个步骤,进行源码走读
源码走读
Class.forName
1 |
|
根据类全路径获取类对象
clz.getMethod
java.lang.Class#getMethod
1 |
|
方法 | 功能 |
---|---|
checkMemberAccess | 检查调用类是否有访问权限,默认是可以访问 |
getMethod0 | 根据方法名和参数列表查询目标方法 |
1 | private Method getMethod0(String name, Class<?>[] parameterTypes, boolean includeStaticMethods) { |
privateGetMethodRecursive:递归的根据方法名和参数类型寻找方法
1 | private Method privateGetMethodRecursive(String name, |
privateGetMethodRecursive 流程图如下
privateGetMethodRecursive 实现分析:
方法 | 功能 |
---|---|
privateGetDeclaredMethods | 返回root方法数组 |
searchMethods(Method[] methods, String name, Class<?>[] parameterTypes) | 在输入的方法列表中,查找方法名为name,参数为parameterTypes 的方法 |
privateGetDeclaredMethods:
从缓存中获取方法,如果缓存中没有,从 VM 中获取
类中的每个方法最初都是向 VM 请求获取的,从 VM 获取的这些 Method 对象,称为类中方法对应的 Method Root(根方法对象),最终找到的方法,是 Method Root 拷贝的副本 Method 对象
1 | // Returns an array of "root" methods. These Method objects must NOT |
涉及的方法分析:
方法 | 功能 |
---|---|
checkInitted() | 读VM变量初始化参数(是否要读缓存 useCaches,是否完成初始化 initted) |
reflectionData() | 懒加载反射数据:如果缓存中存在反射数据,则返回;缓存中没有,创建一个空的反射数据,用于从VM加载后写缓存 |
getDeclaredMethods0(boolean) | native方法,从VM中获取当前类的(公开/全部)方法 |
Reflection.filterMethods() | 一些 unsafe 方法需要被过滤 |
展开:reflectionData
1 | // Lazily create and cache ReflectionData |
有了 privateGetDeclaredMethods 获取的公开方法,放到 searchMethods 中,进行方法名和参数比较,如果最后发现 res 为空,说明没有找到目标方法,如果 res 不为空,要拷贝 Root 方法,返回 root 方法的副本
1 | private static Method searchMethods(Method[] methods, |
展开:getReflectionFactory().copyMethod(res) —— 根方法的拷贝
1 | public Method copyMethod(Method arg) { |
跟进去会调用:java.lang.reflect.Method#copy
1 | Method copy() { |
这里可以看到,拷贝的一定是根方法对象,然后用根方法对象(this)的所有参数创建了一个新的方法对象 res,res 的 root 指向了根方法对象(this),并设置 methodAccessor
问题:
- 为什么要有 Method Root?
- Method Root为什么不能传播到外部,必须经过复制
- 为什么 copy 的方法和根方法要用相同的 methodAccessor?
- methodAccessor 是做什么的?
method.invoke
1 | public Object invoke(Object obj, Object... args) |
把 method.invoke 分为两个部分:Part1. acquireMethodAccessor 和 Part2. ma.invoke
Part1. acquireMethodAccessor
检查方法对象的 methodAccessor(下面简称 ma) 是否为空,如果为空,创建一个 ma
追踪 acquireMethodAccessor:
1 | // NOTE that there is no synchronization used here. It is correct |
解读:先找父方法的 ma,如果父方法没有 ma,为父方法和自己创建 ma,返回这个 ma
追踪:reflectionFactory.newMethodAccessor(this)
1 | public MethodAccessor newMethodAccessor(Method var1) { |
解读:var1 为子方法,首先在 checkInitted() 中进行初始化:ReflectionFactory.noInflation 和 ReflectionFactory.inflationThreshold 两个参数,其中 noInflation 默认为 false 表示有膨胀机制,膨胀机制在后面会提到
checkInitted 后,默认 noInflation 是 false,则走到第二个分支,这里创建了两个对象:NativeMethodAccessorImpl 和 DelegatingMethodAccessorImpl类型,这两个类型都是 MethodAccessor 的实现类,var3 是 var2 的 parent,最后返回 var3。类之间关系如下:
var1, var2, var3 的引用关系如下:
追踪 setMethodAccessor (java.lang.reflect.Method#setMethodAccessor)
1 | // Sets the MethodAccessor for this Method object and |
使用 reflectionFactory.newMethodAccessor(this) 创建了新的 ma 后,将这个 ma 递归的向上赋值给父方法,使得该方法类以及向上所有的方法类的 ma 都是一个(为什么要这样做呢?)
Part2. ma.invoke
通过 Part1. acquireMethodAccessor 获取 ma 对象后,调用 ma 的 invoke 方法,obj 是要反射的实例,args 是方法参数;我们知道 ma 是一个 DelegatingMethodAccessorImlp 类型,追踪调用:
1 | public Object invoke(Object var1, Object[] var2) throws IllegalArgumentException, InvocationTargetException { |
this.delegate 是 NativeMethodAccessorImpl 类型,追踪调用:
1 | public Object invoke(Object var1, Object[] var2) throws IllegalArgumentException, InvocationTargetException { |
解读:如果调用次数大于阈值,生成另一个 ma 对象,并将原来 DelegatingMethodAccessorImpl 对象中的 delegate 属性指向最新的 ma 对象;本次依旧会用 NativeMethodAccessorImpl 的 invoke0 方法,不过下次调用会就不会再调用 NativeMethodAccessorImpl 的invoke() 方法了
1 | private static native Object invoke0(Method var0, Object var1, Object[] var2); |
下面是执行 invoke 的时序图:
当调用次数大于阈值(默认15),会将 DelegatingMethodAccessorImpl 的 delegate 属性更改为一个 GeneratedMethodAccessor,不去用 NativeMethodAccessorImpl 的 invoke0 方法;这个 GeneratedMethodAccessor.invoke 的具体实现,我没有找到,GeneratedMethodAccessor 的类文件也没有找到,这个 GeneratedMethodAccessor 是怎么来的,还要再讨论分析
根据注释所述:如注释所述,实际的MethodAccessor实现有两个版本,一个是Java实现的(GeneratedMethodAccessor.invoke),另一个是native code(NativeMethodAccessorImpl.invoke0)实现的。Java 实现的版本在初始化时需要较多时间,但长久来说性能较好;native 版本正好相反,启动时相对较快,但运行时间长了之后速度就比不过 Java 版了,为了权衡这两个版本的性能,引入了膨胀机制
所谓膨胀机制是指:一开始先使用 native 的 ma 对象,等 native ma 的调用次数达到了 ReflectionFactory.inflationThreshold 设定的阈值后,动态生成 java 版本的 ma 对象来调用
实验分析 invoke 膨胀机制
下面进行实验验证膨胀机制
实验设计
实验目的:比较 GeneratedMethodAccessor.invoke 和 NativeMethodAccessorImpl.invoke0 两种 Invoke 方法的执行时间和内存使用情况
通过 -Dsun.reflect.inflationThreshold=0 或 很大整数(保证实验中不会走到优化的分支)控制使用哪种方法:sun.reflect.inflationThreshold=0 使用 GeneratedMethodAccessor.invoke,sun.reflect.inflationThreshold=很大整数时使用 NativeMethodAccessorImpl.invoke0
1 | public class ReflectTest { |
耗时比较
下面是第n次执行和耗时(单位:ns)的表格,比较发现:第一次运行,GeneratedMethodAccessor.invoke 耗时是 Native Invoke 的20倍,从第2次开始,两者耗时均下降明显,从18-25次,GeneratedMethodAccessor.invoke 耗时均低于 Native Invoke
运行300次-350次的耗时比较,GeneratedMethodAccessor.invoke 平均耗时在625ns,Native invoke 平均耗时在 910 ns
运行960次-1000次的耗时比较,GeneratedMethodAccessor.invoke 平均耗时在125ns,Native invoke 平均耗时在 375 ns
内存比较
使用 visualvm 工具查看-Dsun.reflect.inflationThreshold=0 或 100000两种情况下的堆空间使用率
1 | public class ReflectTest { |
下图为使用 GeneratedMethodAccessor.invoke 的堆空间使用情况,invoke 执行过程中最大的 Used heap = 189.93M
下图为使用 Native Invoke 的堆空间使用情况,invoke 执行过程中最大 Used heap = 90.05M
可以发现 GeneratedMethodAccessor.invoke 比 native invoke 占用更多的堆空间
问题
下面来看下走读过程中遇到的几个问题:
问题:
methodAccessor 是做什么的?
答:用于 invoke 调用指定方法的
为什么要有 Method Root?
答:为了让同一个方法拷贝出来的多个 Method 对象,共享 method Accessor 对象;当第一次调用 method.invoke 时,没有 method Accessor 对象,则取 root 的 Method Accessor,如果 root 的 method Accessor 为空,则新创建一个 method Accessor,并给 root 赋值。又因为一个 Method 对象,一定是从 Method Root 拷贝来的,在拷贝过程中,会赋 root.methodAccessor 值(见java.lang.reflect.Method#copy),从而达到了共享 method Accessor 的效果
Method Root为什么不能传播到外部,必须经过复制,为什么 Method 对象本身不能共享
答:为了保持方法原有的样子,因为用户可以对拷贝出去的 Method 对象setAccessible,如果修改了根方法的 accessible,后面拷贝的方法都被修改了 setAccessible;同理,Method 对象本身也不能共享一个,因为可能只修改某个Method的属性(个人理解)
为什么 copy 的方法和根方法要用相同的 methodAccessor?
答:首先 methodAccessor 是用来执行指定方法的,从根方法拷贝出的多个副本,共享一个 methodAccessor,节省内存空间
个人感受:第一遍看的时候,先看到 Method Root,拷贝Method Root,后看到 methodAccessor,这样无法理解为什么要这么做;第二编看的时候,大概理解了 methodAccessor 的作用后,再看 Method Root,又有了新的认识,所以先理解MethodAccessor,对整个流程的理解比较重要
总结
本文介绍了什么是反射,进行了反射源码走读,主要是 clz.getMethod 和 method.invoke 两个函数的代码,以及其调用追踪,过了下整个反射过程,发现了两个重要的概念:Method Root、Method Accessor,通过代码分析它们的作用,以及膨胀机制,通过实验对膨胀机制在运行时间和内存上进行比较;最后对阅读过程中发现的几个问题进行讨论