Skip to content

Class Loading Interview Questions & Answers

15 questions Updated 2026-06-20 Share:

Java class loading interview questions — ClassLoader hierarchy, delegation model, loading/linking/initialisation phases, custom ClassLoaders, class unloading, ClassNotFoundException vs NoClassDefFoundError, and module system changes.

Read the in-depth guideJava Class Loading Explained — ClassLoaders, Delegation, and Metaspace Leaks(opens in new tab)
15 of 15

Class loading is the process by which the JVM reads a .class file (bytecode), parses it, and creates the corresponding java.lang.Class object in memory. A class is loaded on demand the first time it is referenced by running code — not upfront at JVM startup.

// When this line executes for the first time:
MyService svc = new MyService();
// The JVM loads MyService.class if it hasn't been loaded yet,
// resolves its dependencies, initialises static fields, then calls new.

Rule of thumb: classes are loaded lazily; the JVM only reads a .class file the first time that class is actually needed.

The JVM ships with a hierarchy of ClassLoaders:

ClassLoader Java 8 name Java 9+ name Loads
Bootstrap Bootstrap CL Bootstrap CL Core JDK classes (java.lang.*, java.util.*) from rt.jar / java.base module
Extension / Platform Extension CL Platform CL javax.*, security providers; in Java 9+ it loads named JDK modules not in java.base
Application App / System CL App CL Classes from the user classpath (-cp) or module path
System.out.println(String.class.getClassLoader());        // null (Bootstrap)
System.out.println(ClassLoader.getSystemClassLoader());   // AppClassLoader
System.out.println(MyApp.class.getClassLoader());         // AppClassLoader

Rule of thumb: null for a class's ClassLoader means it was loaded by the Bootstrap ClassLoader — the JVM doesn't expose a Java object for it.

Before loading a class, a ClassLoader delegates to its parent first. If the parent (or its parent) can load the class, that result is used. Only if the parent chain cannot find the class does the current loader attempt to load it itself. This ensures that core JDK classes loaded by Bootstrap always take precedence.

AppClassLoader.loadClass("com.example.MyClass")
    → delegates to PlatformClassLoader
        → delegates to BootstrapClassLoader
            → not found in rt.jar / java.base
        ← BootstrapClassLoader returns null
    ← PlatformClassLoader returns null
→ AppClassLoader loads from classpath ✓

Rule of thumb: parent-delegation prevents a rogue java/lang/String.class on the classpath from shadowing the real String — the Bootstrap loader always wins for JDK classes.

Loading — reads the binary class data (from a file, JAR, network, etc.) and creates the Class<?> object. The bytecode is not yet verified.

Linking — three sub-steps:

  1. Verification — checks that the bytecode is structurally correct and won't violate JVM safety guarantees.
  2. Preparation — allocates memory for static fields and sets them to their default values (0, null, false) — not the values in initialiser code yet.
  3. Resolution — resolves symbolic references (class names, method descriptors) in the constant pool to actual memory addresses (optional at link time; may be deferred).

Initialisation — runs the class's <clinit> method (static initialisers and static field assignments) exactly once, the first time the class is actively used.

class Config {
    static int LIMIT = Integer.parseInt(System.getenv("LIMIT")); // runs in <clinit>
}

Rule of thumb: static fields are zeroed during preparation, then set to their real values during initialisation — never assume a static field holds its declared value before the class is initialised.

A class in the JVM is identified by both its fully qualified name and its ClassLoader. Two Class<?> objects are the same class only if both the name and the ClassLoader instance match.

This is why frameworks like Java EE application servers, OSGi, and hot-deploy systems use separate ClassLoaders per deployment — each application gets its own com.example.Service, isolated from others.

ClassLoader cl1 = new URLClassLoader(urls);
ClassLoader cl2 = new URLClassLoader(urls);
Class<?> a = cl1.loadClass("com.example.Foo");
Class<?> b = cl2.loadClass("com.example.Foo");
System.out.println(a == b);              // false — different loaders
System.out.println(a.equals(b));         // false
// Casting between them would throw ClassCastException

Rule of thumb: class identity = (fully-qualified name, ClassLoader); the same .class file loaded by two different ClassLoaders produces two incompatible types.

ClassNotFoundException NoClassDefFoundError
Type Checked exception Error (unchecked)
When Class.forName(), ClassLoader.loadClass() called explicitly and the class is not on the classpath Class was available at compile time but missing at runtime (e.g., JAR not deployed)
Cause Developer error: typo in class name, missing dependency at runtime Deployment error: class was compiled against a JAR that is absent at runtime
// ClassNotFoundException — explicit dynamic load
Class.forName("com.mysql.jdbc.Driver"); // throws if MySQL JAR is missing

