Skip to content

Instantly share code, notes, and snippets.

@burningtnt
Last active May 3, 2025 15:00
Show Gist options
  • Save burningtnt/e4b39edadd0637cfb78e98dd7cfe3b87 to your computer and use it in GitHub Desktop.
Save burningtnt/e4b39edadd0637cfb78e98dd7cfe3b87 to your computer and use it in GitHub Desktop.

Note

为简化无关环节,部分细节可能会省去,而进一步的深入解释将标记在 NOTE 内。 如果这是您第一次了解 MethodHandle 和 VarHandle,可直接掠过这些部分。

Tip

本文将用 TIP 标记您需要使用刚刚学到的技能实践的部分。 边学边练,效果 ++。

零开销·反射

想必大家都写过类似这样的代码

Field f = MyClass.class.getDeclaredField("name");
f.setAccessible(true);
AnotherClass value = (AnotherClass) f.get(instance);

诚然,这样写并没有任何问题,而且代码可读性极高:获取 MyClassname 字段,然后在 instance 上读取他。

可惜,如果这样一段代码需要在每一帧渲染时运行 1w 次,那你就会发现绝大部分 CPU 都被浪费在了反射的额外开销上。

在这篇文章,我们将一同探讨传统反射的开销来源,并尝试最小化这些开销。

为什么以上代码会有额外开销

以上代码包括两个环节:

1. 获取 name 字段的 Field 对象

这一步包括

  1. 检查访问权限:如果对应字段不能被当前代码访问,则抛出异常;
  2. 调用 VM 本地方法 Class::getField0 来获取一个 Field 对象;
  3. 复制该获取的 Field 对象。

2. 对 Field 对象调用 get 方法

这一步包括

  1. 检查访问权限:如果对应字段不能被当前代码访问且未覆盖访问权限,则抛出异常;
  2. 构建访问器;
  3. 使用 Unsafe 获取对应字段的值。

Note

2-2 仅在一对某一特定的字段第一次访问时构建。后续会有全局(指在全 JVM 级别)的缓存。

显然,最开头的代码会在每次运行时完整的运行以上五步。即便提前将 Field 存储至常量池,2-1 这一无效步骤也无法跳过。 此外,由于这一调用链过于冗长,VM 不能有效的内联 get 方法内部的其他方法调用。

Note

“无法内联”导致的开销在高版本 JVM 上已得到优化。

至此,我们自然地得到了以下结论:将更多的检查放在创建某种类 Field 对象上,而运行时则尽量简单。

MethodHandle 代替 Method

在获取 MethodHandle 时,应先构造 Lookup 对象,然后构造 MethodType 对象。

Lookup 与访问权

Lookup 对象存储了访问权。例如,你不能够在 AClass 中访问 BClass 的私有方法,是因为 AClass 的访问权不足以访问 BClass 中的私有成员。

要获取代表当前类访问权的 Lookup 对象,可直接调用 MethodHandles::lookup

要提升访问权,例如从 AClass 中获取一可以访问 BClass 私有成员的 Lookup 对象,请使用 MethodHandles::privateLookupIn

// In AClass.java
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandles.Lookup promotedLookup = MethodHandles.privateLookupIn(BClass.class, lookup);

此时,promotedLookup 能够自由的操作 BClass 中的所有私有成员,但不再能自由操作 AClass 的私有成员。

获取 MethodType

例如,为创建以下 Object::equals 方法的 MethodType

public class Object {
    public boolean equals(Object obj) {
        // ...
    }
}

在构建 MethodType 对象时,应按照 Java 方法声明的书写顺序。即,先传递方法返回类型,再按照从左到右的顺序传递方法参数类型。

Warning

MethodType 是完全指定的,不会有任何的继承查找、装箱、拆箱。

Note

MethodType::methodType 方法有数个不同的重载。其第一个参数始终为返回类型,后续参数则支持通过不同的方式来传递参数类型。

MethodType methodType = MethodType.methodType(boolean.class, Object.class);

由此,我们成功创建了一描述 Object::equals 方法的 MethodType 对象。

获取 MethodHandle

MethodHandle 在用于反射时,其必然为以下类型之一:

  • 调用一静态方法: findStatic
  • 调用一实例方法: findVirtual
  • 调用一类构造器: findConstructor

注:这里提到的“类型”只对 DirectMethodHandle 成立,且并不包括字段访问器等类型。

