了解Java反射基本用法、原理及效率低原因,以openjdk 12源码分析

预备知识了解 Java 反射基本用法

了解Java反射基本用法、原理及效率低原因,以openjdk 12源码分析

看完本文可以达到什么程度

了解Java反射原理,了解Java反射效率低的原因,文章概览

在Java开发里,我们难免会碰到反射,在一些框架当中,反射的使用更为常见。我坚信,每当提及反射,大家的第一反应肯定是反射效率不高,要尽量少用。

但是反射的效率到底低多少?反射效率低的原因在哪里?

这篇文章就来探索一下这些问题。

本机安装的是openjdk 12 ,因此在这里采用openjdk 12 源码展开分析。

我们先来看一下结论,接着分析一下Java反射的原理,在这个过程中,大家能够依据结论,对源码进行一些思考,随后再依据原理中的一些实现,去探究Java反射效率低的原因。

零、先放结论

Java 反射效率低主要原因是:

Method#invoke方法会对参数进行封装和解封操作,需要检查方法可见性,需要校验参数,反射方法难以内联,JIT无法优化。一、Java反射原理--获取要反射的方法

1.1 反射的使用

我们先来看看 Java 反射使用的一段代码:

我们调用反射时,首先要创建Class对象,接着获取其Method对象,最后调用invoke方法。

获取反射方法时 ,存在两个方法 ,分别是getMethod和getDeclaredMethod ,我们将从这两个方法着手 ,逐步探究反射的原理 。

接下来就进入代码分析,大家做好准备。

获取方法,获取声明的方法

这里我们先从整体上看一下,getMethod的实现情况,再看一下getDeclaredMethod的实现情况。

从上面的代码,我们可以看到,获取方法的流程分三步走:

检查方法权限获取方法 Method 对象返回方法的拷贝

这里主要有两个区别:

getMethod 中,checkMemberAccess 传入的是 Member.PUBLIC,getDeclaredMethod 传入的是 Member.DECLARED,这两个值存在什么区别呢?我们查看代码中的注释:

注释里清晰地解释了PUBLIC和DECLARED的差异,PUBLIC会涵盖所有public方法,其中包括父类的方法,而DECLARED会包含所有自己定义的方法,public、protected、private这些都在其中,不过不包括父类的方法。

这同样是 getMethod 和 getDeclaredMethod 之间的差异所在。

getMethod 中获取方法调用的是 getMethod0,getDeclaredMethod 获取方法调用的是 privateGetDeclaredMethods,关于这个区别,这里简单提及一下,后面具体分析代码。

privateGetDeclaredMethods 用于获取类自身定义的方法,其有一个参数,该参数为 boolean 类型的 publicOnly,它表示是否只获取公共方法。

获取声明的方法数组,该方法数组是私有的,用于获取声明的方法,参数为是否仅获取公共方法的布尔值
 //...
}

getMethod0 会递归查找父类的方法,在这个过程中,会调用到 privateGetDeclaredMethods 方法。

既然我们上面看过了 getMethod 和 getDeclaredMethod 的差异,我们自然会选择 getMethod 方法来进行分析,如此一来便能够深入整个流程。

1.3 getMethod 方法

getMethod 方法流程如下图:

我们上面说到获取方法分三步走:

检查方法权限获取方法 Method 对象返回方法的拷贝

我们先看看检查方法权限做了些什么事情。

1.3.1 checkMemberAccess

在这里能够看到,针对非Member.PUBLIC的访问,会增添一项检测,

安全管理器检查权限

这项检测需要运行时申请“accessDeclaredMembers”的运行时权限

这里就不再继续往下看了,方法整体是在查验是否能够访问对象成员。

接着看下是如何获取方法的 Method 对象。

1.3.2 getMethod0

这里是借助 getMethodsRecursive 获取到 MethodList 对象,之后通过 MethodList#getMostSpecific 方法挑选出对应的方法。MethodList#getMOstSpecific 会筛选出返回值类型最为具体的方法,关于为什么会有返回值的区别,后面会进行讲解。

这里的具体,指的是存在两个方法,其返回值分别是 Child 和 Parent,Child 继承自 Parent,在此处会筛选出返回值为 Child 的方法。

接着看getMethodsRecursive方法,它是用来获取方法的,看看它是怎样获取的 。

1.3.3 getMethodsRecursive

这里获取方法有四个步骤:

