Sitemap

Overview of Java class loading

8 min readMar 31, 2026

--

At runtime, when your code tries to executre new MyClass() , the JVM needs to find, check and initialize the bytecode for that class. This is the responsibility of the Class loader subsystem.

Its understanding is key not only for software engineer interviews, but for also the debugging of ClassNotFoundException and NoClassDefFoundError .

JVM guarantees that before an object is created, the class is fully initialized. Loading, linking and initialization happen on first “active use” of a particular class.

What active use means?

  • new MyClass()
  • static method invocation
  • access to a static field (non-constant)
  • reflection (in most cases)
  • subclass initialization
  • Class.forName("...") initialization

There are three stages in the class loading process: loading, linking and initialization

Let’s start with the loading phase and the types of class loaders.

Loading consists of the JVM finding the according .class file (from disk, network or generated), reads the bytes and creates an accordingjava.lang.Class object on the heap as the result of this stage.

Classloader hierarchy

Java uses three class loaders, which are ordered in a hierarchy. Each loader delegates to its parent before searching on its own for a class.

Parent delegation model and class loader hierarchy

Parent Delegation Model — Before attempting to load a class itself, a class loader first delegates the request to its parent. This delegation continues up the hierarchy until it reaches the bootstrap class loader, which has no parent. If the class cannot be found by any parent, the original class loader attempts to load it.

The JVM uses the ClassLoader.loadClass() method for loading the class into memory. It tries to load the class based on a fully qualified name.

Bootstrap class loader — the root of the Java classloader hierarchy. The most privileged class loader in the whole JVM. This built entirely into the JVM itself. Unlike all other class loaders it is not a Java class. It is implemented in C++. Calling someClass.getClassLoader() on a bootstrap-loaded class returns null .

System.out.println(String.class.getClassLoader());        // null
System.out.println(Object.class.getClassLoader()); // null
System.out.println(int[].class.getClassLoader()); // null

The Bootstrap ClassLoader exists as a security boundary. Because it’s native code with no Java representation, user code cannot replace it or intercept it. If you try to put your own java/lang/String.class on the classpath, the delegation model ensures the Bootstrap loader’s version always wins. This prevents malicious code from substituting core platform classes.

It loads standard packages like java.lang , java.net , java.util , java.io and so on. These packages are present inside the rt.jar file and other core libraries present in the $JAVA_HOME/jre/lib directory (up until Java 8 inclusive).

In modern Java versions (Java 9 and above), these classes are no longer loaded from a single rt.jar file. Instead, they are part of the modular runtime image, stored in the lib/modules file within the JDK. This file uses a specialized format (jimage) rather than a standard JAR archive.

The Platform ClassLoader ( known as Extension ClassLoader ≤ Java 8) is a child of the Bootstrap ClassLoader and the parent of the Application ClassLoader. It is responsible for loading standard Java platform classes that are not part of the core java.base module, but are still included in the Java platform.

Since Java 9 all standard library extensions are organized into modules within the runtime image (lib/modules) which is a jimage archive.

Up until Java 8 inclusive those extensions were present as jar files in $JAVA_HOME/jre/lib/ext directory and the directories noted in java.ext.dir system property.

The Extension ClassLoader loaded JARs from a directory anyone could write to; the Platform ClassLoader loads sealed JDK modules from a controlled source.

The extension mechanism was designed for optional platform libraries — things like cryptography providers, JDBC drivers, or XML parsers that vendors wanted to ship as “part of the platform” without being in rt.jar. You could drop a JAR into ext/ and it would be available to every application on that JVM, without touching the classpath.

This was convenient but widely abused — developers would drop application JARs into ext/ to sidestep classpath issues, causing subtle versioning nightmares across applications sharing a JVM. It was considered a design mistake in hindsight.

Application ClassLoader — This is the final class loader and subclass of Platform / Extension ClassLoader. This loads the files present in the classpath. It is the closest class loader to your application code in the build-in class loader hierarchy. By default the class path is set to be the current directory of the application. The classpath could also be modified by adding the -classpath or -cp option.

It scans classpath entries left to right.
The first match wins — which is why classpath ordering matters when
you have duplicate class names across JARs.

# It searches these, in order
java -cp lib/foo.jar:lib/bar.jar:build/classes com.example.Main

# Or via environment variable (lower precedence)
export CLASSPATH=lib/foo.jar:lib/bar.jar
java com.example.Main

