反射作为 Java 中的万能百宝箱,不论在系统设计还是工具开发中都能频繁看到其身影。
但我们也都知道由于其运行动态编译的特点导致 JIT 无法介入,相较于方法的调用在性能上存在损耗。
那难道鱼和熊掌不可兼得吗,JDK 也意识到了这一点,因此在 JDK 7 中引入 MethodHandles 全新的反射框架,下面就让我们一睹芳容。
一、反射进阶
1. 实例声明
MethodHandles 的声明十分简单,通过静态方法实例化即可,方式如下:
public void init() {
MethodHandles.Lookup lookup = MethodHandles.lookup();
}
但需注意一点,默认 lookup() 声明的实例仅允许访问 public 属性无法访问私有属性。在传统的反射中可通过 setAccessible(true) 实现越权但 MethodHandles 中并不支持。
若想要实现私有属性的访问,在声明实例时需稍微变通一下。
查看 Lookup 的源码可以看见其提供私有的构造器可指定 allowedModes 即访问方式,利用其我们便可绕过限制实现越权。
public static final class Lookup {
Lookup(Class<?> lookupClass) {
this(lookupClass, ALL_MODES);
checkUnprivilegedlookupClass(lookupClass, ALL_MODES);
}
private Lookup(Class<?> lookupClass, int allowedModes) {
this.lookupClass = lookupClass;
this.allowedModes = allowedModes;
}
}
对于此类 private 构造器,最经典的方式即利用传统反射实现初始化。
详细的代码实现如下:
public void init() {
Constructor<MethodHandles.Lookup> ctor =
MethodHandles.Lookup.class.getDeclaredConstructor(Class.class, int.class);
ctor.setAccessible(true);
MethodHandles.Lookup lookup =
ctor.newInstance(Foo.class, MethodHandles.Lookup.PRIVATE);
}
而在 JDK 9 之后引入了模块化管理,因此 JDK 9 之后也默认提供 privateLookupIn() 方式可实现私有访问,无需再通过反射处理。
public void init() {
MethodHandles.Lookup lookup =
MethodHandles.privateLookupIn(Foo.class, MethodHandles.lookup());
}
二、字段反射
在完成实例化之后,让我们先看看如何通过 MethodHandles 实现字段属性的反射操作。
1. 属性读取
对于对象的字段访问,最基本的操作即读写与写入,先以数据读取为例。
在 MethodHandles 中字段的读取可谓相当简单,提供了 findGetter() 与 findStaticGetter() 分别用于获取普通与静态字段实例,即对应传统反射中的 Field 变量。
而对于获取的 MethodHandle 实例变量,通过 invoke() 方法便可读取字段内容,操作示例如下:
public class Foo {
private Integer id;
private static String name = "Alex";
}
public void getter() throws Throwable {
Foo foo = new Foo(123);
MethodHandles.Lookup lookup = MethodHandles
.privateLookupIn(Foo.class, MethodHandles.lookup());
MethodHandle getter = lookup.findGetter(Foo.class, "id", Integer.class);
System.out.println(getter.invoke(foo));
MethodHandle staticGetter = lookup.findStaticGetter(Foo.class, "name", String.class);
System.out.println(staticGetter.invoke());
}
正如之前所提到的,在 MethodHandles 中无需 setAccessible(true) 设置访问权限,而是在声明实例直接定义,如上述代码示例中通过 privateLookupIn() 即可访问公有及私有属性实例。
2. 属性赋值
与属性读取相对应,对于字段的赋值同样提供了 findSetter() 与 findStaticSetter() 方法。
在使用上并无差异故不再复述,相应的使用示例如下:
public void getter() throws Throwable {
Foo foo = new Foo(123);
MethodHandles.Lookup lookup = MethodHandles
.privateLookupIn(Foo.class, MethodHandles.lookup());
MethodHandle setter = lookup.findSetter(Foo.class, "id", Integer.class);
setter.invoke(foo, 123);
MethodHandle staticSetter = lookup.findStaticSetter(Foo.class, "name", String.class);
staticSetter.invoke(123);
}
三、方法反射
下面同样让我们了解下如何通过 MethodHandles 实现方法的反射调用。
1. 方法描述
在开始前让我们先声明一个简单的测试类方法如下:
public class Foo {
public void sayHi() {
System.out.println("Hi");
}
}
我们都知道在传统的反射中,通过类对象的 getMethod() 方法便可获取方法属性。
Method method = Foo.class.getMethod("sayHi");
而在 MethodHandles 中方法与之前字段反射中的类似,以 MethodType 进行描述。
通过查看 MethodType 其初始化方法可以看到,由返回值和方法入参构建了一个方法的描述体。
public static MethodType methodType(Class<?> rtype) {
}
public static MethodType methodType(Class<?> rtype, Class<?> ptype0, Class<?>... ptypes) {
}
在构建 MethodType 方法描述之后,便可通过 findVirtual() 方法获取方法实现,通过 MethodType 实现方法的精确定位。
MethodHandle mh = lookup.findVirtual(
Foo.class,
"sayHi",
MethodType.methodType(void.class)
);
与之前的字段反射类似,findVirtual() 实现了普通方法的获取,而 findStatic() 针对于静态方法而言,在具体的使用上并无太大差异这里不再举例介绍。
2. 链路调用
获取方法示例之后调用方式十分简单,通过 invoke(obj) 调用即可,针对静态方法则通过 invoke() 执行。
相对应的代码示例如下:
public void demo() throws Throwable {
Class<Foo> clazz = Foo.class;
MethodHandles.Lookup lookup = MethodHandles
.privateLookupIn(clazz, MethodHandles.lookup());
MethodHandle constructor = lookup.findConstructor(clazz, MethodType.methodType(void.class));
Object foo = constructor.invoke();
MethodHandle mh = lookup.findVirtual(
clazz,
"one",
MethodType.methodType(void.class)
);
mh.invoke(foo);
}
四、性能差异
1. 传统反射
看到这你也许有个疑问,同样都是 invoke() 执行调用,那差异到底在哪?
让我们先看下传统的反射执行链路,JDK 里反射的实现分为两类:本地代码解释调用 (Native Reflection / Inflated) 和字节码生成的快速调用 (Generated MethodAccessor)
(1) 本地代码调用
本地代码调用即通过 JNI 调用 HotSpot 内部的 MethodAccessor。
优点是不需要生成新类启动速度快,但每次反射调用都会经过很多安全检查,当调用次数达到一定数量时性能相对较低。
(2) 字节码生成调用
字节码生成调用则为动态生成一个字节码类 (MethodAccessorImpl 子类),直接调用目标方法。
优点即性能接近普通方法调用,但缺点就是生成类有开销,如果方法只调用几次反而得不偿失。
因此在传统的反射中当 Method / Constructor / Field 反射调用中,前 15 次执行方式为本地代码调用。当的次数超过 15 次后,JDK 就会膨胀执行链路将转为字节码生成调用。
其中 inflationThreshold 为代码中静态 final 参数不可调整。
public class ReflectionFactory {
private static int inflationThreshold = 15;
}
class NativeMethodAccessorImpl extends MethodAccessorImpl {
private static native Object invoke0(Method m, Object obj, Object[] args);
}
2. 反射优化
查看 MethodHandle 类中的定义内容,可以看到 invoke() 方法同样为 native 方法,不同之处其多了 @IntrinsicCandidate 与 @PolymorphicSignature 注解。
public abstract class MethodHandle implements Constable {
@IntrinsicCandidate
public final native @PolymorphicSignature Object invoke(Object... args) throws Throwable;
}
MethodHandle 性能更优的关键也同样在此,下面分别介绍两个注解的作用。
(1) @IntrinsicCandidate
@IntrinsicCandidate 标记某个方法是 JVM intrinsic 内建方法。
内建方法即在 JDK 代码中有 Java 实现,但 JVM 在执行时会替换成高效的 CPU 指令或专门的优化实现。
通过 @IntrinsicCandidate 标记告诉 HotSpot 该方法有可能被 intrinsic 优化,并且可以在 JIT 编译中进行替换。它不保证一定优化成功,也不影响方法的正常调用,完全是 JVM 和 JDK 之间的内部约定。
(2) @PolymorphicSignature
@PolymorphicSignature 标识一个方法是多态签名方法 (polymorphic signature method)。
正常情况下,Java 方法签名是编译期就固定的(参数类型、返回类型完全确定)。但 MethodHandle 是 Java 7 引入的动态调用机制,需要在运行时根据不同类型的参数调用不同的方法实现。
于是,JVM 允许某些特殊方法在字节码层看起来是签名不固定的,它们会在运行时再解析具体的签名。
通过上面的分别可以看到,基于注解标注 JVM 在 native 实现中进行了二次优化从而实现 JIT 接入与更优性能表现。