延续上一个例子中获取到的 Object::equals 方法类型,接下来我们将尝试获取他的 MethodHandle。 考虑到该方法为实例方法,应使用 findVirtual 来构建 MethodHandle

MethodHandles.Lookup lookup = MethodHandles.lookup();

MethodHandle handle = lookup.findVirtual(
        Object.class, // equals 方法处于 Object 类内
        "equals", // 方法名称
        MethodType.methodType(boolean.class, Object.class) // 方法类型
);

Tip

试一试:创建 String 类下 replaceFirst 方法的 MethodHandle

参考答案
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodType methodType = MethodType.methodType(String.class, String.class);

MethodHandle handle = lookup.findVirtual(
        String.class, // equals 方法处于 Object 类内
        "replaceFirst", // 方法名称
        methodType // 方法类型
);

对于静态方法和构造器方法,只需对应更改 findVirtualfindStaticfindConstructor

  • findStatic: 用法和 findVirtual 一致;
  • findConstructor: 方法类型返回值始终为 void,且不再需要指定方法名称。

Tip

试一试:创建 String 类下接收一 char[] 参数的构造器的 MethodHandle

参考答案
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodType methodType = MethodType.methodType(void.class, String.class); // 方法返回值为 void

MethodHandle handle = lookup.findConstructor(
        String.class, // 构造器处于 Object 类内
        // 不需要指定方法名称
        methodType // 方法类型
);

使用 MethodHandle

您应在类初始化块中完成 MethodHandle 的获取操作。

MethodHandle::invokeExact 方法可调用一 MethodHandle 对象。 对于热点代码,请保证 MethodHandle 对象是直接从一 static final 字段中加载的,而不是函数的返回值或局部变量。

Warning

如果 MethodHandle 不是从 static final 字段中加载的,那么将失去 5% 的性能。 例如,Objects.requireNonNull(handle).invokeExact() 会比 handle.invokeExact 慢。

public class Foo {
    private static final MethodHandle HANDLE = acquireMethodHandle();

    // 如果你有好几个 MethodHandle 需要初始化,可以写数个 acquireMethodHandle。
    // 将 MethodHandle 初始化放入独立函数并不是标准的一部分,但这可以让我们更专注于类的其他部分。
    private static MethodHandle acquireMethodHandle() {
        try {
            // 在这里构造 Lookup, MethodType 并获取 MethodHandle
            return ...;
        } catch (NoSuchMethodException e) {
            // 异常处理。通常来说,我们简单地将错误包装为 RuntimeException。
            throw new RuntimeException(e); // 不应发生。如果确实发生了,说明你 findVirtual 写错了。
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e); // 不应发生。如果确实发生了,说明你没有提升 lookup 的权限。
        }
    }

    public static void foo() {
        HANDLE.invokeExact( /* 传入参数 */); // 正:直接从 static final 字段加载 MethodHandle。

        Objects.requireNonNull(HANDLE).invokeExact(); // 误:从函数返回值中加载了 MethodHandle。

        MethodHandle handle = HANDLE;
        handle.invokeExact(); // 误:从局部变量中加载了 MethodHandle。
    }
}

Note

实际上 JVM 能够通过常量内联将上述两个例子中对 HANDLE 的处理消除,等效地转变为直接加载的情况。 但这会降低方法句柄启动时的效率,请不要这么做!

invokeExact 方法可接收数个参数。

对于 findVirtual 加载的 MethodHandle,其第一个参数必须为代表 this 的对象。 对于 findConstructor 加载的 MethodHandle,其返回值为构造出的对象,而非 void

public class Foo {
    private static final MethodHandle HANDLE = acquireMethodHandle();

    private static MethodHandle acquireMethodHandle() {
        try {
            MethodHandles.Lookup lookup = MethodHandles.lookup();

            return lookup.findVirtual(
                    Object.class,
                    "equals",
                    MethodType.methodType(boolean.class, Object.class)
            );
        } catch (NoSuchMethodException e) {
            throw new RuntimeException(e); // 不应发生。如果确实发生了,说明你 findVirtual 写错了。
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e); // 不应发生。如果确实发生了,说明你没有提升 lookup 的权限。
        }
    }

    public static boolean equals(@NotNull Object object1, Object object2) throws Throwable {
        return HANDLE.invokeExact(object1, object2);
    }
}

需要特别注意的是,invokeExact 方法的参数和返回值类型必须和原方法完全匹配。 在该例子中,HANDLE 指向的方法签名为 (Object, Object) -> boolean

