反射实现1-调用本地方法
例:
1 // v0版本 2 import java.lang.reflect.Method; 3 4 public class Test { 5 public static void target(int i) { 6 new Exception("#" + i).printStackTrace(); 7 } 8 9 public static void main(String[] args) throws Exception { 10 Class<?> klass = Class.forName("Test"); 11 Method method = klass.getMethod("target", int.class); 12 method.invoke(null, 0); 13 } 14 }
java Test结果:
1 $ java Test 2 java.lang.Exception: #0 3 at Test.target(Test.java:6) // 4.最后到达目标方法 4 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) 5 at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) // 3.再然后进入本地实现(NativeMethodAccessorImpl) 6 at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) // 2.然后进入委派实现(DelegatingMethodAccessorImpl) 7 at java.lang.reflect.Method.invoke(Method.java:498) // 1.先是调用了 Method.invoke 8 at Test.main(Test.java:12)
反射实现2-动态生成字节码
问:为什么反射调用还要采取委派实现作为中间层?直接交给本地实现不可以么?
答:其实,Java 的反射调用机制还设立了另一种动态生成字节码的实现(下称动态实现),直接使用 invoke 指令来调用目标方法。之所以采用委派实现,便是为了能够在本地实现以及动态实现中切换。
1 // 动态实现的伪代码,这里只列举了关键的调用逻辑,其实它还包括调用者检测、参数检测的字节码。 2 package jdk.internal.reflect; 3 4 public class GeneratedMethodAccessor1 extends ... { 5 @Overrides 6 public Object invoke(Object obj, Object[] args) throws ... { 7 Test.target((int) args[0]); 8 return null; 9 } 10 }
动态实现和本地实现相比,其运行效率要快上 20 倍 [2] 。这是因为动态实现无需经过 Java 到 C++ 再到 Java 的切换,但由于生成字节码十分耗时,仅调用一次的话,反而是本地实现要快上 3 到 4 倍 [3]。
考虑到许多反射调用仅会执行一次,Java 虚拟机设置了一个阈值 15(可以通过 -Dsun.reflect.inflationThreshold= 来调整),当某个反射调用的调用次数在 15 之下时,采用本地实现;当达到 15 时,便开始动态生成字节码,并将委派实现的委派对象切换至动态实现,这个过程我们称之为 Inflation(翻译:膨胀、通货膨胀)。
反射调用的 Inflation 机制是可以通过参数(-Dsun.reflect.noInflation=true)来关闭的。这样一来,在反射调用一开始便会直接生成动态实现,而不会使用委派实现或者本地实现
例子:
1 // v1版本 2 import java.lang.reflect.Method; 3 4 public class Test { 5 public static void target(int i) { 6 new Exception("#" + i).printStackTrace(); 7 } 8 9 public static void main(String[] args) throws Exception { 10 Class<?> klass = Class.forName("Test"); 11 Method method = klass.getMethod("target", int.class); 12 for (int i = 0; i < 20; i++) { 13 method.invoke(null, i); 14 } 15 } 16 }
结果:
1 $ java -verbose:class Test // 使用-verbose:class打印加载的类 2 [Opened /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib/rt.jar] 3 [Loaded java.lang.Object from /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib/rt.jar] 4 [Loaded java.io.Serializable from /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib/rt.jar] 5 ...... 6 [Loaded java.lang.Throwable$PrintStreamOrWriter from /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib/rt.jar] 7 [Loaded java.lang.Throwable$WrappedPrintStream from /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib/rt.jar] 8 [Loaded java.util.IdentityHashMap from /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib/rt.jar] 9 [Loaded java.util.IdentityHashMap$KeySet from /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib/rt.jar] 10 java.lang.Exception: #0 11 at Test.target(Test.java:6) 12 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) 13 at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) 14 at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) 15 at java.lang.reflect.Method.invoke(Method.java:498) 16 at Test.main(Test.java:13) 17 oke(Method.java:498) 18 ...... 19 java.lang.Exception: #14 20 at Test.target(Test.java:6) 21 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) 22 at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) 23 at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) 24 at java.lang.reflect.Method.invoke(Method.java:498) 25 at Test.main(Test.java:13) 26 [Loaded sun.reflect.ClassFileConstants from /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib/rt.jar] 27 [Loaded sun.reflect.AccessorGenerator from /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib/rt.jar] 28 [Loaded sun.reflect.MethodAccessorGenerator from /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib/rt.jar] 29 [Loaded sun.reflect.ByteVectorFactory from /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib/rt.jar] 30 [Loaded sun.reflect.ByteVector from /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib/rt.jar] 31 [Loaded sun.reflect.ByteVectorImpl from /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib/rt.jar] 32 [Loaded sun.reflect.ClassFileAssembler from /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib/rt.jar] 33 [Loaded sun.reflect.UTF8 from /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib/rt.jar] 34 [Loaded sun.reflect.Label from /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib/rt.jar] 35 [Loaded sun.reflect.Label$PatchInfo from /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib/rt.jar] 36 [Loaded java.util.ArrayList$Itr from /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib/rt.jar] 37 [Loaded sun.reflect.MethodAccessorGenerator$1 from /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib/rt.jar] 38 [Loaded sun.reflect.ClassDefiner from /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib/rt.jar] 39 [Loaded sun.reflect.ClassDefiner$1 from /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib/rt.jar] 40 [Loaded sun.reflect.GeneratedMethodAccessor1 from __JVM_DefineClass__] 41 java.lang.Exception: #15 42 at Test.target(Test.java:6) 43 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) 44 at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) // 本地方法调用 45 at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) 46 at java.lang.reflect.Method.invoke(Method.java:498) 47 at Test.main(Test.java:13) 48 [Loaded java.util.concurrent.ConcurrentHashMap$ForwardingNode from /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib/rt.jar] 49 java.lang.Exception: #16 50 at Test.target(Test.java:6) 51 at sun.reflect.GeneratedMethodAccessor1.invoke(Unknown Source) // 从第16次开始,切换到字节码调用(即动态实现) 52 at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) 53 at java.lang.reflect.Method.invoke(Method.java:498) 54 at Test.main(Test.java:13) 55 ...... 56 java.lang.Exception: #19 57 at Test.target(Test.java:6) 58 at sun.reflect.GeneratedMethodAccessor1.invoke(Unknown Source) 59 at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) 60 at java.lang.reflect.Method.invoke(Method.java:498) 61 at Test.main(Test.java:13) 62 [Loaded java.lang.Shutdown from /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib/rt.jar] 63 [Loaded java.lang.Shutdown$Lock from /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib/rt.jar]
反射性能开销
下面,我们便来拆解反射调用的性能开销。
在刚才的例子中,我们先后进行了 Class.forName,Class.getMethod 以及 Method.invoke 三个操作。其中,Class.forName 会调用本地方法(Java和C++的相互转换,非常耗时),Class.getMethod 则会遍历该类的公有方法。如果没有匹配到,它还将遍历父类的公有方法。可想而知,这两个操作都非常费时。
值得注意的是,以 getMethod 为代表的查找方法操作,会返回查找得到结果的一份拷贝。因此,我们应当避免在热点代码中使用返回 Method 数组的 getMethods 或者 getDeclaredMethods 方法,以减少不必要的堆空间消耗。
在实践中,我们往往会在应用程序中缓存 Class.forName 和 Class.getMethod 的结果。因此,下面我就只关注反射调用本身的性能开销。
例:
1 // v2版本 2 mport java.lang.reflect.Method; 3 4 public class Test { 5 public static void target(int i) { 6 // 空方法 7 } 8 9 public static void main(String[] args) throws Exception { 10 Class<?> klass = Class.forName("Test"); 11 Method method = klass.getMethod("target", int.class); 12 13 long current = System.currentTimeMillis(); 14 for (int i = 1; i <= 2_000_000_000; i++) { 15 if (i % 100_000_000 == 0) { 16 long temp = System.currentTimeMillis(); 17 System.out.println(temp - current); 18 current = temp; 19 } 20 21 method.invoke(null, 128); 22 } 23 } 24 }
反射调用之前,字节码都做了什么
59: aload_2 // 加载Method对象 60: aconst_null // 反射调用的第一个参数null 61: iconst_1 62: anewarray Object // 生成一个长度为1的Object数组 65: dup 66: iconst_0 67: sipush 128 70: invokestatic Integer.valueOf // 将128自动装箱成Integer 73: aastore // 存入Object数组中 74: invokevirtual Method.invoke // 反射调用
这里我截取了循环中反射调用编译而成的字节码。
可以看到,这段字节码除了反射调用外,还额外做了两个操作。
第一,由于 Method.invoke 是一个变长参数方法,在字节码层面它的最后一个参数会是 Object 数组(感兴趣的同学私下可以用 javap 查看)。Java 编译器会在方法调用处生成一个长度为传入参数数量的 Object 数组,并将传入参数一一存储进该数组中。
第二,由于 Object 数组不能存储基本类型,Java 编译器会对传入的基本类型参数进行自动装箱。
这两个操作除了带来性能开销外,还可能占用堆内存,使得 GC 更加频繁。(如果你感兴趣的话,可以用虚拟机参数 -XX:+PrintGC 试试。)
另外,有些情况下 反射调用能够变得非常快(和非反射没什么区别),主要是因为即时编译器中的方法内联。在关闭了 Inflation 的情况下,内联的瓶颈在于 Method.invoke 方法中对 MethodAccessor.invoke 方法的调用。
我会在后面的文章中介绍方法内联的具体实现,这里先说个结论:在生产环境中,我们往往拥有多个不同的反射调用,对应多个 GeneratedMethodAccessor,也就是动态实现。由于 Java 虚拟机的关于上述调用点的类型 profile(注:对于 invokevirtual 或者 invokeinterface,Java 虚拟机会记录下调用者的具体类型,我们称之为类型 profile)无法同时记录这么多个类,因此可能造成所测试的反射调用没有被内联的情况。
例:
1 // v5版本 2 import java.lang.reflect.Method; 3 4 public class Test { 5 public static void target(int i) { 6 // 空方法 7 } 8 9 public static void main(String[] args) throws Exception { 10 Class<?> klass = Class.forName("Test"); 11 Method method = klass.getMethod("target", int.class); 12 method.setAccessible(true); // 关闭权限检查 13 polluteProfile(); // 这个方法里面放射调用另外两个方法,导致上述的target方法的内联失效 14 15 long current = System.currentTimeMillis(); 16 for (int i = 1; i <= 2_000_000_000; i++) { 17 if (i % 100_000_000 == 0) { 18 long temp = System.currentTimeMillis(); 19 System.out.println(temp - current); 20 current = temp; 21 } 22 23 method.invoke(null, 128); 24 } 25 } 26 27 public static void polluteProfile() throws Exception { 28 Method method1 = Test.class.getMethod("target1", int.class); 29 Method method2 = Test.class.getMethod("target2", int.class); 30 for (int i = 0; i < 2000; i++) { 31 method1.invoke(null, 0); 32 method2.invoke(null, 0); 33 } 34 } 35 public static void target1(int i) { } 36 public static void target2(int i) { } 37 }
在上面的 v5 版本中,我在测试循环之前调用了 polluteProfile 的方法。该方法将反射调用另外两个方法,并且循环上 2000 遍。而测试循环则保持不变。测得的结果约为基准的 6.7 倍。也就是说,只要误扰了 Method.invoke 方法的类型 profile,性能开销便会从 1.3 倍上升至 6.7 倍。
今天的实践环节,你可以将最后一段代码中 polluteProfile 方法的两个 Method 对象,都改成获取名字为“target”的方法。请问这两个获得的 Method 对象是同一个吗(==)?他们 equal 吗(.equals(…))?对我们的运行结果有什么影响?
解答:https://blog.csdn.net/ti_an_di/article/details/82049230
显然,我们是不同的引用,但它们指向的值是相等的,即method1==method2 为false,method1.equals(method2)为true。结果就是又会恢复到以前的运行速率,因为类型profile不会被target1和target2占用了。
附录:反射 API 简介
通常来说,使用反射 API 的第一步便是获取 Class 对象。在 Java 中常见的有这么三种。
- 使用静态方法 Class.forName 来获取。
- 调用对象的 getClass() 方法。
- 直接用类名 +“.class”访问。对于基本类型来说,它们的包装类型(wrapper classes)拥有一个名为“TYPE”的 final 静态字段,指向该基本类型对应的 Class 对象。
例如,Integer.TYPE 指向 int.class。对于数组类型来说,可以使用类名 +“[ ].class”来访问,如 int[ ].class。
除此之外,Class 类和 java.lang.reflect 包中还提供了许多返回 Class 对象的方法。例如,对于数组类的 Class 对象,调用 Class.getComponentType() 方法可以获得数组元素的类型。
一旦得到了 Class 对象,我们便可以正式地使用反射功能了。下面我列举了较为常用的几项。
- 使用 newInstance() 来生成一个该类的实例。它要求该类中拥有一个无参数的构造器。
- 使用 isInstance(Object) 来判断一个对象是否该类的实例,语法上等同于 instanceof 关键字(JIT 优化时会有差别,我会在本专栏的第二部分详细介绍)。
- 使用 Array.newInstance(Class,int) 来构造该类型的数组。
- 使用 getFields()/getConstructors()/getMethods() 来访问该类的成员。除了这三个之外,Class 类还提供了许多其他方法,详见[4]。需要注意的是,方法名中带 Declared 的不会返回父类的成员,但是会返回私有成员;而不带 Declared 的则相反。
当获得了类成员之后,我们可以进一步做如下操作。
- 使用 Constructor/Field/Method.setAccessible(true) 来绕开 Java 语言的访问限制。
- 使用 Constructor.newInstance(Object[]) 来生成该类的实例。
- 使用 Field.get/set(Object) 来访问字段的值。
- 使用 Method.invoke(Object, Object[]) 来调用方法。
有关反射 API 的其他用法,可以参考 reflect 包的 javadoc [5] ,这里就不详细展开了。