# Java 9+ module path is handled differently (see below)
java --module-path mods -m com.example/com.example.Main
```

If a parent class loader is unable to find a class, it delegates back the work to a child classloader. If the last child class loader isn’t able to load the class either, it throws NoClassDefFoundError or ClassNotFoundException .

NoClassDefFoundError is emmited when a class was present at compile time but was not present during runtime initialization.

ClassNotFoundException — Dynamic load failed. A classloader couldn’t find a class. Gets thrown by a Class.forName(...) call.

Linking — this is the phase after class loading. It’s the JVM’s quality control and wiring stage — three distinct sub-phases that validate the class, allocate memory for static fields, and connect symbolic names to real runtime references. Those phases transform bytecode into something the JVM can safely use.

Those three phases are called verification, preparation and resolution.

Verification — the first sub-phase answers one question -is this bytecode safe to run?

When a .class file arrives — from disk, network, a code generator, or anywhere else — the JVM cannot assume it was produced by a legitimate Java compiler. It could be hand-crafted, corrupted, or deliberately malicious. Verification is the firewall.

File format check — happens during Loading, technically, but is part of the verification story. Confirms the magic number (0xCAFEBABE), the major/minor version numbers are supported by this JVM, and the constant pool entries are structurally valid.

Get Tsvetomir Denchev’s stories in your inbox

Join Medium for free to get updates from this writer.

Metadata verification — checks the class’s declared structure against Java language rules. Every class except java.lang.Object must have a superclass. A final class must not be subclassed. A final method must not be overridden. Abstract classes must not be instantiated.

Bytecode verification — the most expensive step. The verifier performs dataflow analysis on every method’s bytecode, simulating execution without actually running it. It confirms: every branch target is a valid instruction; the operand stack never overflows or underflows; type safety holds at every instruction (you can’t pass a long where an int is expected); local variables are initialized before use; object references are never used as primitives and vice versa.

Symbolic reference verification — checks that references in the constant pool actually resolve to things that exist and are accessible. If your class references com.example.Foo.bar(), verification confirms that class exists, that method exists on it, and that your class has the access rights to call it.

After verification, Preparation does something narrow and specific: it allocates memory for static fields and sets them to their default zero values.

Not the values you wrote in source code — the JVM defaults:

public class Config {
static int timeout = 30; // → 0 during Preparation
static boolean enabled = true; // → false during Preparation
static String host = "localhost"; // → null during Preparation
static double ratio = 1.5; // → 0.0 during Preparation
static long[] ids = new long[10]; // → null during Preparation
}
// Your actual values (30, true, "localhost", etc.) are assigned
// later, during Initialization, when the static initializer runs.

There is one exception: static final fields with a compile-time constant value (a primitive or String literal, computable at compile time) are set to their actual value during Preparation, not Initialization.

Resolution — in this phase, symbolic references are replaced with direct references present in the runtime constant pool.

For example, if you have references to other classes or constant variables present in other classes, they are resolved in this phase and replaced with their actual references.

Initialization is the final phase before a class becomes usable — the moment the JVM hands control to your actual Java code for the first time. Initialization is where your static blocks and field assignments actually execute.

The compiler collects every static field assignment and every static { } block in the class, merges them into a single synthetic method called <clinit> (class initializer), and preserves their textual order from the source file.

public class Config {
static int timeout = 30; // (1)

static { // (2)
System.out.println("first block");
}

static String host = resolveHost(); // (3)

static { // (4)
System.out.println("second block: " + host);
}

static String resolveHost() {
return System.getProperty("app.host", "localhost");
}
}

The generated <clinit> executes in this exact order: assign 30 to timeout, print "first block", call resolveHost() and assign result to host, print "second block: localhost". Order is strictly top-to-bottom — it matches what you see in source.

Non-static (instance) fields are not initialized during class initialization. They are assigned during object creation, as part of the constructor execution flow. Upon a call to new MyObject() JVM does the following:

  1. ensure class is initialized ( <clinit> got run ) — only the first time the class is loaded
  2. allocate memory for object
  3. set all fields to default values (0, null, false etc.)
  4. execute instance field initializers + instance blocks
  5. execute constructor ( <init> )

Here’s a quick reference for a few interview questions that might pop up at engineering interviews:

What’s Parent delegation model and why it exists?

Each loader asks its parent if it can find a class before searching on its own. It guarantees that the system classes from the standard library are the proper ones coming from the JDK, so that they cannot be exchanged by malicious intent.

What is the difference between NoClassDefFoundError and ClassNotFoundException ?

ClassNotFoundException is a checked exception thrown upon dynamic class loading ( Class.forName(...) ).

NoClassDefFoundError is a runtime error. The class was present at compile time but was not present now upon loading.

When a class does get initialized?

Upon “active usage”: new keyword, access to a static field/method, Class.forName().

Could two classes with the same fully-qualified class names (package name + class name) be loaded by the JVM?

Yes — if they were loaded by different class loaders. The JVM identifies a class by Class loader + fully-qualified class name (FQCN), not only by FQCN. Casting inbetween them throws a ClassCastException even if their bytecode is identical.

When would you prefer creating your own custom class loader?

When you have some plugin architecture (loading JARs at runtime), hot-reload of classes without restart, loading encrypted / compressed classes or isolating specific versions of a library (OSGi, Tomcat webapp class loading).

Quick reference

The three built-in class loaders

Bootstrap — $JAVA_HOME/lib, implemented in C++, its parent class loader is null

Extension/Platform — $JAVA_HOME/lib/ext or Java 9+ platform modules

Application — -classpath, default loader for “user” classes

Linking под-фазите

Verification — checks if the bytecode is valid

Preparation — assign static fields → default values(0, null, false)

Resolution symbolic references in the constant pool → direct references

Java 9+ changes

With the introduction of JPMS (Project Jigsaw) Extension ClassLoader got renamed to Platform classloader and works with modules.

--

--