// NoClassDefFoundError — implicit reference to a class present at compile time
// but missing at runtime — JVM throws this from its own resolution code

Rule of thumb: ClassNotFoundException = you asked for a class by name and it wasn't there; NoClassDefFoundError = the JVM needed a class while loading another and couldn't find it.

Custom ClassLoaders let you load classes from non-standard sources: encrypted JARs, databases, over a network, generated bytecode at runtime, or isolated namespaces (plugins, hot-reload, OSGi bundles).

public class EncryptedClassLoader extends ClassLoader {
    private final Path jarPath;

    EncryptedClassLoader(Path p, ClassLoader parent) {
        super(parent);           // always wire up the parent for delegation
        this.jarPath = p;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] decrypted = decrypt(readBytesFromJar(jarPath, name));
        return defineClass(name, decrypted, 0, decrypted.length);
    }
}

Override findClass(), not loadClass(), to preserve the parent-delegation model. Call defineClass() with the raw bytecode to register the class with the JVM.

Rule of thumb: override findClass(), not loadClass() — breaking delegation opens the door to shadowing core JDK classes.

A class can be unloaded (and its Class<?> object GC'd) only when its ClassLoader instance becomes unreachable. The Bootstrap ClassLoader never becomes unreachable, so core JDK classes are never unloaded. Classes loaded by the Application ClassLoader are effectively permanent too (the loader lives as long as the JVM).

Classes can be unloaded when:

  • They were loaded by a custom ClassLoader (e.g., a plugin loader).
  • All Class objects, all instances, and the ClassLoader itself have no strong references.
URLClassLoader pluginLoader = new URLClassLoader(urls, parent);
Class<?> pluginClass = pluginLoader.loadClass("com.plugin.Plugin");
// ... use the plugin ...
pluginLoader.close();    // no more strong ref to loader or class
pluginLoader = null;
// Now the GC can unload the plugin class and reclaim Metaspace

Metaspace leaks in hot-deploy scenarios (e.g., Tomcat web app reload) are almost always caused by something outside the webapp's ClassLoader still holding a reference to the loader or one of its classes.

Rule of thumb: if Metaspace grows after each hot-deploy, hunt for references to the old ClassLoader held in static fields or thread locals.

The JVM guarantees that a class's <clinit> (static initialiser block and static field assignments) runs exactly once, before any instance of the class is created or any static member is accessed, and is thread-safe — the JVM uses an internal lock so that even if two threads race to initialise the same class, only one runs <clinit> and the other waits.

class Singleton {
    private static final Singleton INSTANCE = new Singleton(); // safe, runs once

    static {
        System.out.println("Initialising Singleton");
    }
}

This guarantee is what makes the Initialization-on-demand Holder idiom thread-safe without explicit synchronisation:

class Holder {
    private Holder() {}
    private static class Inner { static final Holder INSTANCE = new Holder(); }
    static Holder getInstance() { return Inner.INSTANCE; }
}
// Inner is not loaded until getInstance() is first called;
// JVM ensures thread-safe single initialisation

Rule of thumb: static initialisers are guaranteed to run once and safely — this is the foundation of the holder singleton pattern.

Class.forName(String name) loads and initialises the named class using the calling class's ClassLoader. The two-argument overload gives you control over both:

// Simple form: loads + initialises using caller's ClassLoader
Class<?> clazz = Class.forName("com.example.MyPlugin");

// Full control: Class.forName(name, initialize, loader)
Class<?> lazy = Class.forName("com.example.MyPlugin",
                              false,              // don't initialise yet
                              customLoader);      // use this ClassLoader

The classic use case is JDBC driver registration (pre-JDBC 4): Class.forName("com.mysql.jdbc.Driver") loaded and initialised the driver class, whose static block registered it with DriverManager. JDBC 4+ uses ServiceLoader to do this automatically.

Rule of thumb: Class.forName(name) = load + initialise; Class.forName(name, false, loader) = load only, defer initialisation.

Before Java 9, the JDK was a monolithic rt.jar loaded by Bootstrap. Java 9 introduced the module system (JPMS), splitting the JDK into named modules (java.base, java.sql, java.logging, etc.).

Key changes:

  • Bootstrap ClassLoader now loads only java.base and a few core modules; no more rt.jar.
  • Platform ClassLoader (renamed from Extension CL) loads the remaining JDK modules.
  • Module boundaries enforce strong encapsulation — a module must explicitly exports a package for code in other modules to access it; deep reflection into non-exported packages requires --add-opens.
# Allow reflection into java.base internals (needed by some frameworks):
java --add-opens java.base/java.lang=ALL-UNNAMED -jar myapp.jar

Class loading mechanics (parent delegation, defineClass) are unchanged; what changed is the source of classes and what is accessible.

Rule of thumb: JPMS changes where classes come from and what is accessible; the fundamental ClassLoader delegation chain still applies.

java.util.ServiceLoader is a lightweight plugin / SPI mechanism that dynamically discovers and loads implementations of an interface registered in META-INF/services/<interface-name> (or via module-info.java provides in modules).

// In META-INF/services/com.example.Plugin:
// com.example.impl.FooPlugin

ServiceLoader<Plugin> loader = ServiceLoader.load(Plugin.class);
for (Plugin p : loader) {
    p.execute(); // each registered implementation is loaded and instantiated
}

JDBC 4.0 drivers use ServiceLoader to auto-register without Class.forName(). Frameworks like SLF4J, Jackson, and JCA rely on it for extensibility.

Rule of thumb: ServiceLoader is the standard way to decouple an API from its implementation — prefer it over Class.forName() for plugin architectures.

A ClassLoader leak occurs when a custom ClassLoader (e.g., a web app loader in Tomcat) cannot be garbage collected after the app is undeployed because something outside the loader still holds a reference to it or to a class/object loaded by it. Because the loader can't be GC'd, neither can any of its classes — Metaspace grows with each redeploy.

Common causes:

  • ThreadLocal values whose type was loaded by the webapp loader, not cleared before undeployment.
  • Static fields in a shared library (loaded by the parent ClassLoader) holding a reference to an object of a webapp class.
  • JDBC drivers registered with DriverManager and never deregistered.
  • Logging frameworks holding class references in static config.
// Fix: deregister on shutdown
@Override
public void contextDestroyed(ServletContextEvent sce) {
    Enumeration<Driver> drivers = DriverManager.getDrivers();
    while (drivers.hasMoreElements()) DriverManager.deregisterDriver(drivers.nextElement());
    // Also clear any ThreadLocals your code set
}

Detect with a heap dump: look for ClassLoader instances that should have been unloaded; trace their retaining references with Eclipse MAT.

Rule of thumb: every object allocated in a web app that lives beyond the request must be cleaned up in a ServletContextListener.contextDestroyed — otherwise it anchors the ClassLoader forever.

The Bootstrap ClassLoader is implemented in native code (C/C++) inside the JVM itself, not as a Java class. It has no Java object representation, so the JVM signals "loaded by Bootstrap" by returning null from getClassLoader().

System.out.println(String.class.getClassLoader());         // null
System.out.println(int.class.getClassLoader());            // null (primitive)
System.out.println(MyApp.class.getClassLoader());          // sun.misc.Launcher$AppClassLoader

This is a conventional signal, not an error. Code that uses ClassLoaders must handle null and interpret it as Bootstrap:

ClassLoader cl = SomeClass.class.getClassLoader();
ClassLoader toUse = (cl != null) ? cl : ClassLoader.getSystemClassLoader();

Rule of thumb: null ClassLoader = Bootstrap = JDK core class; always null-check before using a class's ClassLoader as an argument.

The standard JVM supports limited HotSwap via JPDA (Java Platform Debugger Architecture) — you can redefine a class's method bodies at runtime in a debug session without restarting. It cannot change class structure (fields, superclasses, interfaces).

For full hot-reload (new fields, new classes, new hierarchies), the two approaches are:

  1. Reload via a new ClassLoader — create a fresh URLClassLoader, discard the old loader and all its instances, let GC clean up. Used by Tomcat, OSGi, and Java EE containers.
  2. Bytecode manipulation agents — tools like JRebel or HotswapAgent use the java.lang.instrument API to redefine class bytecode at runtime, including structural changes. Requires a -javaagent flag at startup.
java -javaagent:hotswap-agent.jar MyApp

Rule of thumb: for dev-time hot reload of full structural changes use a ClassLoader-per-deployment strategy or an instrumentation agent; for minor method-body fixes in a running debugger, JPDA HotSwap is sufficient.

More ways to practice

The self-quiz is live. Get notified when mock interviews and new question packs drop.

or
Join our WhatsApp Channel