javajvmclassloaderbytecodejvm-hotspot

Unexpected `NoClassDefFoundError` when optional library is not on classpath, but related code is not executed


I am trying to use the Google Closure Compiler from Java code, but want it to be an optional dependency (present at build time, but may not be around when deployed). The problem I'm having is that I'm getting a NoClassDefFoundError when the library is not on the classpath at runtime. This would of course be expected, were I to execute code referencing library symbols. But all such code is never executed (behind a flag). In fact, the exception occurs when the class containing that code is first accessed.

I have been banging my head against this for a bit now and I think I may be missing some insights into the semantics of class loading in the JVM.

I have reduced the problem down to the following:

import com.google.javascript.jscomp.CompilerOptions;
import com.google.javascript.jscomp.StrictWarningsGuard;

public class Main {
    public static void main(String[] args) {
        System.out.println("HelloWorld");
    }

    private static void thisMethodIsNotCalled() {
        new CompilerOptions().addWarningsGuard(new StrictWarningsGuard());
    }
}

The only code accessing the library is in a method that is never called.

Compile with (jar downloaded from maven):

javac -cp closure-compiler-v20240317.jar Main.java

And when running, I get a NoClassDefFoundError:

$ java Main
Error: Unable to initialize main class Main
Caused by: java.lang.NoClassDefFoundError: com/google/javascript/jscomp/WarningsGuard

I would have expected the program to run successfully. There is no reason why the JVM should attempt to load any of the library classes.

Why does this happen? I have been looking at the JVM Spec and don't see how the loading of Main would cause the JVM to attempt to load symbols referenced in thisMethodIsNotCalled.

More curiously, replacing the contents of thisMethodIsNotCalled with the following, does not cause this error, so this seems to be caused by a very specific interaction.

new StrictWarningsGuard();
new CompilerOptions().addWarningsGuard(null);

Additional Context

This happens with OpenJDK 21 on Ubuntu 24.04:

$ java -version
openjdk version "21.0.3" 2024-04-16
OpenJDK Runtime Environment (build 21.0.3+9-Ubuntu-1ubuntu1)
OpenJDK 64-Bit Server VM (build 21.0.3+9-Ubuntu-1ubuntu1, mixed mode, sharing)

But I believe this not specific to that version, I have also reproduced this with various other versions.


Solution

  • According to this answer, verification of a class can happen

    on the loading of the containing class or its first use

    Please also note that the method signature of the called method is

    addWarningsGuard:(Lcom/google/javascript/jscomp/WarningsGuard;)V
    

    And WarningsGuardis an abstract class (see here). Hence, the JVM wants to "verify" when loading the class Main that StrictWarningsGuard is actually an implementation of WarningsGuard. Therefore it first tries to load WarningsGuard itself and fails with NoClassDefFoundError for WarningsGuard.

    In your second implementation you just pass null to addWarningsGuard(). The JVM does not need to verify at this stage that StrictWarningsGuard is actually an implementation of WarningsGuard.

    To make the dependency optional, you should place all code that depends on the optional library inside another class and load this class via reflection.