目录
- JVM是如何实现反射的
- 反射的性能开销体现在哪里
- 如何优化反射性能开销
1. JVM是如何实现反射的?
反射是Java语言中的一种强大功能,它允许程序在运行时动态地获取类的信息以及操作对象。下面是一个简单的示例,演示了如何使用反射调用方法:
public class Solution {
public static void show(int i) {
new Exception("#" + i).printStackTrace();
}
public static void main(String[] args) throws Exception {
Class<?> clazz = Class.forName("Solution");
Method method = clazz.getMethod("show", int.class);
method.invoke(null, 0);
}
}
在上述代码中,我们使用Method.invoke
来执行反射方法调用,并通过打印show
方法的栈轨迹来观察调用的类。输出如下:
java.lang.Exception: #0
at Solution.show(Solution.java:15)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at Solution.main(Solution.java:21)
首先我们看一下Method.invoke
的实现:
public final class Method extends Executable {
...
public Object invoke(Object obj, Object... args) throws ... {
... // 权限检查
MethodAccessor ma = methodAccessor;
if (ma == null) {
ma = acquireMethodAccessor();
}
return ma.invoke(obj, args);
}
}
可以看到,实际上它是委派给了MethodAccessor
来处理。MethodAccessor
是一个接口,具有两个具体实现:一个是通过本地方法(NativeMethodAccessorImpl
)来实现的,称为本地实现;另一个是使用了委派模式(DelegatingMethodAccessorImpl
),称为委派实现。
MethodAccessor实例的创建
MethodAccessor
实例是在ReflectionFactory
中创建的:
public class ReflectionFactory {
...
public MethodAccessor newMethodAccessor(Method method) {
checkInitted();
...
if (noInflation && !ReflectUtil.isVMAnonymousClass(method.getDeclaringClass())) {
return new MethodAccessorGenerator().generateMethod(
method.getDeclaringClass(),
method.getName(),
method.getParameterTypes(),
method.getReturnType(),
method.getExceptionTypes(),
method.getModifiers());
} else {
NativeMethodAccessorImpl acc = new NativeMethodAccessorImpl(method);
DelegatingMethodAccessorImpl res = new DelegatingMethodAccessorImpl(acc);
acc.setParent(res);
return res;
}
}
}
在第一次调用反射时,noInflation
为false
,这时会生成一个委派实现,而委派实现的具体实现便是一个本地实现。反射调用在进入Java虚拟机内部后,实际是调用目标方法的具体地址。
动态生成字节码的实现
Java的反射调用机制还设立了另一种动态生成字节码的实现(简称动态实现),直接使用invoke
指令来调用目标方法。动态实现的运行效率要快20倍,因为它避免了Java到C++再到Java的切换,但由于生成字节码非常耗时,仅调用一次的话,本地实现反而要快3到4倍。
Java虚拟机设置了一个阈值15,当某个反射调用的次数在15之下时,采用本地实现;当达到15时,开始动态生成字节码并切换至动态实现,这个过程称为Inflation。
Inflation机制
NativeMethodAccessorImpl
中每次invoke
方法被调用时,都会增加一次计时器,并判断是否超过阈值,超过后调用MethodAccessorGenerator.generateMethod()
生成Java版的MethodAccessor
实现类,并改变DelegatingMethodAccessorImpl
所引用的MethodAccessor
为Java版。
小结
在默认情况下,方法的反射调用为委派实现,调用超过15次后,委派实现便会切换至动态实现,该动态实现的字节码是自动生成的,将直接使用invoke
指令调用目标方法。可以通过参数-Dsun.reflect.noInflation=true
来关闭Inflation机制,直接生成动态实现。
2. 反射的性能开销体现在哪里?
在上面的例子中,我们使用了Class.forName
、Class.getMethod
以及Method.invoke
三个操作。其中Class.forName
调用本地方法,Class.getMethod
则遍历该类的公有方法,如果没有匹配到,还将遍历父类的公有方法。这两个操作都是非常耗时的。
需要注意的是,以getMethod
为代表的查找方法操作,会返回查找结果的一份拷贝。因此,应避免在热点代码中使用返回Method
数组的getMethods
或getDeclaredMethods
方法,以减少不必要的堆空间消耗。
在实践中,通常会在应用程序中缓存Class.forName
和Class.getMethod
的结果,因此下面我们只关注反射调用本身的性能开销。
public class ReflectDemo {
public void doSth(int i) {}
public static void main(String[] args) throws Exception {
Class<?> clazz = ReflectDemo.class;
Constructor constructor = clazz.getConstructor();
Object object = constructor.newInstance();
Method method = clazz.getMethod("doSth", int.class);
ReflectDemo demo = new ReflectDemo();
long current = System.currentTimeMillis();
for (int i = 1; i <= 2_000_000_000; i++) {
if (i % 100_000_000 == 0) {
long temp = System.currentTimeMillis();
System.out.println(temp - current);
current = temp;
}
// 直接调用
demo.doSth(2333);
// 反射调用
// method.invoke(object, 2333);
}
}
}
根据测试结果,一亿次的直接调用耗时为91.6ms,而反射调用耗时281.6ms,约为基准值的3.07倍。
性能开销来源
反射调用的性能开销主要来自以下几个方面:
- 方法表查找
- 构建
Object
数组以及可能存在的自动装拆箱操作 - 运行时权限检查
- 可能没有方法内联/逃逸分析
3. 如何优化反射性能开销?
反射性能优化策略:
- 尽量避免反射调用虚方法:虚方法调用的性能开销更大。
-
关闭运行时权限检查:使用
setAccessible(true)
可以提升性能。 -
扩大基本数据类型对应的包装类缓存:可通过参数
-Djava.lang.Integer.IntegerCache.high=128
来实现。 - 关闭Inflation机制:直接动态生成字节码。
-
提高JVM关于每个调用能够记录的类型数目:通过虚拟机参数
-XX:TypeProfileWidth
设置更大的值。
通过上述方法,可以有效减少反射调用的性能开销,提高程序的整体性能。在实际开发中,根据具体场景灵活应用这些优化策略,可以显著提升反射操作的效率。