Skip to content
QSWEQB
hardConceptScenarioMidSeniorStaff

Where does a class come from at runtime, and what is the difference between ClassNotFoundException and NoClassDefFoundError?

Loaders delegate to their parent before searching themselves, so the JDK always wins a name clash. ClassNotFoundException means a loader was asked for bytes it could not find; NoClassDefFoundError means the JVM needed a class that was there at compile time, or one whose static initialiser had already failed.

5 min readUpdated 2026-07-26

What the interviewer is scoring

  • Whether delegation is described as parent-first, with the consequence that a classpath jar cannot shadow a JDK class
  • Does the candidate tie NoClassDefFoundError to a failed initialisation as well as to a missing artefact
  • That class identity is understood to include the defining loader, not only the fully qualified name
  • Can they name the point at which static initialisers run, and one access that does not trigger them
  • Whether the diagnostic they reach for produces evidence about which loader supplied the class

Answer

Three loaders and a rule about who asks whom

A running JVM has three built-in loaders. The bootstrap loader is part of the VM, is written in native code, and is represented as null when you ask a core class for its loader; since JDK 9 it loads the core modules of the JDK image rather than a rt.jar. The platform loader handles the remaining JDK modules. The application loader handles your classpath and your own modules. Also since JDK 9, the application loader is no longer a URLClassLoader, which broke a surprising amount of older code that cast it to one in order to inject a jar at runtime.

The mechanism that matters is delegation. ClassLoader.loadClass first asks whether it has already defined this name, then hands the request to its parent, and only searches its own locations if the parent came back empty. So resolution goes upward first and the answer comes downward.

sequenceDiagram
    participant C as Calling class
    participant A as Application loader
    participant P as Platform loader
    participant B as Bootstrap loader
    C->>A: loadClass com.acme.Order
    A->>P: delegate upward
    P->>B: delegate upward
    B-->>P: not found
    P-->>A: not found
    A->>A: search the classpath
    A-->>C: defined class

Follow the arrows for a JDK name instead of com.acme.Order and the answer comes back from the bootstrap loader before the application loader ever looks at the classpath. That is why you cannot override a java.lang class by putting your own copy on the classpath, and why a library that ships a package name belonging to the JDK is inert rather than dangerous.

The corollary is the one candidates miss: a class's runtime identity is its fully qualified name plus its defining loader. Two loaders that each define com.acme.Order from the same bytes produce two distinct types. Assigning one to a field of the other throws ClassCastException with the memorably useless message that com.acme.Order cannot be cast to com.acme.Order. This is the normal state of affairs in an application server, an OSGi container or a plugin host, where isolation is the entire purpose of giving each deployment its own loader.

Loading, linking, initialising

Getting a class ready happens in three phases, and being able to name them is what separates a real answer from a description of the classpath.

Loading finds the bytes and produces a Class object. Linking then verifies the bytecode against the structural rules of the VM, prepares the class by allocating its static fields and setting them to their default zero values, and resolves symbolic references to other classes — resolution being permitted to happen lazily, which is why a missing dependency can surface long after startup. Initialisation runs the static field initialisers and static {} blocks, in source order, exactly once, under a lock the JVM holds per class.

Initialisation is lazy and triggered by first active use: instantiating the class, invoking a static method, reading or writing a static field, initialising a subclass, or reflecting on it. The exception worth volunteering is that a static final field of a primitive or String type initialised with a compile-time constant is inlined into the caller's constant pool by the compiler, so reading it does not touch the declaring class at all and does not run its static block. That also means changing such a constant requires recompiling everything that reads it, not just the class that declares it.

The two failures, and why one becomes the other

ClassNotFoundException is a checked exception thrown by a loader that was explicitly asked for a class and could not find the bytes. It comes from Class.forName, loadClass, or a framework doing the same on your behalf from a name in a configuration file. Its presence tells you somebody asked for a class by name and the name did not resolve, so the usual causes are a typo, a missing dependency, or a spring or JDBC driver name in a properties file that no longer matches reality.

NoClassDefFoundError is an Error thrown by the JVM itself during resolution. It means the class was present when the calling code was compiled — the compiler let you reference it — and is absent or unusable now. So it is a deployment-consistency failure rather than a lookup failure: a jar that fell out of the runtime classpath, two versions of a library where the one that wins is missing a method's parameter type, or a shaded artefact that pruned a class somebody reflects on.

The second cause is the one that catches people, and it is the reason to treat these two as a pair rather than as synonyms. If a class's static initialiser throws, the JVM wraps the cause in ExceptionInInitializerError and marks the class erroneous permanently. Every subsequent attempt to use it throws NoClassDefFoundError with no cause attached. So the log shows one ExceptionInInitializerError at the moment the real problem happened, possibly during startup, and then a stream of bare NoClassDefFoundError traces from every later request. Diagnosing the second and never looking for the first is how a team spends a day on a classpath they have not broken. The class is on the classpath. It failed to initialise, once, and the JVM will not try again in that VM's lifetime.

Making the runtime tell you

Reasoning about which artefact supplied a class is guesswork; ask instead.

# Every class as it is loaded, with the source it came from.
java -Xlog:class+load=info -jar app.jar

# Loader tree and Metaspace consumption per loader, on a live process.
jcmd "$PID" VM.classloader_stats

-Xlog:class+load=info prints the code source next to each name, which settles duplicate-jar arguments immediately. For a class you can reach in code, SomeType.class.getClassLoader() and getProtectionDomain().getCodeSource() give the same answer at the point of use.

The loader-level failure mode with the longest tail is the leaked loader. Class metadata lives in Metaspace, which is native memory outside the heap, and a loader's metadata can only be reclaimed when the loader, every class it defined, and every instance of those classes are all unreachable. One ThreadLocal left set on a pooled container thread, one JDBC driver still registered with DriverManager, or one static listener registry in a parent loader is enough to pin the whole graph. Redeploy a dozen times and Metaspace runs out while the heap looks healthy — which is why an out-of-memory error mentioning Metaspace after several redeploys is a class-loading question rather than a tuning question.

Modules change the shape of the errors

If you run on the module path rather than the classpath, the same phases apply but the failures move earlier and get different names. The module graph is resolved at startup, so a missing module or a package split across two modules fails immediately rather than at first use. Reflective access into a package that has not been opened throws IllegalAccessError or InaccessibleObjectException instead of quietly working, which is what the --add-opens flags in so many startup scripts exist to suppress. Knowing that these are the module system's equivalents of the linkage errors above, rather than a separate mystery, is the sign that you understand the phases and not just the symptoms.

Likely follow-ups

  • Two modules load the same class file and a cast between them fails. What is happening?
  • Why does reading a static final int constant not initialise the class that declares it?
  • Which of Class.forName and loadClass runs the static initialiser?
  • Why do repeated hot redeploys exhaust Metaspace rather than the heap?

Related questions

Further reading

class-loadingclassloaderlinkage-errorsmetaspacejpms