2

I'm making an API wrapper library that supports multiple versions of the API. A public class was added in the recent API version. I'm trying to compile the wrapper against the latest API version and make it check in run time if the new class exists. And I'm trying to avoid reflection and instead catch NoClassDefFoundError and set a flag accordingly.

It works until I add a method that returns the class which the non-existent class extends. Then my library fails to load. I mean:

BaseClass exists; ChildClass does not exist; the method uses ChildClass internally. If the method returns BaseClass the library fails to load. If the method returns Object the library loads and the error is deferred and can be caught.

What part of the standard describes this behavior?

Here's a minimal example:

public class TestLoading {

    public static void main(String[] args) throws Exception {
        Class.forName(BaseClass.class.getName());
        // Class.forName(B.class.getName())
        URL classFileB =
            TestLoading.class.getResource(TestLoading.class.getSimpleName() + "$ChildClass.class");
        if (classFileB != null) {
            if (!"file".equals(classFileB.getProtocol())) {
                throw new UnsupportedOperationException();
            }
            Path path = new File(classFileB.getPath()).toPath();
            System.out.println("deleting: " + path);
            Files.delete(path);
        }

        loadMyClass(ObjectReturner.class.getName());
        loadMyClass(BaseClassReturner.class.getName());
    }

    private static void loadMyClass(String name) throws ClassNotFoundException {
        System.out.println("loading: " + name + "...");
        try {
            Class.forName(name);
        } catch (Throwable e) {
            e.printStackTrace(System.out);
        }
    }

    public static class BaseClass {

        static {
            System.out.println("loaded: " + BaseClass.class.getName());
        }
    }

    public static class ChildClass extends BaseClass {

        static {
            System.out.println("loaded: " + ChildClass.class.getName());
        }
    }

    public static class ObjectReturner {

        static {
            System.out.println("loaded: " + ObjectReturner.class.getName());
        }

        public Object getObject() {
            return new ChildClass();
        }
    }

    public static class BaseClassReturner {

        static {
            System.out.println("loaded: " + BaseClassReturner.class.getName());
        }

        public BaseClass getObject() {
            if ("".length() == 10) {
                return new ChildClass();
            } else {
                return null;
            }
        }
    }
}

program output

loaded: snippet.TestLoading$BaseClass
deleting: C:\keep\eclipse\formendix\_pasted_code_\target\classes\snippet\TestLoading$ChildClass.class
loading: snippet.TestLoading$ObjectReturner...
loaded: snippet.TestLoading$ObjectReturner
loading: snippet.TestLoading$BaseClassReturner...
java.lang.NoClassDefFoundError: snippet/TestLoading$ChildClass
    at java.base/java.lang.Class.forName0(Native Method)
    at java.base/java.lang.Class.forName(Class.java:377)
    at snippet.TestLoading.loadMyClass(TestLoading.java:31)
    at snippet.TestLoading.main(TestLoading.java:25)
Caused by: java.lang.ClassNotFoundException: snippet.TestLoading$ChildClass
    at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:606)
    at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:168)
    at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:522)
    ... 4 more
Sergey Tsypanov
  • 3,265
  • 3
  • 8
  • 34
