Java 反射源码学习之旅 | 京东云技术团队
1 背景
前段时间组内针对“拷贝实例属性是应该用 BeanUtils.copyProperties()还是 MapStruct”这个问题进行了一次激烈的 battle。支持 MapStruct 的同学给出了他嫌弃 BeanUtils 的理由:因为用了反射,所以慢。
这个理由一下子拉回了我遥远的记忆,在我刚开始了解反射这个 Java 特性的时候,几乎看到的每一篇文章都会有“Java 反射不能频繁使用”、“反射影响性能”之类的话语,当时只是当一个结论记下了这些话,却没有深究过为什么,所以正好借此机会来探究一下 Java 反射的代码。
2 反射包结构梳理
反射相关的代码主要在 jdk rt.jar 下的 java.lang.reflect 包下,还有一些相关类在其他包路径下,这里先按下不表。按照继承和实现的关系先简单划分下 java.lang.reflect 包:
① Constructor、Method、Field 三个类型分别可以描述实例的构造方法、普通方法和字段。三种类型都直接或间接继承了 AccessibleObject 这个类型,此类型里主要定义两种方法,一种是通用的、对访问权限进行处理的方法,第二种是可供继承重写的、与注解相关的方法。
② 只看选中的五种类型,我们平常所用到的普通类型,譬如 Integer、String,又或者是我们自定义的类型,都可以用 Class 类型的实例来表示。Java 引入泛型之后,在 JDK1.5 中扩充了其他四种类型,用于泛型的表示。分别是 ParameterizedType(参数化类型)、WildcardType(通配符类型)、TypeVariable(类型变量)、GenericArrayType(泛型数组)。
③ 与②中描述的五种基本类型对应,下图这五个接口/类分别用来表示五种基本类型的注解相关数据。
④ 下图为实现动态代理的相关类与接口。java.lang.reflect.Proxy 主要是利用反射的一些方法获取代理类的类对象,获取其构造方法,由此构造出一个实例。
java.lang.reflect.InvocationHandler 是代理类需要实现的接口,由代理类实现接口内的 invoke 方法,此方法会负责代理流程和被代理流程的执行顺序组织。
3 目标类实例的构造源码
以 String 类的对象实例化为例,看一下反射是如何进行对象实例化的。
Class 对象的构造由 native 方法完成,以 java.lang.String 类为例,先看看构造好的 Class 对象都有哪些属性:
可以看到目前只有 name 一个属性有值,其余属性暂时都是 null 或者默认值的状态。
下图是 clz.newInstance() 方法逻辑的流程图,接下来对其中主要的两个方法进行说明:
从上图可以看出整个流程有两个核心部分。因为通常情况下,对象的构造都需要依靠类里的构造方法来实现,所以第一部分就是拿到目标类对应的 Constructor 对象;第二部分就是利用 Constructor 对象,构造目标类的实例。
3.1 获取 Constructor 对象
首先上一张 Constructor 对象的属性图:
java.lang.Class#getConstructor0
此方法中主要做的工作是首先拿到目标类的 Constructor 实例数组(主要由 native 方法实现),数组里每一个对象都代表了目标类的一个构造方法。然后对数组进行遍历,根据方法入参提供的 parameterTypes,找到符合的 Constructor 对象,然后重新创造一个 Constructor 对象,属性值与原 Constructor 一致(称为副本 Constructor),并且副本 Constructor 的属性 root 指向源 Constructor,相当于对源 Constructor 对象进行了一层封装。
由于在 getConstructor0()方法将返回值返回给调用方之后,调用方在后续的流程里进行了 constructor.setAccesssible(true)的操作,这个方法的作用是关闭对 constructor 这个对象访问时的 Java 语言访问检查。语言访问检查是个耗时的操作,所以合理猜测是为了提高反射性能关闭了这个检查,又出于安全考虑,所以将最原始的对象进行了封装。
3.2 目标类实例的构造
sun.reflect.ConstructorAccessor#newInstance
此方法主要是利用上一步创建出来的 Constructor 对象,进行目标类实例的构造。Java 为了提高反射的性能,为类实例的构造提供了两种方案,一种是虚拟机自己实现的 native 方法,一种是 JDK 包里的 Java 方法。
首先来看代码里对 ConstructorAccessor 对象的构造,通过代码可以看出在方法 newConstructorAccessor 中构造了 ConstructorAccessor 接口的两个实现类,两个对象进行了相互引用,像这样子:
在调用 DelegatingConstructorAccessorImpl 的 newInstance 方法时,相当于为 NativeConstructorAccessorImpl 做了一层代理,实际调用的是 NativeConstructorAccessorImpl 类实现的方法。
newInstance 方法中决定使用哪种方法的是一个名为 numInvocations 的 int 类型的变量,每次调用到 newInstance 方法时,这个变量都会+1,当变量值超过阈值(15)时,就会使用 Java 方式进行目标类实例的创造,反之就会使用虚拟机实现的方式进行目标类实例的创造。
这样做是因为 Java 版本的实现流程很长,其中还包含了字节码构造的流程,所以初次构造比较耗时,但是长久来说性能更好,而 native 版本是初期使用速度较块,调用频繁的话性能会有所下降,所以做了根据阈值来判断使用哪个版本的设计。
重点关注以下 Java 版本的实现流程,首先构造了一个 ConstructorAccessorImpl 类的对象。这个对象的构造主要是依靠在代码里按照字节码文件的格式构造出来一个字节数组实现的。首先创建了一个 ByteVactor 接口的实现类对象,此类有两个属性,一个字节数组,一个 int 类型的数用来标识位置。ClassFileAssembler 类主要负责把各类值转化成字节码的格式然后填充到 ByteVactor 的实现类对象里。最后由 ClassDefiner.defineClass 方法对字节码数组进行处理,构造出 ConstructorAccessorImpl 对象。 最后 ConstructorAccessorImpl 实例还是会被传给 newInstance0()这个 native 方法,以此来构造最终的目标类实例
4 小结
最后根据上述学习思考下 Java 反射到底慢不慢这个问题。首先可以看到 JDK 为“反射时创建对象的过程”提供了两套实现,native 版本更快但是也使得 JVM 无法对其进行一些优化(譬如 JIT 的方法内联),当方法成为热点时,转用 Java 版本来进行实现则优化了这个问题。但 Java 版本的实现过程中需要动态生成字节码,还要加载一些额外的类,造成了内存的消耗,所以使用反射的时候还是应当注意一些是否会因为使用过多而造成内存溢出。
一次不成熟的源码学习历程,如有错误还请指正。
参考资料:
https://rednaxelafx.iteye.com/blog/548536
作者:京东物流 秦曌怡
来源:京东云开发者社区
版权声明: 本文为 InfoQ 作者【京东科技开发者】的原创文章。
原文链接:【http://xie.infoq.cn/article/ff06eaa5972093a361a51f607】。文章转载请联系作者。
评论