Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

MethodHandles.lookup().defineClass retention

Tags:

java

java-9

MethodHandles.Lookup.defineClass generates a new class from a byte array at runtime.

Under what circumstances can the returned class be garbage collected? Is it retained for the lifetime of the classloader associated with the Lookup object, or can it be garbage collected if the Class object is no longer referenced?

like image 343
Louis Wasserman Avatar asked Oct 08 '18 22:10

Louis Wasserman


1 Answers

The classes created via MethodHandles.Lookup.defineClass are registered at the defining class loader like any other class and can be referenced by name like ordinary classes, in contrast to defineHiddenClass(...) introduced with JDK 15 (see the end of the answer). They may even supersede statically compiled classes when being registered before those classes have been resolved, like with the following example:

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(("Êþº¾\0\0\0005\0\26\11\0\11\0\12\10\0\13\12\0\14\0"
        +"\15\7\0\16\7\0\17\1\0\3foo\1\0\3()V\1\0\4Code\7\0\20\14\0\21\0\22\1\0"
        +"\30hello from dynamic class\7\0\23\14\0\24\0\25\1\0\4Lazy\1\0\20java/"
        +"lang/Object\1\0\20java/lang/System\1\0\3out\1\0\25Ljava/io/PrintStream;"
        +"\1\0\23java/io/PrintStream\1\0\7println\1\0\25(Ljava/lang/String;)V\6\0"
        +"\0\4\0\5\0\0\0\0\0\1\0\11\0\6\0\7\0\1\0\10\0\0\0\25\0\2\0\0\0\0\0\11²\0"
        + "\1\22\2¶\0\3±\0\0\0\0\0\0").getBytes(StandardCharsets.ISO_8859_1));

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

Try it online

This example dynamically defines a Lazy class whose foo() method will print hello from dynamic class when being invoked.

On JVMs like HotSpot, where the symbolic reference “Lazy” gets resolved lazily, i.e. right when the attempt to invoke Lazy.foo() is made, this will end up at the dynamically defined class. For JVMs eagerly resolving symbolic references, the Lazy class would already exist when MethodHandles.Lookup.defineClass is invoked and therefore, a LinkageError with a message like “attempted duplicate definition for Lazy” will be thrown.

In other words, these dynamically generated classes share the same name space (class loading context) as the statically compiled classes. Being registered at the class loader like an ordinary class, they can only get garbage collected when the defining class loader becomes unreachable (including all of its defined classes), like with ordinary classes.


JDK 15 introduced defineHiddenClass(...) which can be used to define classes not registered at a class loader and also capable of accessing private members of the lookup class, similar to the older non-standard feature sun.misc.Unsafe.defineAnonymousClass.

The behavior is controlled by the ClassOption parameter(s).

  • NESTMATE enables access to private members of the lookup class and its nest members (typically inner classes)

  • STRONG prevents the class from being unloaded until the class loader became unreachable, though I have no idea why anyone should ever want this. Without this option, the hidden class can get garbage collected and unloaded as soon as no reference to it exists.

Here is an adapted version of the example:

public class LookupHiddenClass {
    public static void main(String[] args) throws Throwable {
        var lookup = MethodHandles.lookup();
        lookup = lookup.defineHiddenClass(("Êþº¾\0\0\0005\0\26\11\0\11\0\12\10\0\13\12"
        +"\0\14\0\15\7\0\16\7\0\17\1\0\3foo\1\0\3()V\1\0\4Code\7\0\20\14\0\21\0\22\1\0"
        +"\27hello from hidden class\7\0\23\14\0\24\0\25\1\0\4Lazy\1\0\20java/"
        +"lang/Object\1\0\20java/lang/System\1\0\3out\1\0\25Ljava/io/PrintStream;"
        +"\1\0\23java/io/PrintStream\1\0\7println\1\0\25(Ljava/lang/String;)V\6\0"
        +"\0\4\0\5\0\0\0\0\0\1\0\11\0\6\0\7\0\1\0\10\0\0\0\25\0\2\0\0\0\0\0\11²\0"
        + "\1\22\2¶\0\3±\0\0\0\0\0\0").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");
    }
}

While there’s no guaranty that System.gc() will perform an actual garbage collection nor that it does collect a particular object, with OpenJDK’s default configuration it reproducibly prints

hello from hidden class
class collected
like image 80
Holger Avatar answered Sep 18 '22 10:09

Holger