Class Loading Interview Questions & Answers
Java class loading interview questions — ClassLoader hierarchy, delegation model, loading/linking/initialisation phases, custom ClassLoaders, class unloading, ClassNotFoundException vs NoClassDefFoundError, and module system changes.
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:
- Verification — checks that the bytecode is structurally correct and won't violate JVM safety guarantees.
- Preparation — allocates memory for static fields and sets them to
their default values (
0,null,false) — not the values in initialiser code yet. - 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.baseand a few core modules; no morert.jar. - Platform ClassLoader (renamed from Extension CL) loads the remaining JDK modules.
- Module boundaries enforce strong encapsulation — a module must
explicitly
exportsa 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
DriverManagerand 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:
- 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. - Bytecode manipulation agents — tools like JRebel or
HotswapAgent use the
java.lang.instrumentAPI to redefine class bytecode at runtime, including structural changes. Requires a-javaagentflag 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 JVM Internals interview questions
More ways to practice
The self-quiz is live. Get notified when mock interviews and new question packs drop.