在查看一些资料的时候, 发现到大家都说 Java 的反射效率低, 那么到底是为什么呢?
本文主要来探索这个问题, 本文基于的环境为
java version "1.8.0_221" Java(TM) SE Runtime Environment (build 1.8.0_221-b11) Java HotSpot(TM) 64-Bit Server VM (build 25.221-b11, mixed mode)
主要参考:
Java 反射效率低主要原因是
Method#invoke 方法会对参数做封装和解封操作
需要检查方法可见性
需要校验参数
反射方法难以内联
JIT 无法优化
先看示例代码如下,
public class RefA {
public void foo(String str) {
System.out.println("str: " + str);
}
}
public class RefTest {
public static void main(String[] args){
try {
Class<?> clz = Class.forName("com.joe.test.RefA");
Object o = clz.newInstance();
Method m = clz.getMethod("foo", String.class);
for (int i = 0; i < 16; i++) {
m.invoke(o, Integer.toString(i));
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
上面是很简单的一段反射的使用, RefTest 类上不会有对类 RefA 的符号依赖——也就是说在加载并初始化RefTest 类时不需要关心类 RefA 的存在与否, 而是等到main()方法执行到调用Class.forName()时才试图对类 RefA 做动态加载; 这里用的是一个参数版的forName(), 也就是使用当前方法所在类的ClassLoader来加载, 并且初始化新加载的类
编译上述代码, 并在执行 RefTest 时加入 -XX:+TraceClassLoading 参数
截取关键log如下
[Loaded com.joe.test.RefTest from file:/E:/Projects/IdeaPorject/review/basic/target/classes/]
[Loaded sun.launcher.LauncherHelper$FXHelper from E:\Dev\Java\jdk1.8.0_221\jre\lib\rt.jar]
[Loaded java.lang.Class$MethodArray from E:\Dev\Java\jdk1.8.0_221\jre\lib\rt.jar]
[Loaded java.lang.Void from E:\Dev\Java\jdk1.8.0_221\jre\lib\rt.jar]
[Loaded com.joe.test.A from file:/E:/Projects/IdeaPorject/review/basic/target/classes/]
str: 0
str: 1
str: 2
str: 3
str: 4
str: 5
str: 6
str: 7
str: 8
str: 9
str: 10
str: 11
str: 12
str: 13
str: 14
[Loaded sun.reflect.ClassFileConstants from E:\Dev\Java\jdk1.8.0_221\jre\lib\rt.jar]
[Loaded sun.reflect.AccessorGenerator from E:\Dev\Java\jdk1.8.0_221\jre\lib\rt.jar]
[Loaded sun.reflect.MethodAccessorGenerator from E:\Dev\Java\jdk1.8.0_221\jre\lib\rt.jar]
[Loaded sun.reflect.ByteVectorFactory from E:\Dev\Java\jdk1.8.0_221\jre\lib\rt.jar]
[Loaded sun.reflect.ByteVector from E:\Dev\Java\jdk1.8.0_221\jre\lib\rt.jar]
[Loaded sun.reflect.ByteVectorImpl from E:\Dev\Java\jdk1.8.0_221\jre\lib\rt.jar]
[Loaded sun.reflect.ClassFileAssembler from E:\Dev\Java\jdk1.8.0_221\jre\lib\rt.jar]
[Loaded sun.reflect.UTF8 from E:\Dev\Java\jdk1.8.0_221\jre\lib\rt.jar]
[Loaded sun.reflect.Label from E:\Dev\Java\jdk1.8.0_221\jre\lib\rt.jar]
[Loaded sun.reflect.Label$PatchInfo from E:\Dev\Java\jdk1.8.0_221\jre\lib\rt.jar]
[Loaded sun.reflect.MethodAccessorGenerator$1 from E:\Dev\Java\jdk1.8.0_221\jre\lib\rt.jar]
[Loaded sun.reflect.ClassDefiner from E:\Dev\Java\jdk1.8.0_221\jre\lib\rt.jar]
[Loaded sun.reflect.ClassDefiner$1 from E:\Dev\Java\jdk1.8.0_221\jre\lib\rt.jar]
[Loaded sun.reflect.GeneratedMethodAccessor1 from __JVM_DefineClass__]
str: 15
在调用反射时, 首先会创建 Class 对象, 然后获取其 Method 对象, 调用 invoke 方法 获取反射方法时, 有两个方法, getMethod 和 getDeclaredMethod, 我们就从这两个方法开始, 一步步看下反射的原理
@CallerSensitive
public Method getMethod(String name, Class<?>... parameterTypes)
throws NoSuchMethodException, SecurityException {
// 检查方法权限
checkMemberAccess(Member.PUBLIC, Reflection.getCallerClass(), true);
// 获取方法
Method method = getMethod0(name, parameterTypes, true);
if (method == null) {
throw new NoSuchMethodException(getName() + "." + name + argumentTypesToString(parameterTypes));
}
return method;
}
@CallerSensitive
public Method getDeclaredMethod(String name, Class<?>... parameterTypes)
throws NoSuchMethodException, SecurityException {
// 检查方法权限
checkMemberAccess(Member.DECLARED, Reflection.getCallerClass(), true);
// 获取方法
Method method = searchMethods(privateGetDeclaredMethods(false), name, parameterTypes);
if (method == null) {
throw new NoSuchMethodException(getName() + "." + name + argumentTypesToString(parameterTypes));
}
return method;
}
以上, 可以看出, 获取方法的流程基本上就是首先检查方法权限, 是否可以访问, 然后获取方法对象, 返回其引用.
getMethod 和 getDeclaredMethod 的主要区别有:
getMethod中checkMemberAccess传入的是Member.PUBLIC, 而getDeclaredMethod传入的是Member.DECLARED/** * Identifies the set of all public members of a class or interface, * including inherited members. */ public static final int PUBLIC = 0; /** * Identifies the set of declared members of a class or interface. * Inherited members are not included. */ public static final int DECLARED = 1;通过注释明显看到, PUBLIC 包括继承的成员, DECLARED 不包括继承的成员
getMethod获取方法调用的是getMethod0, 而getDeclaredMethod调用的是searchMethods;privateGetDeclaredMethods是获取类自身定义的方法, 参数是boolean publicOnly, 表示是否只获取公共方法而
getMethod0会递归查找父类的方法, 其中会调用到privateGetDeclaredMethods方法
可以根据自己的需要选择使用合适的方法, 我们想要详细的分析反射的过程, 所以选择 getMethod
获取到方法以后, 通过 Method#invoke 调用方法
// java.lang.reflect.Method
@CallerSensitive
public Object invoke(Object obj, Object... args)
throws IllegalAccessException, IllegalArgumentException,InvocationTargetException {
if (!override) {
// 1. 检查权限
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, obj, modifiers);
}
}
// 2. 获取 MethodAccessor
MethodAccessor ma = methodAccessor; // read volatile
if (ma == null) {
// 创建 MethodAccessor
ma = acquireMethodAccessor();
}
// 3. 调用 MethodAccessor.invoke
return ma.invoke(obj, args);
}
// java.lang.reflect.Method
private MethodAccessor acquireMethodAccessor() {
// First check to see if one has been created yet, and take it
// if so
MethodAccessor tmp = null;
if (root != null) tmp = root.getMethodAccessor();
if (tmp != null) {
methodAccessor = tmp;
} else {
// Otherwise fabricate one and propagate it up to the root
tmp = reflectionFactory.newMethodAccessor(this);
setMethodAccessor(tmp);
}
return tmp;
}
可以看到, Method#invoke方法不是自己实现反射调用逻辑, 而是委托给 sun.reflect.MethodAccessor 来处理
每个实际的 Java 方法只有一个对应的 Method 对象作为 root. 这个 root 是不会暴露给用户的, 而是每次在通过反射获取 Method 对象时新创建 Method 对象把 root 包装起来再给用户. 在第一次调用一个实际 Java 方法对应得 Method 对象的 invoke() 方法之前, 实现调用逻辑的 MethodAccessor 对象还没创建; 等第一次调用时才新创建 MethodAccessor 并更新给 root, 然后调用 MethodAccessor.invoke() 真正完成反射调用.
// sun.reflect
public interface MethodAccessor {
// var1 => obj, var2 => args
Object invoke(Object var1, Object[] var2)
throws IllegalArgumentException, InvocationTargetException;
}
是一个单接口方法, invoke 与 Method.invoke() 对应.
创建 MethodAccessor 实例的是 ReflectionFactory
// sun.reflect.ReflectionFactory
// 该类的注释来自于 R大
public class ReflectionFactory{
/** We have to defer full initialization of this class until after
the static initializer is run since java.lang.reflect.Method's
static initializer (more properly, that for
java.lang.reflect.AccessibleObject) causes this class's to be
run, before the system properties are set up. */
private static void checkInitted() {
if (!initted) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
// Tests to ensure the system properties table is fully
// initialized. This is needed because reflection code is
// called very early in the initialization process (before
// command-line arguments have been parsed and therefore
// these user-settable properties installed.) We assume that
// if System.out is non-null then the System class has been
// fully initialized and that the bulk of the startup code
// has been run.
public Void run() {
if (System.out == null) {
return null;
} else {
String var1 = System.getProperty("sun.reflect.noInflation");
if (var1 != null && var1.equals("true")) {
ReflectionFactory.noInflation = true;
}
var1 = System.getProperty("sun.reflect.inflationThreshold");
if (var1 != null) {
try {
ReflectionFactory.inflationThreshold = Integer.parseInt(var1);
} catch (NumberFormatException var3) {
throw new RuntimeException("Unable to parse property sun.reflect.inflationThreshold", var3);
}
}
ReflectionFactory.initted = true;
return null;
}
}
});
}
}
public MethodAccessor newMethodAccessor(Method var1) {
checkInitted();
if (noInflation && !ReflectUtil.isVMAnonymousClass(var1.getDeclaringClass())) {
return (new MethodAccessorGenerator()).generateMethod(var1.getDeclaringClass(), var1.getName(), var1.getParameterTypes(), var1.getReturnType(), var1.getExceptionTypes(), var1.getModifiers());
} else {
NativeMethodAccessorImpl var2 = new NativeMethodAccessorImpl(var1);
DelegatingMethodAccessorImpl var3 = new DelegatingMethodAccessorImpl(var2);
var2.setParent(var3);
return var3;
}
}
}
如注释所述, 实际的MethodAccessor实现有两个版本, 一个是Java实现的, 另一个是native code实现的.
一共有三种 MethodAccessor: MethodAccessorImpl, NativeMethodAccessorImpl, DelegatingMethodAccessorImpl
采用哪种 MethodAccessor 根据 noInflation 进行判断, noInflation 默认值为 false, 只有指定了 sun.reflect.noInflation 属性为 true, 才会采用 MethodAccessorImpl; 所以默认会调用 NativeMethodAccessorImpl
Java实现的版本在初始化时需要较多时间, 但长久来说性能较好; native版本正好相反, 启动时相对较快, 但运行时间长了之后速度就比不过Java版了.
这是HotSpot的优化方式带来的性能特性, 同时也是许多虚拟机的共同点:跨越native边界会对优化有阻碍作用, 它就像个黑箱一样让虚拟机难以分析也将其内联, 于是运行时间长了之后反而是托管版本的代码更快些.
为了权衡两个版本的性能, Sun 的 JDK 使用了“inflation”的技巧:让Java方法在被反射调用时, 开头若干次使用native版, 等反射调用次数超过阈值时则生成一个专用的 MethodAccessor 实现类, 生成其中的invoke()方法的字节码, 以后对该Java方法的反射调用就会使用Java版.
Sun的JDK是从1.4系开始采用这种优化的
开头若干次用到的是 DelegatingMethodAccessorImpl, 是一个间接层, 方便在 native 和 java 版的 MethodAccessor 之间实现切换
// sun.reflect.DelegatingMethodAccessorImpl
class DelegatingMethodAccessorImpl extends MethodAccessorImpl {
private MethodAccessorImpl delegate;
DelegatingMethodAccessorImpl(MethodAccessorImpl var1) {
this.setDelegate(var1);
}
public Object invoke(Object var1, Object[] var2) throws IllegalArgumentException, InvocationTargetException {
return this.delegate.invoke(var1, var2);
}
void setDelegate(MethodAccessorImpl var1) {
this.delegate = var1;
}
}
下面是 native 版的 methodAccessor
class NativeMethodAccessorImpl extends MethodAccessorImpl {
private final Method method;
private DelegatingMethodAccessorImpl parent;
private int numInvocations;
NativeMethodAccessorImpl(Method var1) {
this.method = var1;
}
public Object invoke(Object var1, Object[] var2) throws IllegalArgumentException, InvocationTargetException {
if (++this.numInvocations > ReflectionFactory.inflationThreshold() && !ReflectUtil.isVMAnonymousClass(this.method.getDeclaringClass())) {
MethodAccessorImpl var3 = (MethodAccessorImpl)(new MethodAccessorGenerator()).generateMethod(this.method.getDeclaringClass(), this.method.getName(), this.method.getParameterTypes(), this.method.getReturnType(), this.method.getExceptionTypes(), this.method.getModifiers());
this.parent.setDelegate(var3);
}
return invoke0(this.method, var1, var2);
}
void setParent(DelegatingMethodAccessorImpl var1) {
this.parent = var1;
}
private static native Object invoke0(Method var0, Object var1, Object[] var2);
}
每次 NativeMethodAccessorImpl.invoke() 方法被调用时, 都会增加一个调用次数计数器, 看超过阈值没有; 一旦超过, 则调用MethodAccessorGenerator.generateMethod() 来生成 Java 版的 MethodAccessor 的实现类, 并且改变DelegatingMethodAccessorImpl所引用的 MethodAccessor 为 Java 版. 后续经由 DelegatingMethodAccessorImpl.invoke() 调用到的就是 Java 版的实现了.
invoke0() 是 native 方法, 在HotSpot VM里是由JVM_InvokeMethod()函数所支持的, 在此就不深入了, 感兴趣的可以看 R 大的分析
### MethodAccessorGenerator
该类的基本工作是在内存里生成新的专用 Java 类, 并将其加载, 这里只贴出一个方法
// sun.reflect.MethodAccessorGenerator
private static synchronized String generateName(boolean var0, boolean var1) {
int var2;
if (var0) {
if (var1) {
var2 = ++serializationConstructorSymnum;
return "sun/reflect/GeneratedSerializationConstructorAccessor" + var2;
} else {
var2 = ++constructorSymnum;
return "sun/reflect/GeneratedConstructorAccessor" + var2;
}
} else {
var2 = ++methodSymnum;
return "sun/reflect/GeneratedMethodAccessor" + var2;
}
}
对本文开头的例子的A.foo(), 生成的Java版MethodAccessor大致如下:
package sun.reflect;
public class GeneratedMethodAccessor1 extends MethodAccessorImpl {
public GeneratedMethodAccessor1() {
super();
}
public Object invoke(Object obj, Object[] args)
throws IllegalArgumentException, InvocationTargetException {
// prepare the target and parameters
if (obj == null) throw new NullPointerException();
try {
A target = (A) obj;
if (args.length != 1) throw new IllegalArgumentException();
String arg0 = (String) args[0];
} catch (ClassCastException e) {
throw new IllegalArgumentException(e.toString());
} catch (NullPointerException e) {
throw new IllegalArgumentException(e.toString());
}
// make the invocation
try {
target.foo(arg0);
} catch (Throwable t) {
throw new InvocationTargetException(t);
}
}
}
就反射调用而言, 这个 invoke() 方法非常干净( 然而就“正常调用”而言这额外开销还是明显的).
注意到参数数组被拆开了, 把每个参数都恢复到原本没有被 Object[] 包装前的样子, 然后对目标方法做正常的 invokevirtual 调用. 由于在生成代码时已经循环遍历过参数类型的数组, 生成出来的代码里就不再包含循环了.
当该反射调用成为热点时, 它甚至可以被内联到靠近Method.invoke()的一侧, 大大降低了反射调用的开销. 而 native 版的反射调用则无法被有效内联, 因而调用开销无法随程序的运行而降低.
Sun 的 JDK 这种实现方式使得反射调用方法成本比以前降低了很多, 但 Method.invoke() 本身要用数组包装参数;
而且每次调用都必须检查方法的可见性(在 Method.invoke() 里), 也必须检查每个实际参数与形式参数的类型匹配性(在NativeMethodAccessorImpl.invoke0() 里或者生成的 Java 版 MethodAccessor.invoke() 里) ; 而且 Method.invoke() 就像是个独木桥一样, 各处的反射调用都要挤过去, 在调用点上收集到的类型信息就会很乱, 影响内联程序的判断, 使得Method.invoke() 自身难以被内联到调用方.
可以看到, invoke 方法的参数是 Object[] 类型, 也就是说, 如果方法参数是简单类型的话, 需要在此转化成 Object 类型, 例如 long ,在 javac compile 的时候 用了Long.valueOf() 转型, 也就大量了生成了Long 的 Object, 同时 传入的参数是Object[]数值,那还需要额外封装object数组. 而在上面 MethodAccessorGenerator#emitInvoke 方法里我们看到, 生成的字节码时, 会把参数数组拆解开来, 把参数恢复到没有被 Object[] 包装前的样子, 同时还要对参数做校验, 这里就涉及到了解封操作. 因此, 在反射调用的时候, 因为封装和解封, 产生了额外的不必要的内存浪费, 当调用次数达到一定量的时候, 还会导致 GC
通过上面的源码分析, 我们会发现, 反射时每次调用都必须检查方法的可见性(在 Method.invoke 里)
反射时也必须检查每个实际参数与形式参数的类型匹配性 (在NativeMethodAccessorImpl.invoke0 里或者生成的 Java 版 MethodAccessor.invoke 里)
Method#invoke 就像是个独木桥一样, 各处的反射调用都要挤过去, 在调用点上收集到的类型信息就会很乱, 影响内联程序的判断, 使得 Method.invoke() 自身难以被内联到调用方
在 JavaDoc 中提到:
Because reflection involves types that are dynamically resolved, certain Java virtual machine optimizations can not be performed. Consequently, reflective operations have slower performance than their non-reflective counterparts, and should be avoided in sections of code which are called frequently in performance-sensitive applications.
因为反射涉及动态解析的类型, 所以某些Java虚拟机优化无法执行. 因此, 反射操作的性能比非反射操作慢, 应该避免在对性能敏感的应用程序中频繁调用的代码段中使用反射操作
因为反射涉及到动态加载的类型, 所以无法进行优化

