您当前的位置:首页 > 电脑百科 > 程序开发 > 语言 > JAVA

Java 反射源码学习之旅

时间:2023-06-29 14:44:51  来源:  作者:京东云开发者

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<?> clz = Class.forName("java.lang.String");

String s =(String)clz.newInstance();

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 语言访问检查。语言访问检查是个耗时的操作,所以合理猜测是为了提高反射性能关闭了这个检查,又出于安全考虑,所以将最原始的对象进行了封装。

private Constructor<T> getConstructor0(Class<?>[] parameterTypes,

int which) throws NoSuchMethodException

{

//1、拿到Constructor实例数组并进行筛选

Constructor<T>[] constructors = privateGetDeclaredConstructors((which == Member.PUBLIC));

//2、通过对入参的比较筛选出符合条件的Constructor

for (Constructor<T> constructor : constructors) {

if (arrayContentsEq(parameterTypes,

constructor.getParameterTypes())) {

//3、创建副本Constructor

return getReflectionFactory().copyConstructor(constructor);

}

}

throw new NoSuchMethodException(getName() + ".<init>" + argumentTypesToString(parameterTypes));

}

3.2 目标类实例的构造

sun.reflect.ConstructorAccessor#newInstance

此方法主要是利用上一步创建出来的 Constructor 对象,进行目标类实例的构造。Java 为了提高反射的性能,为类实例的构造提供了两种方案,一种是虚拟机自己实现的 native 方法,一种是 JDK 包里的 Java 方法。

首先来看代码里对 ConstructorAccessor 对象的构造,通过代码可以看出在方法 newConstructorAccessor 中构造了 ConstructorAccessor 接口的两个实现类,两个对象进行了相互引用,像这样子:

//构造ConstructorAccessor对象

public ConstructorAccessor newConstructorAccessor(Constructor<?> var1) {

if (Modifier.isAbstract(var2.getModifiers())) {

} else {

NativeConstructorAccessorImpl var3 = new NativeConstructorAccessorImpl(var1);

DelegatingConstructorAccessorImpl var4 = new DelegatingConstructorAccessorImpl(var3);

var3.setParent(var4);

return var4;

}

}

在调用 DelegatingConstructorAccessorImpl 的 newInstance 方法时,相当于为 NativeConstructorAccessorImpl 做了一层代理,实际调用的是 NativeConstructorAccessorImpl 类实现的方法。

public Object newInstance(Object[] var1) throws InstantiationException, IllegalArgumentException, InvocationTargetException {

return this.delegate.newInstance(var1);

}

newInstance 方法中决定使用哪种方法的是一个名为 numInvocations 的 int 类型的变量,每次调用到 newInstance 方法时,这个变量都会 + 1,当变量值超过阈值(15)时,就会使用 Java 方式进行目标类实例的创造,反之就会使用虚拟机实现的方式进行目标类实例的创造。

这样做是因为 Java 版本的实现流程很长,其中还包含了字节码构造的流程,所以初次构造比较耗时,但是长久来说性能更好,而 native 版本是初期使用速度较块,调用频繁的话性能会有所下降,所以做了根据阈值来判断使用哪个版本的设计

public Object newInstance(Object[] var1) throws InstantiationException, IllegalArgumentException, InvocationTargetException {

if (++this.numInvocations > ReflectionFactory.inflationThreshold() && !ReflectUtil.isVMAnonymousClass(this.c.getDeclaringClass())) {

//Java方法构造对象

ConstructorAccessorImpl var2 = (ConstructorAccessorImpl)(new MethodAccessorGenerator()).generateConstructor(this.c.getDeclaringClass(), this.c.getParameterTypes(), this.c.getExceptionTypes(), this.c.getModifiers());

this.parent.setDelegate(var2);

}

//native方法实现实例化

return newInstance0(this.c, var1);

}

重点关注以下 Java 版本的实现流程,首先构造了一个 ConstructorAccessorImpl 类的对象。这个对象的构造主要是依靠在代码里按照字节码文件的格式构造出来一个字节数组实现的。首先创建了一个 ByteVactor 接口的实现类对象,此类有两个属性,一个字节数组,一个 int 类型的数用来标识位置。ClassFileAssembler 类主要负责把各类值转化成字节码的格式然后填充到 ByteVactor 的实现类对象里。最后由 ClassDefiner.defineClass 方法对字节码数组进行处理,构造出 ConstructorAccessorImpl 对象。 最后 ConstructorAccessorImpl 实例还是会被传给 newInstance0 () 这个 native 方法,以此来构造最终的目标类实例

