Class Loaders & Class Loading
Every time you run a Java program, the JVM quietly loads hundreds of classes behind the scenes before your main method even gets a chance to run. The mechanism responsible for finding, loading, and linking those .class files is called the class loading subsystem — one of the most powerful (and underappreciated) parts of the Java platform.
What Is Class Loading?
Class loading is the process by which the JVM reads a .class file from disk (or a JAR, a network location, or even memory), transforms the raw bytes into a java.lang.Class object, and makes it available to the running program.
This happens lazily by default — a class is not loaded until something first refers to it. That means a large application with thousands of classes only loads what it actually uses during a given run.
The three phases of the class loading process are:
| Phase | What happens |
|---|---|
| Loading | Finds the binary representation (.class bytes) and creates a Class object |
| Linking | Verifies bytecode correctness, prepares static fields, and resolves symbolic references |
| Initialization | Executes static initializer blocks and assigns static field values |
Note: “Class loading” is often used loosely to mean all three phases together, but technically loading is only the first step.
The Three Built-in Class Loaders
The JVM ships with three class loaders arranged in a parent-child hierarchy.
1. Bootstrap Class Loader
The bootstrap loader is the root of the hierarchy. It is written in native code (C/C++), not Java itself, so it has no Java object representation — getParent() returns null for its children. It loads the core Java runtime classes from $JAVA_HOME/lib (e.g., java.lang.*, java.util.*).
// The bootstrap loader has no Class object — this prints null
ClassLoader cl = String.class.getClassLoader();
System.out.println(cl); // null
Output:
null
2. Platform Class Loader (Extension Class Loader before Java 9)
This loader handles the platform/extension classes. In Java 8 and earlier it was called the Extension Class Loader and read JARs from $JAVA_HOME/lib/ext. From Java 9 onward, with the introduction of the module system, it became the Platform Class Loader and loads JDK modules not in the bootstrap set.
ClassLoader platformCL = ClassLoader.getPlatformClassLoader();
System.out.println(platformCL);
Output:
jdk.internal.loader.ClassLoaders$PlatformClassLoader@<hashcode>
3. Application (System) Class Loader
This is the loader you interact with most often. It loads classes from your application’s classpath — the directories and JARs you specify with -cp or --class-path. When you reference your own classes or third-party libraries, this loader finds them.
// Your own class — loaded by the application class loader
ClassLoader appCL = ClassLoaders.class.getClassLoader();
System.out.println(appCL);
// Output: jdk.internal.loader.ClassLoaders$AppClassLoader@<hashcode>
The Parent-Delegation Model
When a class loader is asked to load a class, it does not try to find it itself first. Instead it delegates the request up to its parent. Only if the parent cannot find the class does the child attempt to load it. This is called the parent-delegation model (also called parent-first delegation).
Bootstrap Class Loader (root, no parent)
↑ delegates to parent
Platform Class Loader
↑ delegates to parent
Application Class Loader
↑ first recipient of your load request
Here is the logic in pseudocode:
// Simplified view of ClassLoader.loadClass()
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 1. Check if already loaded (cache)
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
// 2. Delegate to parent first
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name); // ask bootstrap
}
} catch (ClassNotFoundException e) {
// parent couldn't find it — fall through
}
if (c == null) {
// 3. Try to find it ourselves
c = findClass(name);
}
}
if (resolve) resolveClass(c);
return c;
}
Why Does Delegation Matter?
- Security: Core classes like
java.lang.Stringare always loaded by the trusted bootstrap loader. A rogue JAR on the classpath cannot replace them. - Uniqueness: The same class loaded by the same loader produces a single
Classobject. Two classes are considered identical only if both their name and their loader match. - Isolation: Different loaders can load different versions of the same class name (useful in application servers and OSGi).
Warning: If two
ClassLoaderinstances load the same.classfile independently, the resultingClassobjects are treated as different types. Casting between them throws aClassCastException, even though the bytecode is identical.
Inspecting Class Loaders at Runtime
You can walk up the loader hierarchy from any class:
public class InspectLoaders {
public static void main(String[] args) {
ClassLoader cl = InspectLoaders.class.getClassLoader();
while (cl != null) {
System.out.println(cl);
cl = cl.getParent();
}
System.out.println("null (bootstrap)");
}
}
Output:
jdk.internal.loader.ClassLoaders$AppClassLoader@...
jdk.internal.loader.ClassLoaders$PlatformClassLoader@...
null (bootstrap)
Writing a Custom Class Loader
Custom class loaders let you load classes from unconventional sources — a database, an encrypted JAR, a remote server, or generated bytecode. You extend ClassLoader and override findClass() (not loadClass(), so you preserve delegation).
import java.io.*;
import java.nio.file.*;
public class FileClassLoader extends ClassLoader {
private final Path dir;
public FileClassLoader(Path dir, ClassLoader parent) {
super(parent);
this.dir = dir;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String fileName = name.replace('.', File.separatorChar) + ".class";
Path classFile = dir.resolve(fileName);
try {
byte[] bytes = Files.readAllBytes(classFile);
return defineClass(name, bytes, 0, bytes.length);
} catch (IOException e) {
throw new ClassNotFoundException("Cannot load " + name, e);
}
}
}
Using it:
ClassLoader loader = new FileClassLoader(
Path.of("/tmp/myclasses"),
ClassLoader.getSystemClassLoader()
);
Class<?> clazz = loader.loadClass("com.example.Greeter");
Object instance = clazz.getDeclaredConstructor().newInstance();
// Use reflection to call methods — see /java/reflection
Tip: Always pass a sensible parent to your custom loader (usually
ClassLoader.getSystemClassLoader()). Skipping the parent breaks delegation and can cause obscureClassCastExceptionor duplicate-class bugs.
Dynamic Class Loading with Class.forName()
You can trigger class loading explicitly at runtime without knowing the class at compile time:
// Loads, links, AND initializes the class (runs static blocks)
Class<?> clazz = Class.forName("java.util.ArrayList");
System.out.println(clazz.getName()); // java.util.ArrayList
// Second argument false = load and link, but DON'T initialize yet
Class<?> lazy = Class.forName("com.example.HeavyService", false,
Thread.currentThread().getContextClassLoader());
This pattern is used by JDBC drivers (which register themselves in a static block), plugin systems, and dependency injection frameworks.
Under the Hood
Class Object Identity
The JVM represents every loaded class as a java.lang.Class instance stored in the Metaspace (renamed from PermGen in Java 8). This object holds the class’s metadata: field descriptors, method tables, constant pool, and a pointer back to its defining class loader.
Two Class objects are the same if and only if (classA == classB) — reference equality, not just name equality. This is why a class loader is part of the class’s identity.
Linking in Detail
Linking has three sub-steps:
- Verification — the bytecode verifier checks that the
.classfile is structurally valid and won’t violate JVM safety guarantees (type safety, stack discipline, etc.). This is a security boundary, not optional. - Preparation — static fields are allocated in Metaspace and set to their default zero values (
0,null,false). Your explicit initializers haven’t run yet. - Resolution — symbolic references in the constant pool (class names, method names) are replaced with direct memory references. This may trigger loading of additional classes.
Class Unloading
A class can be garbage collected only when all three of the following are true:
- No instances of the class exist
- No
Classobject references exist - The defining
ClassLoaderitself is unreachable
Bootstrap-loaded classes are never unloaded. In long-running servers that generate classes dynamically (e.g., JPA proxies, lambda metafactories), Metaspace can fill up and cause an OutOfMemoryError: Metaspace — the modern equivalent of PermGen exhaustion.
Note: You can tune Metaspace with
-XX:MaxMetaspaceSize=256m. Unlike the old PermGen, Metaspace grows from native memory by default with no hard cap unless you set one.
Thread Context Class Loader
The JVM does not always use the calling class’s loader for dynamic lookups. Frameworks like JNDI and JDBC use Thread.currentThread().getContextClassLoader() instead, which lets web containers swap in a per-application loader on each request thread — a key trick for classloader isolation in application servers.
ClassLoader ctxLoader = Thread.currentThread().getContextClassLoader();
// Frameworks use this to break upward delegation when needed
Related Topics
- JVM Architecture — understand the runtime data areas where loaded classes live
- JIT Compilation & Bytecode — what the JVM does with a class after it is loaded
- Garbage Collection Deep-Dive — how the GC interacts with class unloading and Metaspace
- Reflection API — work with
Classobjects and loaded types at runtime - Java 9: Modules (JPMS) — how the module system changed class loader boundaries
- JDK, JRE & JVM — the bigger picture of where class loading fits in the Java platform