为使方法签名匹配,所有传入的参数必须通过紧邻的强转到所需要的类型。 在该例子中,即为:(boolean) HANDLE.invokeExact((Object) object1, (Object) object2); 由于 equals 方法返回 boolean 类型,且 object1, object2 类型均为 Object,上述三处强转可以省略。

Warning

如果你错误地省略了强转或参数个数不对,将会收获 WrongMethodTypeException: handle's method type <1> but found <2>。 此时,<1> 会标出 MethodHandle 的类型,<2> 会标出 invokeExact 时使用的类型。

Tip

试一试:创建 String 类下 replaceFirst 方法的 MethodHandle,然后将 Hello World 字符串中的 Hello 替换为 Hi

参考答案
public class Foo {
    private static final MethodHandle HANDLE = acquireMethodHandle();

    private static MethodHandle acquireMethodHandle() {
        try {
            MethodHandles.Lookup lookup = MethodHandles.lookup();
            MethodType methodType = MethodType.methodType(String.class, String.class);

            return lookup.findVirtual(
                    String.class, // equals 方法处于 Object 类内
                    "replaceFirst", // 方法名称
                    methodType // 方法类型
            );
        } catch (NoSuchMethodException e) {
            throw new RuntimeException(e); // 不应发生。如果确实发生了,说明你 findVirtual 写错了。
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e); // 不应发生。如果确实发生了,说明你没有提升 lookup 的权限。
        }
    }

    public static void main(String[] args) throws Throwable {
        // invokeExact 根据字符串字面量推断参数类型为 String, String, String,根据将 (String) 强转推断返回类型为 String。
        String result = (String) HANDLE.invokeExact("Hello World", "Hello", "Hi");

        System.out.println(result);
    }
}

VarHandle 代替 Field

获取 VarHandle

VarHandle 在用于反射时,其必然为以下类型之一:

  • 操作一静态字段: findStaticVarHandle
  • 操作一实例字段: findVarHandle

例如:若想获取 System::out 这一静态字段,我们应使用 findStaticVarHandle

MethodHandles.Lookup lookup = MethodHandles.lookup();

VarHandle handle = lookup.findStaticVarHandle(
        System.class, // out 字段处于 System 类中
        "out", // 字段名称
        PrintWriter.class // out 字段类型
);

Tip

试一试:创建 Boolean 类下 TRUE 字段的 VarHandle

参考答案
MethodHandles.Lookup lookup = MethodHandles.lookup();

VarHandle handle = lookup.findStaticVarHandle(
        Boolean.class, // TRUE 字段处于 System 类中
        "TRUE", // 字段名称
        Boolean.class // TRUE 字段类型
);

需要特别注意的是,在查找 VarHandle 时,字段类型是不会按继承关系自动查找的。 例如,在上述代码中,若将 findStaticVarHandle 的最后一个参数改为 Object.class,那么查找将直接失败。

使用 VarHandle

你同样应当将 VarHandle 放入 static final 字段中。

  • 获取字段:使用 get* 方法;
  • 写入字段:使用 set* 方法,并附带想要写入的值。

对于操作实例字段的 VarHandle,应将第一个参数前插入指向 this 的对象。 其类型依然是完全匹配的。

public class Foo {
    private static final VarHandle HANDLE = acquireVarHandle();

    private static VarHandle acquireVarHandle() {
        MethodHandles.Lookup lookup = MethodHandles.lookup();

        return lookup.findStaticVarHandle(
                Boolean.class, // TRUE 字段处于 System 类中
                "TRUE", // 字段名称
                Boolean.class // TRUE 字段类型
        );
    }

    public static void foo() {
        Boolean booleanTrue = (Boolean) HANDLE.get();
    }
}

Note

实际上 VarHandle 默认为 invoke-behavior,JVM 会执行向上、向下转型来让类型匹配。 如果调用 withInvokeExactBehavior,那么会禁用这些操作并略微提升性能。


Warning

如果您是 MethodHandle 的初学者,那么您不需要继续阅读以下部分。

一些奇妙的小代码

privateLookupIn 无法提升权限

模块化后,Java 不再允许用户随意反射 JDK 内部类和未开放反射权限的第三方模块。 但,Lookup.IMPL_LOOKUP 可以能够不受限的访问一切内容。

我想要调用的方法签名中含有不可访问的类,没法直接强转

在正常创建 MethodHandle 后,使用 asType 方法传入一个新的 MethodType 来修改其类型。 该 MethodType 应当是原方法的向上转形版本。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment