合成lambda类的神奇类卸载?

时间:2016-01-21 15:02:30

标签: java lambda garbage-collection

我们通常知道来自ClassLoader的类cannot be unloaded,但似乎lambda的合成类可以。

证明:

public class Play {
    static String test = new String("test");

    public static void main(String[] args) throws Exception {
        WeakReference<String> wr = new WeakReference<>(test);
        Runnable run = test::toString;
        System.out.println(Play.class.getClassLoader() == run.getClass().getClassLoader());
        System.out.printf("sys=%s, lambda=%s", Runnable.class, run.getClass());
        run.run();
        test = null;
        run = null;
        while (wr.get() != null) {
            System.gc();
        }
    }
}

输出:

true
sys=interface java.lang.Runnable, lambda=class Play$$Lambda$1/918221580
<then terminates>

这表明lambda的clousure引用的String(将被合成类引用)被解除引用,这意味着合成的类也必须已经GC。这是有道理的,否则会出现各种基于类加载器的内存泄漏。

我的问题是他们打破了班级无法卸载设计吗?如果是这样,我们可以用自己的课程来做这件事吗?

2 个答案:

答案 0 :(得分:2)

有趣的是,尽管你的测试不足以确定,但实际上,为lambda表达式生成的类可以获取垃圾,如下面的代码所示:

import java.lang.invoke.*;
import java.lang.ref.WeakReference;

public class LambdaClassUnloading {
    public static void main(String[] args) throws Throwable {
        MethodHandles.Lookup l = MethodHandles.lookup();
        MethodType t=MethodType.methodType(void.class);
        Runnable r=(Runnable)LambdaMetafactory.metafactory(l, "run",
            MethodType.methodType(Runnable.class), t,
            l.findStatic(LambdaClassUnloading.class, "testMethod", t), t)
            .getTarget().invokeExact();
        System.out.println("generated "+r);
        r.run();
        WeakReference<Class<?>> ref=new WeakReference<>(r.getClass());
        r=null;
        while(ref.get()!=null) System.gc();
        System.out.println("class collected");
    }
    private static void testMethod() {
        System.out.println("testMethod() called");
    }
}

这打印出类似的东西:

generated java8test.LambdaClassUnloading$$Lambda$1/723074861@77459877
testMethod() called
class collected

虽然数字可能会有所不同。

但是出于实际目的,重要的是要理解上面的代码反射调用的生成器工具通常通过invokedynamic字节代码指令触发,该指令将永久链接到第一次完成的引导的结果过程

换句话说,考虑到当前JRE如何处理它,只要包含invokedynamic指令的代码可以访问,永久链接的lambda类也将保持不变。这符合Andremoniy’s answer中演示的行为;行Runnable run = test::toString;的代码可以再次执行,并将生成与第一次执行相同的类的实例,因为它的构造函数永久链接到它。

但理论上,替代JRE实现可以让引导方法返回MutableCallSite并在稍后重新定位,例如生成更优化的类的实例或强制在语义上等效的lambda表达式之间共享类。在这种情况下,当没有更多的lambda实例时,现有的lambda类可能比其定义类更早地收集垃圾。

所以答案是,确实有一个“神奇的类卸载”,但是目前,它与大多数实际目的无关,当然,如果它发生在未来的JVM中,它仍然会保留语义指定的语义。 Java语言规范。无论方法引用test::toString捕获的实例如何存储,如果功能接口的实例无法访问,它将不会阻碍其收集。

如果您对更多详细信息感兴趣,请参阅文章“anonymous classes in the VM” by John Rose。您也可以搜索sun.misc.Unsafe.defineAnonymousClass ...

答案 1 :(得分:1)

我认为你对lambda类卸载感到困惑,这就是原因。您必须不测试wr.get(),因为取消引用此String实例不会基于它来讨论有关lambda的任何内容。您必须为课程WeakReference

...
Runnable run = test::toString;
WeakReference<Class> wcr = new WeakReference<Class>(run.getClass());
...

并尝试“等待”,同时将其取消引用:

while (wcr.get() != null) {
   System.gc();
}

所以,这个循环永远不会完成。