MethodHandles.lookup().defineClass保留

MethodHandles.lookup().defineClass retention

MethodHandles.Lookup.defineClass 在运行时从字节数组生成一个新的 class。

什么情况下返回的class可以被垃圾回收?它是否在与 Lookup 对象关联的 classloader 的生命周期内保留,或者如果不再引用 Class 对象,它是否可以被垃圾收集?

通过 MethodHandles.Lookup.defineClass 创建的 classes 像任何其他 class 一样在定义的 class 加载程序中注册,并且可以像普通 class 一样通过名称引用]es,与 JDK 15 引入的 defineHiddenClass(...) 形成对比(请参阅答案末尾)。在 classes 被解析之前注册时,它们甚至可以取代静态编译的 classes,就像下面的例子:

import java.lang.invoke.MethodHandles;
import java.nio.charset.StandardCharsets;

public class LookupDynamicClass {
    public static void main(String[] args) throws IllegalAccessException {
        MethodHandles.Lookup lookup = MethodHandles.lookup();
        lookup.defineClass(("Êþº¾[=10=][=10=][=10=]05[=10=][=10=][=10=][=10=][=10=][=10=]"
        +"[=10=][=10=][=10=]foo[=10=]()V[=10=]Code[=10=][=10=][=10=][=10=]"
        +"hello from dynamic class[=10=][=10=][=10=][=10=]Lazy[=10=]java/"
        +"lang/Object[=10=]java/lang/System[=10=]out[=10=]Ljava/io/PrintStream;"
        +"[=10=]java/io/PrintStream[=10=]println[=10=](Ljava/lang/String;)V[=10=]"
        +"[=10=][=10=][=10=][=10=][=10=][=10=][=10=][=10=][=10=][=10=][=10=][=10=][=10=][=10=][=10=][=10=][=10=][=10=][=10=][=10=][=10=]²[=10=]"
        + "¶[=10=]±[=10=][=10=][=10=][=10=][=10=][=10=]").getBytes(StandardCharsets.ISO_8859_1));

        Lazy.foo();
    }
}
interface Lazy {
    static void foo() {
    }
}

Try it online

此示例动态定义了一个 Lazy class 其 foo() 方法将在被调用时打印 hello from dynamic class

在像 HotSpot 这样的 JVM 上,符号引用“Lazy”被延迟解析,即当尝试调用 Lazy.foo() 时,这将以动态定义的 class。对于热切解析符号引用的 JVM,当调用 MethodHandles.Lookup.defineClassLazy class 已经存在,因此,带有“attempted duplicate definition for Lazy”之类消息的 LinkageError 将被抛出。

换句话说,这些动态生成的 classes 与静态编译的 classes 共享相同的名称 space(class 加载上下文)。像普通的 class 一样在 class 加载器上注册,它们只能在定义的 class 加载器变得不可访问(包括它定义的所有 classes)时被垃圾收集,就像普通的 classes.


JDK 15 引入了 defineHiddenClass(...) 可用于定义 class 未在 class 加载程序中注册并且还能够访问 private 成员查找 class,类似于旧的非标准功能 sun.misc.Unsafe.defineAnonymousClass

该行为由 ClassOption 参数控制。

  • NESTMATE 允许访问查找 class 的 private 成员及其嵌套成员(通常是内部 classes)

  • STRONG 阻止 class 被卸载,直到 class 加载程序变得无法访问,尽管我不知道为什么有人会想要这个。如果没有此选项,隐藏的 class 可以在不存在对它的引用时立即进行垃圾收集和卸载。

这是该示例的改编版本:

public class LookupHiddenClass {
    public static void main(String[] args) throws Throwable {
        var lookup = MethodHandles.lookup();
        lookup = lookup.defineHiddenClass(("Êþº¾[=11=][=11=][=11=]05[=11=][=11=][=11=][=11=]"
        +"[=11=][=11=][=11=][=11=][=11=]foo[=11=]()V[=11=]Code[=11=][=11=][=11=][=11=]"
        +"hello from hidden class[=11=][=11=][=11=][=11=]Lazy[=11=]java/"
        +"lang/Object[=11=]java/lang/System[=11=]out[=11=]Ljava/io/PrintStream;"
        +"[=11=]java/io/PrintStream[=11=]println[=11=](Ljava/lang/String;)V[=11=]"
        +"[=11=][=11=][=11=][=11=][=11=][=11=][=11=][=11=][=11=][=11=][=11=][=11=][=11=][=11=][=11=][=11=][=11=][=11=][=11=][=11=][=11=]²[=11=]"
        + "¶[=11=]±[=11=][=11=][=11=][=11=][=11=][=11=]").getBytes(StandardCharsets.ISO_8859_1), true);

        lookup.findStatic(lookup.lookupClass(), "foo", MethodType.methodType(void.class))
              .invokeExact();

        var q = new ReferenceQueue<Class<?>>();
        var r = new PhantomReference<>(lookup.lookupClass(), q);

        lookup = null;

        do System.gc(); while(q.remove(1000) != r);

        System.out.println("class collected");
    }
}

虽然不能保证 System.gc() 会执行实际的垃圾收集,也不会收集特定对象,但使用 OpenJDK 的默认配置,它可以重复打印

hello from hidden class
class collected