You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
717 lines
29 KiB
717 lines
29 KiB
---
|
|
深入理解 反射
|
|
---
|
|
|
|
### 目录
|
|
|
|
1. 前言
|
|
2. 反射的定义以及基本使用
|
|
* Class
|
|
* Member
|
|
* Array and Enumerated
|
|
3. JVM 是如何实现反射的?
|
|
* 委派实现
|
|
* 本地实现
|
|
* 动态实现
|
|
4. 反射性能开销体现在哪?
|
|
5. 如何优化反射性能开销?
|
|
6. 参考
|
|
|
|
### 前言
|
|
|
|
本文主要参考 [JVM 是如何实现反射的?](https://time.geekbang.org/column/article/12192),但是这篇文章需要购买才能阅读,有兴趣的可以支持一下原作者。
|
|
|
|
但是笔者在测试的过程中发现两个问题:
|
|
|
|
1. 关闭 Inflation 机制并不会减少反射调用耗时
|
|
2. 增加类型 Profile 默认数量并不会减少反射调用耗时
|
|
|
|
可能是我的测试方法有误,有理解的小伙伴可以拉我一把哇~
|
|
|
|
### 反射的定义以及基本使用
|
|
|
|
反射是 Java 语言中一个相当重要的特性,它允许正在运行的 Java 程序观测,甚至是修改程序的动态行为。表现为如下两点:
|
|
|
|
1. 对于任意一个类,都能知道这个类的所有属性和方法
|
|
2. 对于任意一个对象,都能调用它的任意一个属性和方法
|
|
|
|
反射涉及的 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<T> {
|
|
|
|
private String s;
|
|
public static final int AGE = 18;
|
|
public float aFloat = 0f;
|
|
public boolean[][] booleans;
|
|
public List<String> 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<T> {
|
|
|
|
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 <clinit>, 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 指令来调用目标方法。
|
|
|
|
反射调用的 Inflation 机制是可以通过参数(-Dsun.reflect.noInflation=true)来关闭的,这样一来,在反射调用一开始便会直接生成动态实现,而不会使用委派实现或者本地实现。
|
|
|
|
### 反射性能开销体现在哪?
|
|
|
|
在刚才的例子中,我们先后进行了 Class.forName,Class.getMethod 以及 Method.invoke 三个操作。其中,Class.forName 会调用本地方法,Class.getMethod 则会遍历该类的公有方法。如果没有匹配到,它还将遍历父类的公有方法,可想而知,这两个操作都非常耗时。
|
|
|
|
值得注意的是,以 getMethod 为代表的查找方法操作,会返回查找得到结果的一份拷贝。因此,我们应当避免在热点代码中使用返回 Method 数组的 getMethods 或者 getDeclaredMethods 方法,以减少不必要的堆空间消耗。
|
|
|
|
在实践中,我们往往会在应用程序中缓存 Class.forName 和 Class.getMethod 的结果,因此,下面我们就只关注反射调用本身的性能开销。
|
|
|
|
```java
|
|
public class ReflectDemo {
|
|
|
|
public void doSth(int i) {
|
|
|
|
}
|
|
|
|
public static void main(String[] args) throws Exception {
|
|
Class<?> clazz = ReflectDemo.class;
|
|
Constructor constructor = clazz.getConstructor();
|
|
Object object = constructor.newInstance();
|
|
Method method = clazz.getMethod("doSth", int.class);
|
|
|
|
ReflectDemo demo = new ReflectDemo();
|
|
|
|
long current = System.currentTimeMillis();
|
|
for (int i = 1; i <= 2_000_000_000; i++) {
|
|
if (i % 100_000_000 == 0) {
|
|
long temp = System.currentTimeMillis();
|
|
System.out.println(temp - current);
|
|
current = temp;
|
|
}
|
|
// 直接调用
|
|
demo.doSth(2333);
|
|
// 反射调用
|
|
// method.invoke(object, 2333);
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
取最后五个记录的平均值,作为预热后的峰值性能,一亿次的直接调用耗时为 94ms(macOS + JDK11)然后把 94 作为基准值。
|
|
|
|
改为反射调用,传入 2333 作为反射调用的参数,测得的结果约为基准值的 3.2 倍(301ms)。
|
|
|
|
除了反射调用外,还额外做了两个操作:
|
|
|
|
第一,由于 Method.invoke 是一个变长参数方法,在字节码层面它的最后一个参数会是 Object 数组。Java 编译器会在方法调用处生成一个长度为传入参数数量的 Object 数组,并将传入参数一一存储进该数组中。
|
|
|
|
第二,由于 Object 数组不能存储基本类型,Java 编译器会对传入的基本数据类型进行自动装箱。
|
|
|
|
这两个操作除了带来性能开销外,还可能占用堆内存,使得 GC 更加频繁。
|
|
|
|
使用 -Xlog:gc 参数,打印 GC 信息,可以看到在疯狂的 GC:
|
|
|
|
```
|
|
// java -Xlog:gc ReflectDemo.java
|
|
// ...
|
|
[7.671s][info][gc] GC(108) Pause Young (Normal) (G1 Evacuation Pause) 301M->2M(499M) 1.087ms
|
|
[7.736s][info][gc] GC(109) Pause Young (Normal) (G1 Evacuation Pause) 301M->2M(499M) 1.132ms
|
|
[7.819s][info][gc] GC(110) Pause Young (Normal) (G1 Evacuation Pause) 301M->2M(499M) 1.219ms
|
|
[7.891s][info][gc] GC(111) Pause Young (Normal) (G1 Evacuation Pause) 301M->2M(499M) 1.159ms
|
|
[7.960s][info][gc] GC(112) Pause Young (Normal) (G1 Evacuation Pause) 301M->2M(499M) 1.172ms
|
|
```
|
|
|
|
那么,如何消除这部分开销呢?
|
|
|
|
关于第二个自动装箱,Java 缓存了 [-128, 127] 中所有整数所对应的 Integer 对象。当需要自动装箱的整数在这个范围之内时,便返回缓存的 Integer,否则需要新建一个 Integer 对象。
|
|
|
|
因此,我们可以使用已经缓存的 Integer 对象或者扩大 Integer 对象。以这种方式测得的结果约为基准的 2.4 倍(222ms)。
|
|
|
|
现在我们再来看看因变长参数生成的 Object 数组,既然每个反射调用对应的参数个数是固定的,那么我们可以选择在循环外新建一个 Object 数组,设置好参数并直接交给反射调用,代码如下:
|
|
|
|
```java
|
|
public class ReflectDemo {
|
|
|
|
public void doSth(int i) {
|
|
|
|
}
|
|
|
|
public static void main(String[] args) throws Exception {
|
|
Class<?> clazz = ReflectDemo.class;
|
|
Constructor constructor = clazz.getConstructor();
|
|
Object object = constructor.newInstance();
|
|
Method method = clazz.getMethod("doSth", int.class);
|
|
|
|
// 在循环外构造参数数组
|
|
Object[] arg = new Object[1];
|
|
arg[0] = 2333;
|
|
|
|
long current = System.currentTimeMillis();
|
|
for (int i = 1; i <= 2_000_000_000; i++) {
|
|
if (i % 100_000_000 == 0) {
|
|
long temp = System.currentTimeMillis();
|
|
System.out.println(temp - current);
|
|
current = temp;
|
|
}
|
|
// 反射调用
|
|
method.invoke(object, arg);
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
测试的结果反而更加糟糕了,为基准值的 3.5 倍(331ms)。
|
|
|
|
再解决了自动装箱之后查看运行时的 GC 状况时,你会发现这段程序并不会触发 GC。其原因在于,原本的反射调用被内联了,从而使得即时编译器中的逃逸分析将原本新建的 Object 数组判断为不逃逸的对象。
|
|
|
|
如果一个对象不逃逸,那么即时编译器可以选择栈分配甚至是虚拟分配,也就是不占用堆空间。
|
|
|
|
如果在循环外新建数组,即时编译器无法确定这个数组会不会中途被更改,因此无法优化掉访问数组的操作,可谓是得不偿失。
|
|
|
|
前面我们提到过,可以关闭反射调用的 Inflation 机制,从而取消委派实现,直接使用动态实现。此外,每次反射调用都会检查目标方法权限,而这个检查同样可以在 Java 代码里关闭,在关闭了这权限检查机制之后,代码如下:
|
|
|
|
```java
|
|
public class ReflectDemo {
|
|
|
|
public void doSth(int i) {
|
|
|
|
}
|
|
|
|
public static void main(String[] args) throws Exception {
|
|
Class<?> clazz = ReflectDemo.class;
|
|
Constructor constructor = clazz.getConstructor();
|
|
Object object = constructor.newInstance();
|
|
Method method = clazz.getMethod("doSth", int.class);
|
|
method.setAccessible(true); // 关闭权限检查
|
|
|
|
long current = System.currentTimeMillis();
|
|
for (int i = 1; i <= 2_000_000_000; i++) {
|
|
if (i % 100_000_000 == 0) {
|
|
long temp = System.currentTimeMillis();
|
|
System.out.println(temp - current);
|
|
current = temp;
|
|
}
|
|
// 反射调用
|
|
method.invoke(object, 23);
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
测得的结果约为基准值的 2.2 倍(204ms)。
|
|
|
|
首先,在这个例子中,之所以反射调用能够变得那么快,主要是因为即时编译器中的方法内联。在关闭了 Inflation 的情况下,内联的瓶颈在于 Method.invoke 方法中对 MethodAccessor.invoke 方法的调用。
|
|
|
|
![Method.invoke.png](https://i.loli.net/2019/09/16/3ZhN8Of95zujI1P.png)
|
|
|
|
在生产环境中,我们往往拥有多个不同的反射调用,对应多个 GeneratedMethodAccessor,也就是动态实现。
|
|
|
|
由于 Java 虚拟机的关于上述调用点的类型 profile(注:对于 invokevirtual 或者 invokeinterface,Java 虚拟机会记录调用者的具体类型,我们称之为类型 profile)无法同时记录这么多个类,因此可能造成所测试的反射调用没有被内联的情况。
|
|
|
|
```java
|
|
public class ReflectDemo {
|
|
|
|
public void doSth(int i) {
|
|
|
|
}
|
|
|
|
public static void main(String[] args) throws Exception {
|
|
Class<?> clazz = ReflectDemo.class;
|
|
Constructor constructor = clazz.getConstructor();
|
|
Object object = constructor.newInstance();
|
|
Method method = clazz.getMethod("doSth", int.class);
|
|
method.setAccessible(true); // 关闭权限检查
|
|
|
|
polluteProfile();
|
|
|
|
long current = System.currentTimeMillis();
|
|
for (int i = 1; i <= 2_000_000_000; i++) {
|
|
if (i % 100_000_000 == 0) {
|
|
long temp = System.currentTimeMillis();
|
|
System.out.println(temp - current);
|
|
current = temp;
|
|
}
|
|
// 反射调用
|
|
method.invoke(object, 23);
|
|
}
|
|
}
|
|
|
|
public static void polluteProfile() throws Exception {
|
|
Class<?> clazz = ReflectDemo.class;
|
|
Constructor constructor = clazz.getConstructor();
|
|
Object object = constructor.newInstance();
|
|
Method method1 = clazz.getMethod("target1", int.class);
|
|
Method method2 = clazz.getMethod("target2", int.class);
|
|
for (int i = 0; i < 2000; i++) {
|
|
method1.invoke(object, 0);
|
|
method2.invoke(object, 0);
|
|
}
|
|
}
|
|
|
|
public void target1(int i) {
|
|
|
|
}
|
|
|
|
public void target2(int i) {
|
|
|
|
}
|
|
}
|
|
```
|
|
|
|
这时测试的结果为基准值的 7.2 倍(679ms)。也就是说,只要耽误了 Method.invoke 方法的类型 profile,性能开销便从 2.2 上升到 7.2 倍。
|
|
|
|
之所以这么慢,除了没有方法内联之外,另一个原因是逃逸分析不再生效。这个时候便可以在循环外构造参数数组,并直接传递给反射调用,这样子测的结果为基准值的 5.8 倍(548ms)。
|
|
|
|
除此之外,我们还可以提高 Java 虚拟机关于每个调用能够记录的类型数目(对应虚拟机参数 -XX:TypeProfileWidth,默认值为 2,这里设置为 8)。
|
|
|
|
#### 总结
|
|
|
|
影响反射调用耗时有以下原因:
|
|
|
|
1. 方法表查找
|
|
2. 构建 Object 数组以及可能存在的自动装拆箱操作
|
|
3. 运行时权限检查
|
|
4. 方法内联/逃逸分析
|
|
|
|
### 如何优化反射性能开销?
|
|
|
|
1. 尽量避免反射调用虚方法
|
|
2. 关闭运行时权限检查
|
|
3. 可能需要增大基本数据类型对应的包装类缓存
|
|
4. 关闭 Inflation 机制
|
|
5. 提高 JVM 关于每个调用能够记录的类型数目
|
|
|
|
### 参考
|
|
|
|
[https://docs.oracle.com/javase/tutorial/reflect/](https://docs.oracle.com/javase/tutorial/reflect/)
|
|
|
|
[JVM 是如何实现反射的?](https://time.geekbang.org/column/article/12192)
|
|
|
|
[Java反射原理简析](http://fanyilun.me/2015/10/29/Java%E5%8F%8D%E5%B0%84%E5%8E%9F%E7%90%86/)
|
|
|
|
[关于反射调用方法的一个log](https://www.iteye.com/blog/rednaxelafx-548536) |