If you’re somewhat familiar with sun.misc.Unsafe, you are probably familiar with the Unsafe.defineAnonymousClass API. Before it was removed in JDK 16, this API allowed defining classes with live objects ‘patched’ into the constant pool, by passing them as an Object[] argument when defining the class. This is a powerful idiom that can be used to generate classes that references pre-resolved constant data that is not necessarily representable using other constant pool types.

The functionality of this method was replaced with MethodHandles.Lookup::defineHiddenClassWithClassData in JDK 16. The javadoc for that method says:

A framework can … load the class data as dynamically-computed constant(s) via a bootstrap method

But, if you’re unfamiliar with dynamic constants, it might be unclear how to do this with, say, ASM.

So, here is an example which uses the new class data API using ASM (tested with ASM 9.3 and JDK 18).

package main;

import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.ConstantDynamic;
import org.objectweb.asm.Handle;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Type;

import java.lang.constant.ConstantDescs;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.util.List;

import static org.objectweb.asm.Opcodes.*;

public class Main {

    // an ASM 'Handle' describing the MethodHandles::classDataAt method
    private static final Handle H_CLASS_DATA_AT = new Handle(
            H_INVOKESTATIC,
            Type.getInternalName(MethodHandles.class),
            "classDataAt",
            MethodType.methodType(Object.class, MethodHandles.Lookup.class, String.class, Class.class, int.class)
                    .descriptorString(),
            false
        );

    public static void main(String[] args) throws Throwable {
        // generate a class
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
        cw.visit(V18, ACC_PUBLIC, "main/Widget", null, Type.getInternalName(Object.class), null);

        // generate a `getClassData` method which will be used to demonstrate the 
        // retrieval of class data.
        MethodVisitor mv = cw.visitMethod(ACC_PUBLIC | ACC_STATIC, "getClassData",
                MethodType.methodType(Object.class).descriptorString(), null, null);
        mv.visitCode();
        mv.visitLdcInsn(
            new ConstantDynamic(
                ConstantDescs.DEFAULT_NAME, // mandated name
                Object.class.descriptorString(), // type of the constant
                H_CLASS_DATA_AT, // bootstrap method reference
                0 // bootstrap method arg. The index of the class data object we want to load.
            )
        );
        mv.visitInsn(ARETURN); // return the loaded result
        mv.visitMaxs(-1, -1);
        mv.visitEnd();

        byte[] bytes = cw.toByteArray();
        List<?> data = List.of(new Object() { // some arbitrary data
            @Override
            public String toString() {
                return "Hello from custom object!";
            }
        });
        // define the class, with our class data
        MethodHandles.Lookup lookup = MethodHandles.lookup().defineHiddenClassWithClassData(bytes, data, true);
        // lookup the `getClassData` method we generated above
        MethodHandle MH_getClassData = lookup.findStatic(lookup.lookupClass(), "getClassData",
                                                         MethodType.methodType(Object.class));
        // invoke it to get our class data
        System.out.println(MH_getClassData.invoke()); // prints out "Hello from custom object!"
    }
}

The class data is loaded through an ldc instruction (mv.visitLdcInsn), to which we pass a ConstantDynamic that represents our dynamic constant. This dynamic constant is set up to call the MethodHandles::classDataAt method (H_CLASS_DATA_AT) as a bootstrap method, with a single bootstrap method argument, 0.

The first time this ldc instruction runs, the VM will call this bootstrap method, which will retrieve the object at index 0 of the list of class data which we use when we define the class below that (see the defineHiddenClassWithClassData line). The first three arguments to the bootstrap method, the method handle lookup, name, and type, are provided by the VM, and we provide the index of the object we want to load from the class data list as a constant argument. After resolution, the loaded object will be stored in the constant pool slot associated with the dynamic constant, so the next time it is loaded the resolution step will be skipped, and the stored object is loaded directly instead.

This powerful idiom can be used to put live objects into the constant pool of a generated class. These objects will then be treated by the JIT as constants. This approach can be used for instance to generate code that calls a pre-resolved method handle. Because the method handle will be a constant in the constant pool, the call can be fully optimized.

Thanks for reading