反射作为 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
接入与更优性能表现。