JVM是如何处理反射的

 反射实现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 中常见的有这么三种。

  1. 使用静态方法 Class.forName 来获取。
  2. 调用对象的 getClass() 方法。
  3. 直接用类名 +“.class”访问。对于基本类型来说,它们的包装类型(wrapper classes)拥有一个名为“TYPE”的 final 静态字段,指向该基本类型对应的 Class 对象。

  例如,Integer.TYPE 指向 int.class。对于数组类型来说,可以使用类名 +“[ ].class”来访问,如 int[ ].class。

  除此之外,Class 类和 java.lang.reflect 包中还提供了许多返回 Class 对象的方法。例如,对于数组类的 Class 对象,调用 Class.getComponentType() 方法可以获得数组元素的类型。

一旦得到了 Class 对象,我们便可以正式地使用反射功能了。下面我列举了较为常用的几项。

  1. 使用 newInstance() 来生成一个该类的实例。它要求该类中拥有一个无参数的构造器。
  2. 使用 isInstance(Object) 来判断一个对象是否该类的实例,语法上等同于 instanceof 关键字(JIT 优化时会有差别,我会在本专栏的第二部分详细介绍)。
  3. 使用 Array.newInstance(Class,int) 来构造该类型的数组。
  4. 使用 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] ,这里就不详细展开了。

posted on 2020-12-16 11:17  gogoy  阅读(215)  评论(0编辑  收藏  举报

导航