通过privateGetDeclaredMethods获取自己所有的public方法,通过MethodList#filter查找方法名、参数相同的方法,若找到则直接返回,若自己未实现对应的方法,便去父类中查找对应的方法,还要查找接口中对应的方法。

通过上面四个步骤,最终获取到一个对象,这个对象是MethodList对象,它是一个链表结点,其next指向下一个结点,也就是说,这里获取到的Method会有多个。

这里稍作解释,我们平时编写Java代码时,同一个类中,方法名相同且方法参数也相同的方法是不存在的。而实际上,在JVM中,一个方法签名与返回值、方法名、方法参数这三者相关。也就是说,在JVM中,方法名和方法参数都相同,但返回值不同的方法是可以存在的。

所以这里返回的是一个方法链表。

所以上面最终返回方法时,会借助 MethodList#getMostSpecific 来进行返回值的筛选,通过这种筛选,能找出返回值类型最具体的方法 。

这里我们先暂停回顾一下整体的调用链路:

getMethod 指向 getMethod0,getMethod0 指向 getMethodsRecursive,getMethodsRecursive 指向 privateGetDeclaredMethods

通过函数调用,最终会调用到privateGetDeclaredMethods方法,这里才是真正获取方法的地方 。

获取已声明的方法,此方法为私有性质

在获取方法时,会用到privateGetDeclaredMethods,此过程包含两个步骤:

relectionData 通过缓存获取,若缓存未命中,便通过 getDeclaredMethods0 获取方法。

先看看 relectionData 方法:

在Class中,会维护一个软引用,这个软引用指向ReflectionData,它被用作反射数据的缓存。

ReflectionData 结构如下:

可以看到,它保存了Class中的属性和方法,若缓存为空,便会借助getDeclaredMethods0从JVM中查找方法。

getDeclaredMethods0是一个本地方法,这里暂且先不予以关注。

通过上面几个步骤,就获取到 Method 数组。

这就是 getMethod 方法的整个实现了。

我们再次回过头查看一下getDeclaredMethod方法的实现,借助privateGetDeclaredMethods获取方法之后,会运用searchMethods对方法开展筛选。

searchMethods方法的实现较为简单,它是通过对比方法名来实现的,它也是通过对比参数来实现的,它还是通过对比方法返回值来实现的。

1.3.5 Method#copy

获取到对应方法后,不会直接返回,而是会通过getReflectionFactory().copyMethod(method)来返回方法的一个拷贝。

最终调用的是 Method#copy,我们来看看其实现。

会 new 一个 Method 实例并返回。

这里有两点要注意:

设置root等于this,这会给Method设置MethodAccessor,该设置用于后面的方法调用,也就是说,所有Method的拷贝都会使用同一份methodAccessor。

通过上面的步骤,就获取到了需要反射的方法。

我们再回顾一下之前的流程。

二、Java 反射原理--调用反射方法

获取到方法以后,借助 Method#invoke 来调用方法。

invoke 方法的实现,分为三步:

2.1 检查是否有权限调用方法

这里会对override变量做判断,若override等于true,便会跳过检查。我们一般在调用Method#invoke之前,会调用Method#setAccessible(true),此操作就是将override值设为true 。

2.2 获取 MethodAccessor

在上面获取Method的时候曾提到,Method#copy会为Method的methodAccessor赋值,所以此处的methodAccessor就是拷贝时所用的MethodAccessor。

如果 ma 为空,就去创建 MethodAccessor。

这里会先查找 root 的 MethodAccessor,root 在上面 Method#copy 中被设置过 。

如果还是没有找到,就去创建 MethodAccessor。

在这里能够看到,存在三种MethodAccessor ,分别是MethodAccessorImpl ,NativeMethodAccessorImpl ,

DelegatingMethodAccessorImpl。

采用哪种MethodAccessor要依据noInflation来判断,noInflation的默认值是false,只有将sun.reflect.noInflation属性指定为true,才会采用MethodAccessorImpl。

所以,默认情况下,会调用 NativeMethodAccessorImpl 。

MethodAccessorImpl 通过动态生成字节码进行方法调用,它是Java版本的MethodAccessor,字节码生成较为复杂,此处不展示代码。若大家感兴趣,可查看这里的generate方法。

DelegatingMethodAccessorImpl 只是起到代理的作用,真正的实现依靠的是 NativeMethodAccessorImpl 。