private MagicAccessorImpl generate(final Class<?> var1, String var2, Class<?>[] var3, Class<?> var4, Class<?>[] var5, int var6, boolean var7, boolean var8, Class<?> var9) {

//创建ByteVectorImpl对象

ByteVector var10 = ByteVectorFactory.create();

//创建ClassFileAssembler对象

this.asm = new ClassFileAssembler(var10);

var10.trim();

//拿出构造好的字节数组(就是字节码文件的格式)

final byte[] var17 = var10.getData();

return (MagicAccessorImpl)AccessController.doPrivileged(new PrivilegedAction<MagicAccessorImpl>() {

public MagicAccessorImpl run() {

try {

//调用native方法,创建ConstructorAccessorImpl类的实例

//最后ConstructorAccessorImpl实例还是会被传给newInstance0()这个native方法,以此来构造最终的目标类实例

return (MagicAccessorImpl)ClassDefiner.defineClass(var13, var17, 0, var17.length, var1.getClassLoader()).newInstance();

} catch (IllegalAccessException | InstantiationException var2) {

throw new InternalError(var2);

}

}

});

}

}

4 小结

最后根据上述学习思考下 Java 反射到底慢不慢这个问题。首先可以看到 JDK 为 “反射时创建对象的过程” 提供了两套实现,native 版本更快但是也使得 JVM 无法对其进行一些优化(譬如 JIT 的方法内联),当方法成为热点时,转用 Java 版本来进行实现则优化了这个问题。但 Java 版本的实现过程中需要动态生成字节码,还要加载一些额外的类,造成了内存的消耗,所以使用反射的时候还是应当注意一些是否会因为使用过多而造成内存溢出。

一次不成熟的源码学习历程,如有错误还请指正。

参考资料:

https://rednaxelafx.iteye.com/blog/548536

 

作者:京东物流 秦曌怡
来源:京东云开发者社区


