反射是java中一个相当重要的特性,它的应用十分广泛。譬如java调试器,在调试过程中枚举对象所有字段的值。在Web开发中,各种可配置的框架。为了框架的扩展性,基本上都是使用反射机制。譬如Spring的IOC容器。当然,这么方便的东西往往是牺牲另一部分的特性锁带来的,而反射牺牲的则是代码执行的性能。下面就来简单分析下反射的机制
反射的实现
首先看下Method类的源码
public final class Method extends Executable { //java8
@CallerSensitive
public Object invoke(Object var1, Object... var2) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
if (!this.override && !Reflection.quickCheckMemberAccess(this.clazz, this.modifiers)) {
Class var3 = Reflection.getCallerClass();
this.checkAccess(var3, this.clazz, var1, this.modifiers);
}
MethodAccessor var4 = this.methodAccessor;
if (var4 == null) {
var4 = this.acquireMethodAccessor();
}
return var4.invoke(var1, var2);
}
省略了其他的东西,只看invoke方法。可以看到,方法具体的实现是委托给了MethodAccessor。MethodAccessor是一个接口,有三个子类,子类中有一个抽象类MethodAccessorImpl,两个具体实现DelegatingMethodAccessorImpl和NativeMethodAccessorImpl。在Method实例化的时候,就会生成一个委托,默认的委托则是NativeMethodAccessorImpl,即本地方法实现。在东西很容易理解,在进入jvm之后,就可以得到这个本地方法的地址,然后把参数传过去直接调用你就行了。
贴个代码
public class ReflectTest {
public static void test(int i) {
new Exception(i + "").printStackTrace();
}
public static void main(String[] args) throws Exception {
Class<?> aClass = Class.forName("ReflectTest");
Method test = aClass.getMethod("test", int.class);
test.invoke(null, 1);
}
}
--------------输出
java.lang.Exception: 1
at com.lv.ddpay.test.ReflectTest.test(ReflectTest.java:13)
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 com.lv.ddpay.test.ReflectTest.main(ReflectTest.java:19)
在test方法打印了一个调用栈,可以看到,首先进入Method.invoke方法,然后进入委托,委托在调用本地方法实现,然后在进入具体的方法。
在这个流程中,有疑问的应该就是这个委托了,问什么要用这个委托呢,直接使用本地方法不是更快吗?那是因为jvm还有一种反射实现,是直接生产字节码,使用invoke指令直接调用的方式。之所以使用委托,是为了在本地方法和动态字节码的方式之间做切换。下面,在text.invoke上加个循环
public class ReflectTest {
public static void test(int i) {
new Exception(i + "").printStackTrace();
}
public static void main(String[] args) throws Exception {
Class<?> aClass = Class.forName("com.lv.ddpay.test.ReflectTest");
Method test = aClass.getMethod("test", int.class);
for (int i = 0; i < 20; i++) {
long l = System.currentTimeMillis();
test.invoke(null, i);
System.out.println(System.currentTimeMillis() - l );
}
}
}
------输出-------java8
java.lang.Exception: 15
at com.lv.ddpay.test.ReflectTest.test(ReflectTest.java:13)
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 com.lv.ddpay.test.ReflectTest.main(ReflectTest.java:21)
java.lang.Exception: 16
at com.lv.ddpay.test.ReflectTest.test(ReflectTest.java:13)
at sun.reflect.GeneratedMethodAccessor1.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at com.lv.ddpay.test.ReflectTest.main(ReflectTest.java:21)
从输出可以看到,java8在第17次输出的时候,方法调用栈改变了使用的是DelegatingMethodAccessorImpl,然后在进入GeneratedMethodAccessor1,GeneratedMethodAccessor1就是动态生成的字节码。动态字节码比本地实现快了差不多20倍,因为这里不需要有java-C++-java的切换,但生成字节码比较耗时,如果仅调用一次的话,本地实现比动态字节码快。
jvm有个参数-Dsun.reflect.inflationThresold? 来设置使用字节码的阈值。当达到了这个阈值的时候,jvm就生成字节码,采用动态调用。还有个参数-Dsun.reflect.noInflation 来设置当使用反射的时候,是不是直接使用动态字节码的方式。
反射的开销
从刚才的代码来看,主要有三个操作Class.forName、class.getMethod、method.invoke。Class.forName调用的是本地方法,class.getMethod会遍历该类所有的公有方法,没找到还会遍历父类的公有方法。可想而知,这两个方法的调用开销都不小。
但在应用程序里,大部分使用Class.forName、class.getMethod的结果都会缓存下来,所以开销主要是看method.invoke方法。
27: aload_2 //加载invoke方法
28: aconst_null //第一个参数 null
29: iconst_1
30: anewarray #18 //生成长度为1的object数组 // class java/lang/Object
33: dup
34: iconst_0
35: iconst_1
36: invokestatic #19
//int参数自动装箱
// Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
39: aastore //把装箱后的参数放入数组
40: invokevirtual #20 // Method java/lang/reflect/Method.i
nvoke:(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;
把上面的代码编译一下,在用javap解析一下字节码? ?method.invoke(null,1)编译后就得到上面的代码。可以看到,除了直接滴啊用invoke方法外,还有两个额外的操作,
invoke方法的参数是一个可变长度,在字节码层面上是一个Object数组,所以java编译器会在方法调用的地方生产一个Object数组,并把参数存储进该数组
Object数组不能存储基本类型,所以java编译器会对基本类型进行自动装箱处理。