NativeMethodAccessorImpl 是 Native 版本的实现,它属于 MethodAccessor 。

在 NativeMethodAccessorImpl 的实现里,我们能够看到,存在一个 numInvocations 阀值控制,numInvocations 所代表的是调用次数。如果numInvocations大于15,默认阀值为15,那么就使用Java版本的MethodAccessorImpl。

为什么采用这个策略呢,可以 JDK 中的注释:

Java版本的MethodAccessorImpl调用效率,比Native版本快20倍以上,然而Java版本加载时,比Native多消耗3至4倍资源,所以默认会调用Native版本,要是调用次数超过15次后,就会选择运行效率更高的Java版本。

那为何Native版本的运行效率不如Java版本高呢?从R大博客可知,这是HotSpot的优化方式所带来的性能特性,也是许多虚拟机的共同之处,跨越native边界会对优化产生阻碍作用,它如同黑箱一般,使虚拟机难以进行分析并将其内联,所以运行时间一长,反而是托管版本的代码更快些 。

调用 MethodAccessor 的 invoke 方法,实现方法的调用。

生成 MethodAccessor 之后,调用其 invoke 方法,进行最终的反射调用。

这里我们对Java版本的MethodAccessorImpl进行一个简单的分析,Native版本暂时不进行分析。

前面我们提到过,MethodAccessorImpl 是通过 MethodAccessorGenerator#generate 生成动态字节码的,之后动态加载到 JVM 中。

生成invoke方法字节码的是MethodAccessorGenerator#emitInvoke 。其中 ,MethodAccessorGenerator#emitInvoke负责生成该字节码 。

我们看其中校验参数的一小段代码:

通过上面的注释以及字节码,我们能够看到,生成的invoke方法,会对传入的参数进行校验,在这个过程中会涉及到unboxing操作。

到此,基本上 Java 方法反射的原理就介绍完了。

三、Java 反射效率低的原因

了解了反射的原理以后,我们来分析一下反射效率低的原因。

Method#invoke方法会进行对参数的封装操作,还会进行对参数的解封操作。

我们能够看到,invoke方法的参数属于Object[]类型,这意味着,要是方法参数为简单类型,那么在此需转化成Object类型,比如long,在javac compile时使用了Long.valueOf()进行转型,如此便大量生成了Long的Object,并且传入的参数是Object[]数值,此时还需要额外封装object数组。

在上面 MethodAccessorGenerator#emitInvoke 方法里,我们看到在生成字节码时,会将参数数组拆解开来,把参数恢复到未被 Object[] 包装前的样子,同时还要对参数进行校验,这里涉及到了解封操作。

因此,在进行反射调用时,由于存在封装和解封的操作,从而产生了额外的、不必要的内存浪费。当调用次数达到一定数量时,这还会导致 GC 。

2. 需要检查方法可见性

经过上面的源码分析,我们能够发现,反射时每次进行调用,都一定要检查方法的可见性,这一检查操作在Method.invoke里

3. 需要校验参数

反射时也必须检查每个实际参数与形式参数的类型匹配性(在

在 NativeMethodAccessorImpl.invoke0 之中,或者在生成的 Java 版 MethodAccessor.invoke 之中);

4. 反射方法难以内联

Method#invoke如同独木桥,各处反射调用都要通过它,在调用点收集到的类型信息会变得杂乱,影响内联程序判断,致使Method.invoke()自身难以被内联到调用方。参见 …

5. JIT 无法优化

在 JavaDoc 中提到:

因为反射涉及动态解析的类型,所以某些Java虚拟机优化无法执行。因此,反射操作的性能比非反射操作慢,并且在对性能敏感的应用程序中频繁调用的代码段中应避免使用反射操作。

因为反射涉及到动态加载的类型,所以无法进行优化。

总结

上面就是对反射原理和反射效率低的一些分析。

你可能想看:
« 上一篇
下一篇 »

相关推荐

java中什么是冒泡,冒泡算法

2026年09月06日

27阅读

java中冒号,java双冒号

2026年09月06日

26阅读

java中的->,java中的是什么意思

2026年09月06日

24阅读

java中的super用法,Java中super的说明

2026年09月06日

28阅读

java中反射的应用,java中反射的应用场景

2026年09月06日

26阅读

java中开关的定义,Java关键字有哪些

2026年09月06日

26阅读