1

We know normally a class cannot be unloaded from a ClassLoader, but it seems the synthesised class for lambdas can.

Proof:

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();
        }
    }
}

Output:

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

This shows that the String referenced by the lambda's clousure (would be referenced by the synthesised class) was dereferenced, which means the synthesised class must have been GC'ed as well. This makes sense as otherwise there would be all kinds of classloader-based memory leaks.

My question is did they break the class cannot be unloaded design to do this? If so, can we do this with our own classes?

Community
  • 1
  • 1
billc.cn
  • 7,187
  • 3
  • 39
  • 79
  • What `java` version do you use? For me it prints just `sys=sun.misc.Launcher$AppClassLoader@73d16e93, lambda=sun.misc.Launcher$AppClassLoader@73d16e93 sys=interface java.lang.Runnable, lambda=class q34927171.Play$$Lambda$1/918221580` – Andremoniy Jan 21 '16 at 15:19
  • @Andremoniy Sorry, I actually changed the first line of the print code but forgot to update the qustion. – billc.cn Jan 21 '16 at 15:45

2 Answers2

3

The funny thing is, that despite your test is insufficient to determine that, it’s actually true, the classes generated for lambda expressions can get garbage collected as the following code can demonstrate:

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");
    }
}

This prints something alike:

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

Though the numbers may vary.

But for practical purposes, it’s important to understand that the generator facility, which the code above invokes reflectively, is normally triggered via an invokedynamic byte code instruction which will get permanently linked to the result of the first completed bootstrapping process.

In other words, given how the current JRE handles it, as long as the code containing the invokedynamic instruction is reachable, the permanently linked lambda class will persist as well. This matches the behavior demonstrated in Andremoniy’s answer; the code of the line Runnable run = test::toString; could get executed again and will produce an instance of the same class as the first execution, as it’s constructor got permanently linked to it.

But in theory, an alternative JRE implementation could let the bootstrap method return a MutableCallSite and retarget it at a later time, e.g. to produce instances of a more optimized class or to enforce sharing of classes between semantically equivalent lambda expressions. In that cases, existing lambda classes could get garbage collected earlier than their defining class when there are no more lambda instances.

So the answer is, there is indeed a “magical class unloading”, but currently, it’s irrelevant for most practical purposes and, of course, if it happens in a future JVM, it will still retain the semantics as specified by the Java Language Specification. Regardless of how the instance captured by your method reference test::toString gets stored, it will not hinder its collection if the instance of the functional interface is unreachable.


If you are interested in more details, the article “anonymous classes in the VM” by John Rose provides an introduction. You may also search for sun.misc.Unsafe.defineAnonymousClass

Holger
  • 285,553
  • 42
  • 434
  • 765
  • I suddenly realised this may have something to do with the lack of a PermGen in Java 8. Maybe classes are no longer special to the GC. – billc.cn Jan 26 '16 at 13:52
  • 1
    @billc.cn: no, there is no connection. Anonymous classes already exist in Oracle’s JRE 7, but there’s no lambda expression facility using it. But they are used internally by the `MethodHandle` implementation. Besides that, classes are not special to the GC. There’s simply a reference from a `ClassLoader` to each of its defined classes and a reference back from a `Class` to its defining `ClassLoader`. Since anonymous classes aren’t registered at a `ClassLoader` (as the term “anonymous” suggests), they can be collected without the need of a class loader becoming unreachable… – Holger Jan 26 '16 at 14:19
1

I think you are confused about lambda class unloading here, and here is why. You have to test not wr.get(), because dereferencing of this String instance doesn't talk anything about lambda based on it. You have to make WeakReference for class:

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

and try to "wait" while it would be dereferenced:

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

So, this loop will never be finished.

Andremoniy
  • 34,031
  • 20
  • 135
  • 241
  • Even though the question is about unloading classes, I only actually care about breaking links to the referenced data when the lambda goes out of scope. But it seems even for a reference to static data, the reference is in the lambda instance as opposed to its class. – billc.cn Jan 21 '16 at 17:29
  • 2
    @billc.cn: Of course, it’s in the instance rather than the class. The code containing the expression `test::toString` might get evaluated multiple times, with `test` containing different values, hence, capturing a different instance each time. – Holger Jan 21 '16 at 18:15