basin
  • 3,949
  • 2
  • 27
  • 63
  • It's unclear what you are asking. You have no asked a single question in your body. What is your goal here? – Sweeper May 16 '23 at 07:36
  • upd: What part of the standard describes this behavior? – basin May 16 '23 at 08:12
  • Does this answer your question? [How do you disable lazy class loading/initialization in Sun's JVM?](https://stackoverflow.com/questions/8497660/how-do-you-disable-lazy-class-loading-initialization-in-suns-jvm) – daniu May 22 '23 at 08:20
  • 1
    “*And I'm trying to avoid reflection and instead catch NoClassDefFoundError*” Really bad idea. This has been discussed in [Does the JVM throw if an unused class is absent?](https://stackoverflow.com/q/41965457/2711488) and [Is it specified in the JVM/JLS that classes in unused code paths will never be loaded?](https://stackoverflow.com/q/69623174/2711488) and [When is a Java Class loaded?](https://stackoverflow.com/q/34259275/2711488) – Holger May 26 '23 at 11:22

2 Answers2

3

Let's have a look into byte code of ObjectReturner.getObject():

// access flags 0x1
public getObject()Ljava/lang/Object;
 L0
  LINENUMBER 43 L0
  NEW org/example/TestLoading$ChildClass
  DUP
  INVOKESPECIAL org/example/TestLoading$ChildClass.<init> ()V
  ARETURN
 L1
  LOCALVARIABLE this Lorg/example/TestLoading$ObjectReturner; L0 L1 0
  MAXSTACK = 2
  MAXLOCALS = 1

And this one is for BaseClassReturner.getObject():

// access flags 0x1
public getObject()Lorg/example/TestLoading$BaseClass;
 L0
  LINENUMBER 49 L0
  NEW org/example/TestLoading$ChildClass
  DUP
  INVOKESPECIAL org/example/TestLoading$ChildClass.<init> ()V
  ARETURN
 L1
  LOCALVARIABLE this Lorg/example/TestLoading$BaseClassReturner; L0 L1 0
  MAXSTACK = 2
  MAXLOCALS = 1

Apart from type of this the only difference is return type, so let's concentrate on it.

Class file verification can require classes to be loaded. The requirements are described in JVMS Chapter 4 (The class File Format). In that chapter we have §4.2.9 claiming

If the method returns a reference type, only an areturn instruction may be used, and the type of the returned value must be assignment compatible with the return descriptor of the method (§4.3.3).

Later on in §4.2.10 there's a requirement for validation of areturn instruction:

An areturn instruction is type safe iff the enclosing method has a declared return type, ReturnType, that is a reference type, and one can validly pop a type matching ReturnType off the incoming operand stack.

So the JVM does the verification of the class instruction by instruction and for areturn it must make sure that referenced type is correct.

For the Object case the type-checking is always obviously trivially correct so it doesn't need to load the actual return type. Could we have any more proofs for this? Remi Forax suggested to return null:

public static class BaseClassReturner {
  public BaseClass getObject() {
    return null;
  }
}

and for this code NoClassDefFoundError is not thrown any more which means that it comes from the bytecode verifier.

Also if we run the code with -Xlog:class+init,class+load we see verification message preceding NCDFE:

[0.216s][info][class,load] org.example.TestLoading$BaseClassReturner source: file:/C:/Users/STsypanov/IdeaProjects/test/target/classes/
loading: org.example.TestLoading$BaseClassReturner...
[0.216s][info][class,init] Start class verification for: org.example.TestLoading$BaseClassReturner
[0.217s][info][class,load] org.example.TestLoading$BaseClass source: file:/C:/Users/STsypanov/IdeaProjects/test/target/classes/
[0.217s][info][class,init] 762 Initializing 'java/lang/ReflectiveOperationException'(no method) (0x0000000800004028)
[0.217s][info][class,init] 763 Initializing 'java/lang/ClassNotFoundException'(no method) (0x0000000800004288)
[0.217s][info][class,init] 764 Initializing 'java/lang/LinkageError'(no method) (0x00000008000044f8)
[0.217s][info][class,init] 765 Initializing 'java/lang/NoClassDefFoundError'(no method) (0x0000000800004758)
[0.217s][info][class,init] Verification for org.example.TestLoading$BaseClassReturner has exception pending 'java.lang.NoClassDefFoundError org/example/TestLoading$ChildClass'
[0.217s][info][class,init] End class verification for: org.example.TestLoading$BaseClassReturner

But if we run the code with -Xlog:class+init,class+load -noverify NCDFE disappears either:

loading: org.example.TestLoading$ObjectReturner...
[0.281s][info][class,init] 761 Initializing 'org/example/TestLoading$ObjectReturner'(no method) (0x0000000800067248)
[0.282s][info][class,load] org.example.TestLoading$BaseClassReturner source: file:/C:/Users/STsypanov/IdeaProjects/test/target/classes/
loading: org.example.TestLoading$BaseClassReturner...
[0.283s][info][class,init] 762 Initializing 'org/example/TestLoading$BaseClassReturner'(no method) (0x0000000800067458)

So the answer to your question is: classes can be loaded lazily at verification step only in case the return type is Object. I've reworked your code to extend from Throwable and return it from BaseClassReturner.getObject() but even in this case verification triggered loading of ChildClass.

Sergey Tsypanov
  • 3,265
  • 3
  • 8
  • 34
0

As for "what part of the standard describes this behavior", I don't think the JLS needs to. In fact, it states that "A full discussion of class loaders and type safety is beyond the scope of this specification. For a more comprehensive discussion, readers are referred to Dynamic Class Loading in the Java Virtual Machine by Sheng Liang and Gilad Bracha" (although that is in the somewhat different context of loading constraints rather than the loading process itself).

It also states that "The Java Virtual Machine starts up by creating an initial class, which is specified in an implementation-dependent manner, using the bootstrap class loader (§5.3.1). The Java Virtual Machine then links the initial class, initializes it, and invokes the public class method void main(String[]). The invocation of this method drives all further execution." So the only guarantee that is given is that the "initial" (ie the class containing main() is loaded.

I found this post that describes what actually triggers the loading of a class; however it's hardly an authoritative source and doesn't state its own source:

Lazy class loaders wait until the first active use of a class before loading and linking its class file. The first active use of a class occurs when one of the following occurs:

  • An instance of that class is created
  • An instance of one of its subclasses is initialized
  • One of its static fields is initialized

In your case, it's "an instance of that class is created" (the exception is thrown when your code first creates a ChildClass instance to return).

daniu
  • 14,137
  • 4
  • 32
  • 53
  • "the exception is thrown when your code first creates a ChildClass instance to return" but Class.forName() doesn't create any instance, it just returns Class object – Sergey Tsypanov May 22 '23 at 10:02
  • @SergeyTsypanov You're right, I wasn't reading the stack trace right. It seems to be due to some transitive class loading because `BaseClassReturner` has a reference to `ChildClass` - that's not covered by the post I linked. But that was only something I came across while looking for what might be going on; the main point is "the JLS doesn't define this specific behavior". – daniu May 22 '23 at 10:50
  • Yep, I've specified in my answer that JVM spec is not clear about it – Sergey Tsypanov May 22 '23 at 15:03