Tags:Java   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
17 个你需要知道的 JavaScript 优化技巧
你可能一直在使用JavaScript搞开发,但很多时候你可能对它提供的最新功能并不感冒,尽管这些功能在无需编写额外代码的情况下就可以解决你的问题。作为前端开发人员,我们必须了解...【详细内容】
2024-04-03  Search: Java  点击:(5)  评论:(0)  加入收藏
你不可不知的 15 个 JavaScript 小贴士
在掌握如何编写JavaScript代码之后,那么就进阶到实践&mdash;&mdash;如何真正地解决问题。我们需要更改JS代码使其更简单、更易于阅读,因为这样的程序更易于团队成员之间紧密协...【详细内容】
2024-03-21  Search: Java  点击:(27)  评论:(0)  加入收藏
Oracle正式发布Java 22
Oracle 正式发布 Java 22,这是备受欢迎的编程语言和开发平台推出的全新版本。Java 22 (Oracle JDK 22) 在性能、稳定性和安全性方面进行了数千种改进,包括对Java 语言、其API...【详细内容】
2024-03-21  Search: Java  点击:(10)  评论:(0)  加入收藏
构建一个通用灵活的JavaScript插件系统?看完你也会!
在软件开发中,插件系统为应用程序提供了巨大的灵活性和可扩展性。它们允许开发者在不修改核心代码的情况下扩展和定制应用程序的功能。本文将详细介绍如何构建一个灵活的Java...【详细内容】
2024-03-20  Search: Java  点击:(20)  评论:(0)  加入收藏
Java 8 内存管理原理解析及内存故障排查实践
本文介绍Java8虚拟机的内存区域划分、内存垃圾回收工作原理解析、虚拟机内存分配配置,以及各垃圾收集器优缺点及场景应用、实践内存故障场景排查诊断,方便读者面临内存故障时...【详细内容】
2024-03-20  Search: Java  点击:(14)  评论:(0)  加入收藏
如何编写高性能的Java代码
作者 | 波哥审校 | 重楼在当今软件开发领域,编写高性能的Java代码是至关重要的。Java作为一种流行的编程语言,拥有强大的生态系统和丰富的工具链,但是要写出性能优异的Java代码...【详细内容】
2024-03-20  Search: Java  点击:(24)  评论:(0)  加入收藏
在Java应用程序中释放峰值性能:配置文件引导优化(PGO)概述
译者 | 李睿审校 | 重楼在Java开发领域,优化应用程序的性能是开发人员的持续追求。配置文件引导优化(Profile-Guided Optimization,PGO)是一种功能强大的技术,能够显著地提高Ja...【详细内容】
2024-03-18  Search: Java  点击:(25)  评论:(0)  加入收藏
对JavaScript代码压缩有什么好处?
对JavaScript代码进行压缩主要带来以下好处: 减小文件大小:通过移除代码中的空白符、换行符、注释,以及缩短变量名等方式,可以显著减小JavaScript文件的大小。这有助于减少网页...【详细内容】
2024-03-13  Search: Java  点击:(2)  评论:(0)  加入收藏
跨端轻量JavaScript引擎的实现与探索
一、JavaScript 1.JavaScript语言JavaScript是ECMAScript的实现,由ECMA 39(欧洲计算机制造商协会39号技术委员会)负责制定ECMAScript标准。ECMAScript发展史: 2.JavaScript...【详细内容】
2024-03-12  Search: Java  点击:(2)  评论:(0)  加入收藏
面向AI工程的五大JavaScript工具
令许多人惊讶的是,一向在Web开发领域中大放异彩的JavaScript在开发使用大语言模型(LLM)的应用程序方面同样大有价值。我们在本文中将介绍面向AI工程的五大工具,并为希望将LLM...【详细内容】
2024-02-06  Search: Java  点击:(53)  评论:(0)  加入收藏
▌简易百科推荐
Java 8 内存管理原理解析及内存故障排查实践
本文介绍Java8虚拟机的内存区域划分、内存垃圾回收工作原理解析、虚拟机内存分配配置,以及各垃圾收集器优缺点及场景应用、实践内存故障场景排查诊断,方便读者面临内存故障时...【详细内容】
2024-03-20  vivo互联网技术    Tags:Java 8   点击:(14)  评论:(0)  加入收藏
如何编写高性能的Java代码
作者 | 波哥审校 | 重楼在当今软件开发领域,编写高性能的Java代码是至关重要的。Java作为一种流行的编程语言,拥有强大的生态系统和丰富的工具链,但是要写出性能优异的Java代码...【详细内容】
2024-03-20    51CTO  Tags:Java代码   点击:(24)  评论:(0)  加入收藏
在Java应用程序中释放峰值性能:配置文件引导优化(PGO)概述
译者 | 李睿审校 | 重楼在Java开发领域,优化应用程序的性能是开发人员的持续追求。配置文件引导优化(Profile-Guided Optimization,PGO)是一种功能强大的技术,能够显著地提高Ja...【详细内容】
2024-03-18    51CTO  Tags:Java   点击:(25)  评论:(0)  加入收藏
Java生产环境下性能监控与调优详解
堆是 JVM 内存中最大的一块内存空间,该内存被所有线程共享,几乎所有对象和数组都被分配到了堆内存中。堆被划分为新生代和老年代,新生代又被进一步划分为 Eden 和 Survivor 区,...【详细内容】
2024-02-04  大雷家吃饭    Tags:Java   点击:(56)  评论:(0)  加入收藏
在项目中如何避免和解决Java内存泄漏问题
在Java中,内存泄漏通常指的是程序中存在一些不再使用的对象或数据结构仍然保持对内存的引用,从而导致这些对象无法被垃圾回收器回收,最终导致内存占用不断增加,进而影响程序的性...【详细内容】
2024-02-01  编程技术汇  今日头条  Tags:Java   点击:(68)  评论:(0)  加入收藏
Java中的缓存技术及其使用场景
Java中的缓存技术是一种优化手段,用于提高应用程序的性能和响应速度。缓存技术通过将计算结果或者经常访问的数据存储在快速访问的存储介质中,以便下次需要时可以更快地获取。...【详细内容】
2024-01-30  编程技术汇    Tags:Java   点击:(72)  评论:(0)  加入收藏
JDK17 与 JDK11 特性差异浅谈
从 JDK11 到 JDK17 ,Java 的发展经历了一系列重要的里程碑。其中最重要的是 JDK17 的发布,这是一个长期支持(LTS)版本,它将获得长期的更新和支持,有助于保持程序的稳定性和可靠性...【详细内容】
2024-01-26  政采云技术  51CTO  Tags:JDK17   点击:(89)  评论:(0)  加入收藏
Java并发编程高阶技术
随着计算机硬件的发展,多核处理器的普及和内存容量的增加,利用多线程实现异步并发成为提升程序性能的重要途径。在Java中,多线程的使用能够更好地发挥硬件资源,提高程序的响应...【详细内容】
2024-01-19  大雷家吃饭    Tags:Java   点击:(106)  评论:(0)  加入收藏
这篇文章彻底让你了解Java与RPA
前段时间更新系统的时候,发现多了一个名为Power Automate的应用,打开了解后发现是一个自动化应用,根据其描述,可以自动执行所有日常任务,说的还是比较夸张,简单用了下,对于office、...【详细内容】
2024-01-17  Java技术指北  微信公众号  Tags:Java   点击:(97)  评论:(0)  加入收藏
Java 在 2023 年仍然流行的 25 个原因
译者 | 刘汪洋审校 | 重楼学习 Java 的过程中,我意识到在 90 年代末 OOP 正值鼎盛时期,Java 作为能够真正实现这些概念的语言显得尤为突出(尽管我此前学过 C++,但相比 Java 影响...【详细内容】
2024-01-10  刘汪洋  51CTO  Tags:Java   点击:(75)  评论:(0)  加入收藏
站内最新
站内热门
站内头条