Java 反射进阶优化


反射作为 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 编译中进行替换。它不保证一定优化成功,也不影响方法的正常调用,完全是 JVMJDK 之间的内部约定。

(2) @PolymorphicSignature

@PolymorphicSignature 标识一个方法是多态签名方法 (polymorphic signature method)

正常情况下,Java 方法签名是编译期就固定的(参数类型、返回类型完全确定)。但 MethodHandleJava 7 引入的动态调用机制,需要在运行时根据不同类型的参数调用不同的方法实现。

于是,JVM 允许某些特殊方法在字节码层看起来是签名不固定的,它们会在运行时再解析具体的签名。

通过上面的分别可以看到,基于注解标注 JVMnative 实现中进行了二次优化从而实现 JIT 接入与更优性能表现。


文章作者: 烽火戏诸诸诸侯
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 烽火戏诸诸诸侯 !
  目录