一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第12天,点击查看活动详情
本系列专栏:JVM专栏
前言
本篇文章来说一下Java的重要特性:反射,反射相信大家都很熟悉了,它的基本概念就是在程序运行时,可以获取一个类的信息,该信息包括属性、方法等,以及通过这个信息来调用方法、设置属性等。
而该类就是前面文章在加载时所说的Class类,即当加载Java类时,当把类的信息加载入方法区时,会生成一个Class对象放入堆中,而这个Class类就是关键。
而对于反射的使用,不仅可以绕开Java语言的语法限制比如可以修改private修饰的属性以及调用其方法,还可以动态加载类,比如在配置文件中配置的一个类名。
正文
在说JVM是如何实现反射之前,我们先来熟悉回顾一下反射相关的API。
反射相关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 关键字。
- 使用 Array.newInstance(Class,int) 来构造该类型的数组。
- 使用 getFields()/getConstructors()/getMethods() 来访问该类的成员。
除了这三个之外,Class 类还提供了许多其他方法。需要注意的是,方法名中带 Declared 的不会返回父类的成员,但是会返回私有成员;而不带 Declared 的则相反。
当获得了类成员之后,我们可以进一步做如下操作。
- 使用 Constructor/Field/Method.setAccessible(true) 来绕开 Java 语言的访问限制。
- 使用 Constructor.newInstance(Object[]) 来生成该类的实例。
- 使用 Field.get/set(Object) 来访问字段的值。
- 使用 Method.invoke(Object, Object[]) 来调用方法。
有关反射 API 的其他用法,可以参考官方文档。
JVM对反射的实现
这里我们直接看个例子,比如下面代码:
public class TestReflect {
public static void target(int i) {
new Exception("#" + i).printStackTrace();
}
}
fun main(args: Array<String>) {
val testReflect = TestReflect()
val klass = testReflect::class.java
val method = klass.getMethod("target", Int::class.javaPrimitiveType)
method.invoke(null, 0)
}
这里我们注意2点:
-
可以使用异常打印堆栈的方式来查看堆栈调用情况。
-
Class对象可以查看其源码注释,该类是没有公有的构造函数,其对象只能JVM在加载类时获取到。
所以我们来看一下打印情况:
java.lang.Exception: #0
at com.wayeal.arithmeticapp.testReflect.TestReflect.target(TestReflect.java:8)
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:497)
at com.wayeal.arithmeticapp.testReflect.ReflectKt.main(reflect.kt:8)
可以发现这里的调用栈调用,我们不难发现main方法调用后,是Method的invoke方法,然后调用链是:DelegatingMethodAccessorImpl -> NativeMethodAccessorImpl -> native方法,会发现这里先是调用委托类,再去调用本地方法实现。
这里也侧面印证了为什么反射会比较慢,因为这里会调用本地方法去实现方法调用。
而既然有委托类,那是不是有不是本地方法实现的路径呢,答案是有的。
只需要把上述代码改成下面这样:
fun main(args: Array<String>) {
val testReflect = TestReflect()
val klass = testReflect::class.java
val method = klass.getMethod("target", Int::class.javaPrimitiveType)
for (i in 0 .. 20){
method.invoke(null, i)
}
}
连续调用21次invoke方法,我们来看看打印:
java.lang.Exception: #14
at com.wayeal.arithmeticapp.testReflect.TestReflect.target(TestReflect.java:8)
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:497)
at com.wayeal.arithmeticapp.testReflect.ReflectKt.main(reflect.kt:9)
java.lang.Exception: #15
at com.wayeal.arithmeticapp.testReflect.TestReflect.target(TestReflect.java:8)
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:497)
at com.wayeal.arithmeticapp.testReflect.ReflectKt.main(reflect.kt:9)
java.lang.Exception: #16
at com.wayeal.arithmeticapp.testReflect.TestReflect.target(TestReflect.java:8)
at sun.reflect.GeneratedMethodAccessor1.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:497)
at com.wayeal.arithmeticapp.testReflect.ReflectKt.main(reflect.kt:9)
java.lang.Exception: #17
at com.wayeal.arithmeticapp.testReflect.TestReflect.target(TestReflect.java:8)
at sun.reflect.GeneratedMethodAccessor1.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:497)
at com.wayeal.arithmeticapp.testReflect.ReflectKt.main(reflect.kt:9)
会发现从#16开始的打印变了,变成了调用GeneratedMethodAccessor1中的invoke方法,而这个就是JVM另一种反射实现机制,动态生成字节码,上述这个类大致如下面意思:
// 动态实现的伪代码,这里只列举了关键的调用逻辑,其实它还包括调用者检测、参数检测的字节码。
package jdk.internal.reflect;
public class GeneratedMethodAccessor1 extends ... {
@Overrides
public Object invoke(Object obj, Object[] args) throws ... {
Test.target((int) args[0]);
return null;
}
}
而这种Java代码调用会比上面本地方法调用快20倍左右,但是要生成额外字节码,所以JVM会设置一个阈值,当方法反复调用到一定程度才会触发。
某种程度上说,这也是JVM为反射所做的优化。
而对于反射的开销问题,从上面代码我们差不多就可以得出其方法就是增加必要的缓存,因为像Class.forName这种方法最终实现都是本地方法,而且方法调用等API在少数调用时也是本地方法实现,所以这部分代码很难优化。我们可以做的就是缓存Class.forName 和 Class.getMethod 的结果,以及根据项目需要调用合适的API。
总结
Java反射这章内容主要就是介绍了调用栈以及JVM做的优化,以及作为开发者,有什么好的方法提升一下性能。