JVM是如何实现反射的

252 阅读5分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 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做的优化,以及作为开发者,有什么好的方法提升一下性能。