Medium zalando java

How do JVM class loaders work, and what do ClassNotFoundException and NoClassDefFoundError tell you?

The 30 second answer

Class loaders find class bytes and define classes. Java has three built-in ones: bootstrap for the core platform, platform for the rest of the Java SE modules, and the application loader for the class path. Each loader asks its parent first, so core classes cannot be replaced. ClassNotFoundException means an explicit lookup failed; NoClassDefFoundError means a class that existed at compile time is missing at run time.

Loading, linking, initialising. The JVM specification splits a class's start into loading (find the bytes and create the class), linking (verify, prepare static fields, resolve references) and initialising (run static initialisers). Loading is done by a class loader, and a class's run-time identity is its name plus its defining loader, so the same class name loaded by two loaders gives two different types.

The built-in loaders. The ClassLoader Javadoc lists three:

  • Bootstrap: the virtual machine's built-in loader, "typically represented as null", with no parent. It loads core classes such as java.lang.String.
  • Platform: loads the remaining Java SE platform APIs and JDK classes.
  • System (application): loads classes from the application class path and module path.

Parent delegation. The Javadoc: a loader "will usually delegate the search for the class or resource to its parent class loader before attempting to find the class or resource itself." So an application cannot replace java.lang.String with its own copy.

public class Loaders {
    public static void main(String[] args) throws Exception {
        System.out.println("String     -> " + String.class.getClassLoader());          // bootstrap: null
        System.out.println("java.sql   -> " + java.sql.Connection.class.getClassLoader().getName());
        ClassLoader app = Loaders.class.getClassLoader();
        System.out.println("Loaders    -> " + app.getName() + ", parent " + app.getParent().getName());
        try {
            Class.forName("com.example.Missing");
        } catch (ClassNotFoundException e) {
            System.out.println("forName    -> " + e);
        }
    }
}

Verified with OpenJDK 21.0.13. It prints String -> null, java.sql -> platform, Loaders -> app, parent platform, and java.lang.ClassNotFoundException: com.example.Missing.

Reading the two errors.

  • **ClassNotFoundException** is a checked exception from an explicit lookup such as Class.forName or loadClass: the name is wrong, or the JAR is not on the path. Common with JDBC drivers and plugins.
  • **NoClassDefFoundError** is an error: the class existed when the calling code was compiled, but its definition "can no longer be found" at run time. The JVM specification says that when loading fails because the class cannot be found, it throws NoClassDefFoundError with the ClassNotFoundException as its cause. The other common cause is a static initialiser that threw: the first use fails with ExceptionInInitializerError, later uses with NoClassDefFoundError. Always read the cause.

Where it matters in production. Dependency conflicts (two versions of a library, the wrong one wins), application servers and plugin systems with a loader per application, and memory leaks when something holds a reference to an old loader so none of its classes can be unloaded after a redeploy. A Spring Boot fat JAR uses its own launcher class loader to load the nested JARs inside it; see fat JAR vs normal JAR.

Sources (4)
  1. ClassLoader Javadoc (Java 21): delegation model and built-in class loaders
  2. JVM Specification SE 21, chapter 5: Loading, Linking, and Initializing
  3. NoClassDefFoundError Javadoc (Java 21)
  4. Spring Boot reference: The executable JAR format