From f7358643a88605cb713f985d82a1cf9f50f65e94 Mon Sep 17 00:00:00 2001 From: Omooo <869759698@qq.com> Date: Fri, 6 Sep 2019 16:41:40 +0800 Subject: [PATCH] update Reflect --- blogs/Java/深入理解反射.md | 465 ++++++++++++++++++++++++++++++- 1 file changed, 460 insertions(+), 5 deletions(-) diff --git a/blogs/Java/深入理解反射.md b/blogs/Java/深入理解反射.md index 499cfeb..ace9c15 100644 --- a/blogs/Java/深入理解反射.md +++ b/blogs/Java/深入理解反射.md @@ -4,11 +4,18 @@ ### 目录 -1. 反射的定义以及基本使用 -2. JVM 是如何实现反射的? -3. 反射性能开销体现在哪? -4. 如何优化反射性能开销? -5. 参考 +1. 前言 +2. 反射的定义以及基本使用 + * Class + * Member + * Array and Enumerated +3. JVM 是如何实现反射的? + * 委派实现 + * 本地实现 + * 动态实现 +4. 反射性能开销体现在哪? +5. 如何优化反射性能开销? +6. 参考 ### 反射的定义以及基本使用 @@ -19,9 +26,457 @@ 反射涉及的 API 分为三类:Class、Member(Field、Method、Constructor)、Array and Enumerated。详细的基本使用可以参考我以前写的文章 [反射基础使用](https://github.com/Omooo/Android-Notes/blob/master/blogs/Java/%E5%8F%8D%E5%B0%84.md),或者翻阅一下 [官方文档](https://docs.oracle.com/javase/tutorial/reflect/member/index.html)。 +这里只是简单的概括一下,已经熟悉的小伙伴可以跳过啦~~~ + +#### Class + +获取 Class 的五种方式: + +```java +public final class Main { + + enum E { + A, B + } + + public static void main(String[] args) throws Exception { + + // 1. Object.getClass() + Main main = new Main(); + Class clazzMain = main.getClass(); + + Class clazzEnum = E.A.getClass(); + + String[] strings = new String[20]; + Class clazzStrings = strings.getClass(); + + // 2. The .class Syntax + Class clazzMainSyntax = Main.class; + + Class clazzBoolean = boolean.class; + + Class clazzIntArray = int[][][].class; + + // 3. Class.forName() + Class clazzMainName = Class.forName("Main"); + + Class clazzDoubleArray = Class.forName("[D"); + Class clazzStringArray = Class.forName("[[Ljava.lang.String;"); + + // 4. TYPE Field for Primitive Type Wrappers + Class clazzDouble = Double.TYPE; + Class clazzVoid = Void.TYPE; + + // 5. Methods that Return Classes + Class clazzSuperclass = Main.class.getSuperclass(); + + Class[] clazzClasses = Main.class.getClasses(); + + Class[] clazzDeclaredClasses = Main.class.getDeclaredClasses(); + + Class clazzMainEnclose = Main.class.getEnclosingClass(); + } +} +``` + +#### Member + +Member 可能有的小伙伴没怎么见过,它只是一个接口,有三个我们最常见的三个实现类: + +1. Field +2. Method +3. Constructor + +这里多说一点,这三个实现类都有一个相同的父类,AccessibleObject,在访问私有属性时需要设置 setAccessible 关闭访问权限检查,就是出自这个类里面的方法。 + +由于篇幅限制,下面就只举例 Field 的使用,其他请参考官方文档: + +```java +public final class Main { + + private String s; + public static final int AGE = 18; + public float aFloat = 0f; + public boolean[][] booleans; + public List list = new ArrayList<>(); + public T t; + + public static void main(String[] args) throws Exception { + Class clazz = Class.forName("Main"); + Object object = clazz.getConstructor().newInstance(); + for (Field field : clazz.getDeclaredFields()) { + if (Modifier.isPrivate(field.getModifiers())) { + field.setAccessible(true); + } + System.out.println(field.getGenericType()); + System.out.println(field.get(object)); + System.out.println(Modifier.toString(field.getModifiers())); + System.out.println(); + } + } +} +``` + +#### Arrays and Enumerated Types + +这一小节讲的是两种特殊类型:数组和枚举。我是基本上从来没用过... + +所以就简单的熟悉一下 API 好了。 + +##### Arrays + +```java +public final class Main { + + public String[] strings = new String[]{"Demo", "Text"}; + + public static void main(String[] args) throws Exception { + Class clazz = Main.class; + Object object = clazz.getConstructor().newInstance(); + Field field = clazz.getField("strings"); + if (field.getType().isArray()){ + System.out.println(field.getName()); + System.out.println(field.getGenericType()); + String[] stringArray = (String[]) field.get(object); + for (String s : stringArray) { + System.out.println(s); + } + } + + // 创建一维数组 + Object array = Array.newInstance(int.class, 2); + Array.set(array, 0, 2333); + Array.set(array, 1, 2333333); + System.out.println(Array.get(array, 1)); + + // 创建二维数组 + // 1 2 + // 3 4 + Object matrix = Array.newInstance(int.class, 2, 2); + Object row1 = Array.get(matrix, 0); + Object row2 = Array.get(matrix, 1); + Array.set(row1, 0, 1); + Array.set(row1, 1, 2); + Array.set(row2, 0, 3); + Array.set(row2, 1, 4); + } +} +``` + +##### Enumerated Types + +```java +public final class Main { + + enum E { A, B,} + + public static void main(String[] args) throws Exception { + Class clazz = E.class; + if (clazz.isEnum()) { + + System.out.println(Arrays.asList(E.values())); + + for (Field field : clazz.getFields()) { + System.out.println(field.getName()); + System.out.println(field.getGenericType()); + } + + for (Constructor c : clazz.getDeclaredConstructors()) { + System.out.println(c.toGenericString()); + } + + for (Method method : clazz.getDeclaredMethods()) { + System.out.println(method.toGenericString()); + } + } + } +} +``` + ### JVM 是如何实现反射的? +首先我们看一个反射的例子: + +```java +public class Main { + + public static void show(int i) { + new Exception("#" + i).printStackTrace(); + } + + public static void main(String[] args) throws Exception { + Class clazz = Class.forName("Main"); + Method method = clazz.getMethod("show", int.class); + method.invoke(null, 0); + } + +} +``` + +在以上代码中,调用 Method.invoke 来执行反射调用,并且为了方便查看调用了哪些类,我们打印了 show 方法的栈轨迹,如下: + +```verilog +java.lang.Exception: #0 + at Main.show(Main.java:8) + at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) + at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) + at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) + at java.base/java.lang.reflect.Method.invoke(Method.java:566) + at Main.main(Main.java:14) +``` + +可以看到方法调用链是: + +``` +Method.invoke() --> DelegatingMethodAccessorImpl.invoke() --> NativeMethodAccessorImpl.invoke0() +``` + +这个时候就应该看看 Method.invoke 源码是如何实现的: + +```java +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 实例是在哪创建的呢? + +答案就在 ReflectionFactory 中: + +```java +public class ReflectionFactory { + + private static boolean initted = false; + private static final ReflectionFactory soleInstance = new ReflectionFactory(); + // Provides access to package-private mechanisms in java.lang.reflect + private static volatile LangReflectAccess langReflectAccess; + + /* Method for static class initializer , or null */ + private static volatile Method hasStaticInitializerMethod; + + // + // "Inflation" mechanism. Loading bytecodes to implement + // Method.invoke() and Constructor.newInstance() currently costs + // 3-4x more than an invocation via native code for the first + // invocation (though subsequent invocations have been benchmarked + // to be over 20x faster). Unfortunately this cost increases + // startup time for certain applications that use reflection + // intensively (but only once per class) to bootstrap themselves. + // To avoid this penalty we reuse the existing JVM entry points + // for the first few invocations of Methods and Constructors and + // then switch to the bytecode-based implementations. + // + // Package-private to be accessible to NativeMethodAccessorImpl + // and NativeConstructorAccessorImpl + private static boolean noInflation = false; + private static int inflationThreshold = 15; + + //... + public MethodAccessor newMethodAccessor(Method method) { + checkInitted(); + + if (Reflection.isCallerSensitive(method)) { + Method altMethod = findMethodForReflection(method); + if (altMethod != null) { + method = altMethod; + } + } + + // use the root Method that will not cache caller class + Method root = langReflectAccess.getRoot(method); + if (root != null) { + method = root; + } + + // 这里需要注意一点,VMAnonymousClass 并不是指匿名内部类 + // 它可以看做是 JVM 里面的一个模板机制 + 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 虚拟机内部之后,我们便拥有了 Method 实例所指向方法的具体地址。这时候,反射调用无非就是将传入的参数准备好,然后调用进去目标方法即可。 + +那为什么还需要委派实现作为中间层呢?直接交给本地实现不就可以了吗? + +其实,Java 的反射调用机制还设立了另一种动态生成字节码的实现(简称**动态实现**),直接使用 invoke 指令来调用目标方法,之所以采用委派实现,便是为了能够在本地实现以及动态实现中切换。 + +如注释所述,动态实现和本地实现相比,其运行效率要快上 20 倍。这是因为动态实现无需经过 Java 到 C++ 再到 Java 的切换,但由于生成字节码十分耗时,仅调用一次的话,反而是本地实现要快上 3 到 4 倍。 + +考虑到许多反射调用仅会执行一次,Java 虚拟机设置了一个阈值 15,当某个反射调用的调用次数在 15 之下时,采用本地实现;当达到 15 时,便开始动态生成字节码,并将委派实现的委派对象切换至动态实现,这个过程我们称之为 Inflation。 + +再看一下这个 inflationThreshold = 15 是在哪判断的呢?答案在本地实现中: + +```java +/** Used only for the first few invocations of a Method; afterward, + switches to bytecode-based implementation */ + +class NativeMethodAccessorImpl extends MethodAccessorImpl { + private final Method method; + private DelegatingMethodAccessorImpl parent; + private int numInvocations; + + NativeMethodAccessorImpl(Method method) { + this.method = method; + } + + public Object invoke(Object obj, Object[] args) + throws IllegalArgumentException, InvocationTargetException + { + // We can't inflate methods belonging to vm-anonymous classes because + // that kind of class can't be referred to by name, hence can't be + // found from the generated bytecode. + if (++numInvocations > ReflectionFactory.inflationThreshold() + && !ReflectUtil.isVMAnonymousClass(method.getDeclaringClass())) { + MethodAccessorImpl acc = (MethodAccessorImpl) + new MethodAccessorGenerator(). + generateMethod(method.getDeclaringClass(), + method.getName(), + method.getParameterTypes(), + method.getReturnType(), + method.getExceptionTypes(), + method.getModifiers()); + parent.setDelegate(acc); + } + + return invoke0(method, obj, args); + } + + void setParent(DelegatingMethodAccessorImpl parent) { + this.parent = parent; + } + + private static native Object invoke0(Method m, Object obj, Object[] args); +} +``` + +每次 NativeMethodAccessorImpl.invoke 方法被调用时,都会增加一次计数器,看超过阈值没有;一旦超过,则调用 MethodAccessorGenerator.generateMethod 来生成 Java 版的 MethodAccessor 的实现类,并且改变 DelegatingMethodAccessorImpl 所引用的 MethodAccessor 为 Java 版。后续经由 DelegatingMethodAccessorImpl.invoke 调用就是 Java 版的实现了。 + +这里,我在翻译一下开头注释:**在前几次的反射调用时会使用本地实现,之后会生成字节码,切换至基于字节码的动态实现。** + +在 MethodAccessorGenerator#generateMethod 中看起来是通过 ASM(一个知名字节码操作库)来生成字节码的。我们看一下 MethodAccessorGenerator#generateName 方法: + +```java + private static synchronized String generateName(boolean isConstructor, + boolean forSerialization) + { + if (isConstructor) { + if (forSerialization) { + int num = ++serializationConstructorSymnum; + return "jdk/internal/reflect/GeneratedSerializationConstructorAccessor" + num; + } else { + int num = ++constructorSymnum; + return "jdk/internal/reflect/GeneratedConstructorAccessor" + num; + } + } else { + int num = ++methodSymnum; + return "jdk/internal/reflect/GeneratedMethodAccessor" + num; + } + } +``` + +在这里,我们就能找到生成的字节码所对应的类的全限定名。 + +然后,我们就可以来验证一下啦,看看是不是反射调用超过十五次之后就会加载这样的一个类: + +```java +public class Main { + + public static void show(int i) { + new Exception("# " + i).printStackTrace(); + } + + public static void main(String[] args) throws Exception { + Class clazz = Class.forName("Main"); + Method method = clazz.getMethod("show", int.class); + + for (int i = 1; i < 20; i++) { + method.invoke(null, i); + } + } + +} +``` + +执行一下命令来运行这段 Java 代码: + +``` +// PS: 现在不需要先执行 javap 再执行 java 啦~ +// -verbose:class 参数会打印加载的类 +java -verbose:class Main.java +``` + +```verilog +// 省略 1 - 14 次,到第十五次还是本地实现 +java.lang.Exception: # 15 + at Main.show(Main.java:8) + at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) + at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) + at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) + at java.base/java.lang.reflect.Method.invoke(Method.java:566) + at Main.main(Main.java:16) + +// 开始加载 GeneratedMethodAccessor 类 +[0.864s][info][class,load] jdk.internal.reflect.GeneratedMethodAccessor0 source: __JVM_DefineClass__ + +// 第十六次还是本地实现,这时是因为字节码还未生成完 +java.lang.Exception: # 16 + at Main.show(Main.java:8) + at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) + at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) + at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) + at java.base/java.lang.reflect.Method.invoke(Method.java:566) + at Main.main(Main.java:16) + +// 第 17 次已经使用动态实现了 +java.lang.Exception: # 17 + at Main.show(Main.java:8) + at jdk.internal.reflect.GeneratedMethodAccessor1.invoke(Unknown Source) + at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) + at java.base/java.lang.reflect.Method.invoke(Method.java:566) + at Main.main(Main.java:16) +``` + +> 可以推断,反射调用的第一次和第十六次是最耗时的(初始化 NativeMethodAccessorImpl 和 字节码拼装 MethodAccessorImpl)。毕竟初始化是不可避免的,而 Native 方式的初始化会更快,因此前几次的调用会采用 Native 方法。 +> +> 随着调用次数的增加,每次反射都使用 JNI 跨越 Native 边界会对优化有阻碍作用,相对来说使用拼装出的字节码可以直接以 Java 调用的形式实现反射,发挥了 JIT 优化的作用,避免了 JNI 为了维护 OopMap(HotSpot 用来实现准确式 GC 的数据结构)进行封装 / 解封装的性能损耗因此在已经创建了 MethodAccessor 的情况下,使用 Java 版本的实现会比 Native 版本更快,所以当调用次数到达一定次数后,会切换成 Java 实现的版本,来优化未来可能的更频繁的反射调用。 + +到这里,JVM 如何实现反射就很清楚了,简单小结一下: + +在默认情况下,方法的反射调用为委派实现,委派给本地实现来进行方法调用。再调用超过 15 次之后,委派实现便会将委派对象切换至动态实现。这个动态实现的字节码是自动生成的,它将直接使用 invoke 指令来调用目标方法。 + +### 反射性能开销体现在哪? + +在刚才的例子中,我们先后进行了 Class.forName,Class.getMethod 以及 Method.invoke 三个操作。其中,Class.forName 会调用本地方法,Class.getMethod 则会遍历该类的公有方法。如果没有匹配到,它还将遍历父类的公有方法,可想而知,这两个操作都非常耗时。 + +值得注意的是,以 getMethod 为代表的查找方法操作,会返回查找得到结果的一份拷贝。因此,我们应当避免在热点代码中使用返回 Method 数组的 getMethods 或者 getDeclaredMethods 方法,以减少不必要的堆空间消耗。 +在实践中,我们往往会在应用程序中缓存 Class.forName 和 Class.getMethod 的结果,因此 ### 参考