Search code examples
javajvmjshell

In Java, what does the / (i.e., forward slash) mean in object references like $Lambda$15/0x00000008000a9440@32e6e9c3)?


In JShell, if I do this:

interface Foo { String foo(); }
(Foo) () -> "hi"

I get

|  created interface Foo
$2 ==> $Lambda$15/0x00000008000a9440@32e6e9c3

From the research below, I know the following:

$Lambda = an in-memory reference, as opposed to one persisted to disk by an anonymous inner class (AIC), to the generated bytecode

$15 = an object reference to the AIC

@32e6e9c3 = the sequential number of the object created--at least, in IntelliJ

But what does the / (slash) indicate, as in /0x00000008000a9440?


Solution

  • Summary

    $Lambda$15/0x00000008000a9440 is the name of the created hidden class.

    As it will be shown below, 0x00000008000a9440 is called a suffix.

    The name of the class can be retrieved by calling the java.lang.Class.getName() method. Therefore:

    • For example, the same class names can be retrieved by a Java program (not through JShell).
    • The question does not seem to be about JShell, but about the Java language and the Java Virtual Machine.

    Example program to show name of hidden class

    Program class

    package info.brunov.stackoverflow.question72804142;
    
    import java.util.function.Supplier;
    
    public final class Program {
        public static void main(final String args[]) {
            printRuntimeInformation();
    
            final Supplier<String> supplier1 = () -> "";
            final Supplier<String> supplier2 = () -> "";
            final Supplier<String> supplier3 = () -> "";
            System.out.println(
                String.format("Supplier 1: %s", supplier1.getClass().getName())
            );
            System.out.println(
                String.format("Supplier 2: %s", supplier2.getClass().getName())
            );
            System.out.println(
                String.format("Supplier 3: %s", supplier3.getClass().getName())
            );
        }
    
        private static void printRuntimeInformation() {
            System.out.println(
                String.format(
                    "Java Virtual Machine specification name: %s",
                    System.getProperty("java.vm.specification.name")
                )
            );
            System.out.println(
                String.format(
                    "Java Virtual Machine specification version: %s",
                    System.getProperty("java.vm.specification.version")
                )
            );
            System.out.println(
                String.format(
                    "Java Virtual Machine specification vendor: %s",
                    System.getProperty("java.vm.specification.vendor")
                )
            );
            System.out.println(
                String.format(
                    "Java Virtual Machine implementation name: %s",
                    System.getProperty("java.vm.name")
                )
            );
            System.out.println(
                String.format(
                    "Java Virtual Machine implementation version: %s",
                    System.getProperty("java.vm.version")
                )
            );
            System.out.println(
                String.format(
                    "Java Virtual Machine implementation vendor: %s",
                    System.getProperty("java.vm.vendor")
                )
            );
        }
    }
    

    Program output

    Java Virtual Machine specification name: Java Virtual Machine Specification
    Java Virtual Machine specification version: 18
    Java Virtual Machine specification vendor: Oracle Corporation
    Java Virtual Machine implementation name: OpenJDK 64-Bit Server VM
    Java Virtual Machine implementation version: 18.0.1-ea+10-Debian-1
    Java Virtual Machine implementation vendor: Debian
    Supplier 1: info.brunov.stackoverflow.question72804142.Program$$Lambda$18/0x0000000800c031f0
    Supplier 2: info.brunov.stackoverflow.question72804142.Program$$Lambda$19/0x0000000800c033f8
    Supplier 3: info.brunov.stackoverflow.question72804142.Program$$Lambda$20/0x0000000800c03600
    

    Documentation references

    JEP 371: Hidden Classes

    The hidden classes have been introduced since JDK 15. For additional details, please, refer to the JEP: JEP 371: Hidden Classes.

    Here is an excerpt from the JEP on the hidden class names:

    The major difference in how a hidden class is created lies in the name it is given. A hidden class is not anonymous. It has a name that is available via Class::getName and may be shown in diagnostics (such as the output of java -verbose:class), in JVM TI class loading events, in JFR events, and in stack traces. However, the name has a sufficiently unusual form that it effectively makes the class invisible to all other classes. The name is the concatenation of:

    1. The binary name in internal form (JVMS 4.2.1) specified by this_class in the ClassFile structure, say A/B/C;
    2. The '.' character; and
    3. An unqualified name (JVMS 4.2.2) that is chosen by the JVM implementation.

    For example, if this_class specifies com/example/Foo (the internal form of the binary name com.example.Foo), then a hidden class derived from the ClassFile structure may be named com/example/Foo.1234. This string is neither a binary name nor the internal form of a binary name.

    Given a hidden class whose name is A/B/C.x, the result of Class::getName is the concatenation of:

    1. The binary name A.B.C (obtained by taking A/B/C and replacing each '/' with '.');
    2. The '/' character; and
    3. The unqualified name x.

    For example, if a hidden class is named com/example/Foo.1234, then the result of Class::getName is com.example.Foo/1234. Again, this string is neither a binary name nor the internal form of a binary name.

    The namespace of hidden classes is disjoint from the namespace of normal classes. Given a ClassFile structure where this_class specifies com/example/Foo/1234, invoking cl.defineClass("com.example.Foo.1234", bytes, ...) merely results in a normal class named com.example.Foo.1234, distinct from the hidden class named com.example.Foo/1234. It is impossible to create a normal class named com.example.Foo/1234 because cl.defineClass("com.example.Foo/1234", bytes, ...) will reject the string argument as being not a binary name.

    Javadoc: java.lang.Class#getName() method

    Let's refer to the method documentation: Class (Java SE 15 & JDK 15).

    An excerpt from the documentation:

    public String getName()

    Returns the name of the entity (class, interface, array class, primitive type, or void) represented by this Class object.

    If this Class object represents a class or interface, not an array class, then:

    • If the class or interface is not hidden, then the binary name of the class or interface is returned.
    • If the class or interface is hidden, then the result is a string of the form: N + '/' + <suffix> where N is the binary name indicated by the class file passed to Lookup::defineHiddenClass, and <suffix> is an unqualified name.

    Implementation details: OpenJDK Java Virtual Machine: Hidden class name

    Introduction

    Let's consider the source code of OpenJDK 18.

    Let's refer to the tag: openjdk/jdk18 at jdk-18+37.

    Please, note that:

    • The below execution paths are theoretical: I am using the mentioned source code tag.
    • The below call stacks are real: I am using OpenJDK 18.0.1-ea+10-Debian-1.

    Hidden class name mangling

    Hidden class creation (the java.lang.invoke.MethodHandles.Lookup.defineHiddenClass() method) includes the mangling of its name.

    Let's consider the following call stack:

    "main@1" prio=5 tid=0x1 nid=NA runnable
      java.lang.Thread.State: RUNNABLE
          at java.lang.System$2.defineClass(System.java:2346)
          at java.lang.invoke.MethodHandles$Lookup$ClassDefiner.defineClass(MethodHandles.java:2432)
          at java.lang.invoke.MethodHandles$Lookup$ClassDefiner.defineClassAsLookup(MethodHandles.java:2413)
          at java.lang.invoke.MethodHandles$Lookup.defineHiddenClass(MethodHandles.java:2119)
          at java.lang.invoke.InnerClassLambdaMetafactory.generateInnerClass(InnerClassLambdaMetafactory.java:385)
          at java.lang.invoke.InnerClassLambdaMetafactory.spinInnerClass(InnerClassLambdaMetafactory.java:293)
          at java.lang.invoke.InnerClassLambdaMetafactory.buildCallSite(InnerClassLambdaMetafactory.java:228)
          at java.lang.invoke.LambdaMetafactory.metafactory(LambdaMetafactory.java:341)
          at java.lang.invoke.DirectMethodHandle$Holder.invokeStatic(DirectMethodHandle$Holder:-1)
          at java.lang.invoke.Invokers$Holder.invokeExact_MT(Invokers$Holder:-1)
          at java.lang.invoke.BootstrapMethodInvoker.invoke(BootstrapMethodInvoker.java:134)
          at java.lang.invoke.CallSite.makeSite(CallSite.java:315)
          at java.lang.invoke.MethodHandleNatives.linkCallSiteImpl(MethodHandleNatives.java:279)
          at java.lang.invoke.MethodHandleNatives.linkCallSite(MethodHandleNatives.java:269)
          at info.brunov.stackoverflow.question72804142.Program.main(Program.java:9)
    

    Then let's consider the following execution path as the continuation of the call stack:

    Class<?> java.lang.ClassLoader#defineClass0(ClassLoader loader, Class<?> lookup, String name, byte[] b, int off, int len, ProtectionDomain pd, boolean initialize, int flags, Object classData)
    
    // Native calls below.
    jclass Unsafe_DefineClass0(JNIEnv *env, jobject unsafe, jstring name, jbyteArray data, int offset, int length, jobject loader, jobject pd)
    jclass Unsafe_DefineClass_impl(JNIEnv *env, jstring name, jbyteArray data, int offset, int length, jobject loader, jobject pd)
    JNIEXPORT jclass JNICALL
    jclass JVM_DefineClass(JNIEnv *env, const char *name, jobject loader, const jbyte *buf, jsize len, jobject pd)
    jclass jvm_define_class_common(const char *name, jobject loader, const jbyte *buf, jsize len, jobject pd, const char *source, TRAPS)
    InstanceKlass* SystemDictionary::resolve_from_stream(ClassFileStream* st, Symbol* class_name, Handle class_loader, const ClassLoadInfo& cl_info, TRAPS)
    InstanceKlass* SystemDictionary::resolve_hidden_class_from_stream(ClassFileStream* st, Symbol* class_name, Handle class_loader, const ClassLoadInfo& cl_info, TRAPS)
    InstanceKlass* KlassFactory::create_from_stream(ClassFileStream* stream, Symbol* name, ClassLoaderData* loader_data, const ClassLoadInfo& cl_info, TRAPS)
    InstanceKlass* ClassFileParser::create_instance_klass(bool changed_by_loadhook, const ClassInstanceInfo& cl_inst_info, TRAPS)
    void ClassFileParser::mangle_hidden_class_name(InstanceKlass* const ik)
    

    Let's refer to the piece of source code: jdk18/classFileParser.cpp at jdk-18+37 · openjdk/jdk18:

    void ClassFileParser::mangle_hidden_class_name(InstanceKlass* const ik) {
      ResourceMark rm;
      // Construct hidden name from _class_name, "+", and &ik. Note that we can't
      // use a '/' because that confuses finding the class's package.  Also, can't
      // use an illegal char such as ';' because that causes serialization issues
      // and issues with hidden classes that create their own hidden classes.
      char addr_buf[20];
      if (DumpSharedSpaces) {
        // We want stable names for the archived hidden classes (only for static
        // archive for now). Spaces under default_SharedBaseAddress() will be
        // occupied by the archive at run time, so we know that no dynamically
        // loaded InstanceKlass will be placed under there.
        static volatile size_t counter = 0;
        Atomic::cmpxchg(&counter, (size_t)0, Arguments::default_SharedBaseAddress()); // initialize it
        size_t new_id = Atomic::add(&counter, (size_t)1);
        jio_snprintf(addr_buf, 20, SIZE_FORMAT_HEX, new_id);
      } else {
        jio_snprintf(addr_buf, 20, INTPTR_FORMAT, p2i(ik));
      }
    

    Please, note that the + character is used as the separator.

    Get hidden class name

    The java.lang.Class#getName() method includes the character replacement: + is replaced with /.

    Let's consider the following execution path:

    String java.lang.Class.getName()
    String java.lang.Class.initClassName()
    
    // Native calls below.
    JNIEXPORT jstring JNICALL JVM_InitClassName(JNIEnv *env, jclass cls)
    oop java_lang_Class::name(Handle java_class, TRAPS)
    const char* java_lang_Class::as_external_name(oop java_class)
    const char* Klass::external_name() const
    static char* convert_hidden_name_to_java(Symbol* name)
    

    Let's refer to the piece of source code: jdk18/klass.cpp at jdk-18+37 · openjdk/jdk18:

    // Replace the last '+' char with '/'.
    static char* convert_hidden_name_to_java(Symbol* name) {