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?
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
…