Class Loading Mechanism
Follow loading, linking, initialization, parent delegation, dynamic loading, and eventual unloading, with practical examples and initialization caveats.
Loading Phase
The class-loader subsystem makes compiled .class bytecode available to the JVM. Loading locates the binary representation, reads it, and creates the runtime representation together with its java.lang.Class object. A Class object describes a runtime type; it is not an instance created by that class's constructor.
A class passes through loading, linking, and initialization before the uses that require initialization. Linking contains verification, preparation, and resolution. Resolution may be deferred until a symbolic reference is actually needed; the stages should not be mistaken for an entirely eager pipeline.
For the name com.example.Test, a file-based loader commonly looks for com/example/Test.class. Bytecode can also come from archives, generated bytes, or another source supported by a custom loader.
Linking: Verification, Preparation, and Resolution
Verification checks class-format and bytecode constraints, including valid instructions, type-compatible operations, and valid operand-stack use. Invalid bytecode can result in VerifyError; malformed class files can also produce class-format errors. Verification is not a promise that application logic is correct or that every action is authorized.
Preparation creates static-field storage and gives ordinary static fields their default values. The source example's x = 10 and name = "Java" are not those defaults: x initially has 0 and name initially has null; their explicit assignments execute during initialization.
Resolution turns symbolic constant-pool references to classes, fields, and methods into usable runtime references. The source calls these direct references; this does not require Java references to be literal, exposed physical memory addresses. Resolution may be eager or on demand.
class Demo {
static int x = 10;
static String name = "Java";
static final int LIMIT = 100;
}
// Ordinary field defaults during preparation: x == 0, name == null.
// After successful initialization: x == 10, name.equals("Java").
// LIMIT is a constant variable: its ConstantValue is handled specially.Initialization and Static Initialization Order
Initialization executes static field initializers and static blocks in textual order. For the simple Parent/Child example, the superclass initializes before the subclass, so constructing Child prints the parent static-block message first.
The JVM coordinates initialization across threads. A successfully initialized runtime class is not initialized again for every instance. The same binary name defined by different defining loaders denotes different runtime classes and can have separate static state and initialization.
Class initialization and instance construction are different events: the first prepares class-level state, while each successful new expression runs the applicable instance-initialization and constructor chain. If static initialization fails, the class can enter an erroneous state; ordinary subsequent use does not simply retry it.
class Parent {
static { System.out.println("Parent static block"); }
}
class Child extends Parent {
static { System.out.println("Child static block"); }
}
// new Child() prints Parent static block, then Child static block.Example: Loading Is Not Constructor Execution
The document's Example program deliberately separates initialization from construction. The one-argument Class.forName call initializes Example and prints the static-block message before Main reports that the call has returned. The later new Example() prints the constructor message, not a second static-block message.
This distinction matters in frameworks that inspect a Class before deciding whether to create an instance. Merely obtaining a class definition and actually triggering its initialization are separate decisions.
class Example {
static int value = 100;
static { System.out.println("Example class initialized"); }
Example() { System.out.println("Constructor called"); }
}
public class Main {
public static void main(String[] args) throws Exception {
System.out.println("1. Before loading");
Class<?> c = Class.forName("Example");
System.out.println("2. After Class.forName");
Example obj = new Example();
System.out.println("3. Object created");
}
}
// Output:
// 1. Before loading
// Example class initialized
// 2. After Class.forName
// Constructor called
// 3. Object createdClass Loader Types and Hierarchy
The built-in loader hierarchy separates core runtime classes, platform classes, and application classes. Java 9 introduced the platform loader in place of the older extension loader; extension-directory explanations belong to older Java releases.
String.class.getClassLoader() returns null to represent the bootstrap loader, not to indicate that String has no runtime type. Application-loader implementation names in diagnostic output are details, not portable API names.
A custom loader can support plugins, encrypted class bytes, network sources, databases, or reloadable modules. Class identity includes the defining loader as well as the binary name, so two identically named classes from distinct defining loaders are not automatically assignment-compatible.
| Loader | Typical responsibility | Important detail |
|---|---|---|
| Bootstrap | Core Java classes such as String and ArrayList | Represented by null through Class.getClassLoader() |
| Platform | Platform/JDK classes | Java 9+ replacement for the extension loader |
| Application | Application classpath and application classes | Usually loads ordinary project code |
| Custom | Plugin or other application-defined byte sources | Can define an independent runtime type namespace |
System.out.println(String.class.getClassLoader()); // null
System.out.println(Main.class.getClassLoader()); // application loader, typicallyParent Delegation Model
The normal ClassLoader.loadClass algorithm first checks whether the class is already loaded, then delegates to its parent, and only tries its own findClass when delegation cannot find the class. A request travels upward; a successful result or fallback decision travels back toward the requesting loader.
For java.util.List, delegation finds the platform's established definition rather than substituting an application copy. For a plugin-specific type that the parent cannot locate, the custom loader can supply the bytes itself.
Delegation helps keep core types consistent and reduces duplicate definitions. Custom loader policies can differ, so parent-first is the standard model rather than an unavoidable rule for every loader. Restrictions on defining java.* classes also contribute to protecting core definitions.
Dynamic Loading: loadClass() versus Class.forName()
Dynamic loading chooses a class at runtime rather than hard-coding every implementation at compile time. The document connects it to plugins, dependency injection, JDBC drivers, service discovery, reflection, and application servers.
The one-argument Class.forName(name) initializes the named class. ClassLoader.loadClass(name) normally loads without triggering initialization. The three-argument Class.forName overload makes the initialization choice explicit.
Class.forName("com.mysql.cj.jdbc.Driver") is a traditional driver-loading example. Modern JDBC drivers commonly register through service-provider mechanisms, so explicit Class.forName is not universally required.
| API | Initialization behavior | Useful for |
|---|---|---|
| loader.loadClass(name) | Does not normally initialize | Loading or inspecting a definition |
| Class.forName(name) | Initializes the named class | Loading with initialization side effects |
| Class.forName(name, false, loader) | Explicitly suppresses initialization | Inspection with a chosen loader |
ClassLoader loader = Main.class.getClassLoader();
Class<?> loaded = loader.loadClass("com.example.Plugin");
Class<?> inspected = Class.forName("com.example.Plugin", false, loader);
Class<?> initialized = Class.forName("com.example.Plugin", true, loader);Custom FileClassLoader Example
Override findClass rather than replacing the standard loadClass delegation algorithm. The example maps a binary name to a relative .class path, reads the bytes, and passes them to defineClass. IOException is translated to ClassNotFoundException so callers receive the loader API's expected checked failure.
Compile Hello into /tmp/classes and keep it off the parent application's classpath when demonstrating custom definition. Otherwise parent delegation may find Hello first and the printed defining loader will not be FileClassLoader.
getDeclaredConstructor().newInstance() creates the instance and triggers any required initialization. This is a teaching loader, not a secure plugin sandbox: production loading needs trusted paths, concurrency/lifecycle design, and appropriate validation.
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
class FileClassLoader extends ClassLoader {
private final Path classesDir;
FileClassLoader(String classesDir) {
this.classesDir = Path.of(classesDir);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Path file = classesDir.resolve(name.replace('.', '/') + ".class");
try {
byte[] bytes = Files.readAllBytes(file);
return defineClass(name, bytes, 0, bytes.length);
} catch (IOException e) {
throw new ClassNotFoundException("Could not load " + name, e);
}
}
}
// In a caller that declares throws ReflectiveOperationException:
// FileClassLoader loader = new FileClassLoader("/tmp/classes");
// Class<?> type = loader.loadClass("Hello");
// System.out.println(type.getClassLoader());
// Object instance = type.getDeclaredConstructor().newInstance();Initialization Triggers and Compile-Time Constants
Creating an instance, invoking a static method declared by a class, and reading or writing a nonconstant static field declared by it are common active uses that require initialization. Reflective operations such as the one-argument Class.forName also trigger it.
Reading a compile-time constant is different. In the source's Test.A example, A is a static final int initialized by a constant expression; its value can be embedded in the caller, so reading it alone does not initialize Test. Not every static final field is a compile-time constant: an Integer, an object, or an int computed by a method has different rules.
A static member inherited from a superclass is associated with its declaring class. Merely writing a subclass name to access that inherited field is not a general trigger to initialize the subclass.
class Test {
static final int A = 100;
static final Integer BOXED = 100;
static { System.out.println("Test initialized"); }
}
// System.out.println(Test.A); // 100; does not itself initialize Test
// System.out.println(Test.BOXED); // requires Test initializationClass Unloading and Reloadable Systems
A class defined by a collectable loader can become unloadable when its defining loader is no longer reachable through live computation. Retained instances, Class objects, threads, framework caches, and other paths can keep that loader alive.
Dropping one variable is not an unload command, and unloading is not immediate or guaranteed on demand. Bootstrap-defined classes normally remain for the JVM lifetime. A plugin reload usually uses a new loader rather than redefining an already loaded class through the same ordinary loader.
The source associates unloading with plugin replacement, dynamic modules, and shutdown. Process shutdown releases the process's memory, but it should not be confused with observing an individual class-unloading event while the JVM continues running.
Why This Matters: Phase-by-Phase Revision
The book analogy in the source is useful: loading brings a book to the desk, verification checks its structure, preparation sets placeholders, resolution locates references, initialization begins its active setup, and unloading clears it away when no longer needed.
For real debugging, identify the phase rather than describing every failure as a loading error. A missing definition, invalid bytecode, and a failing static initializer have different causes and should lead to different investigations.
| Phase | Primary work | Question to ask |
|---|---|---|
| Loading | Find bytes and create runtime class representation | Which loader and byte source supplied this type? |
| Verification | Check class and bytecode constraints | Is the class structurally and type-correct? |
| Preparation | Create static storage and ordinary defaults | Am I confusing defaults with explicit initializers? |
| Resolution | Bind symbolic references | Which referenced type/member must be resolved? |
| Initialization | Execute static initializers | Which active use triggered the initializer? |
| Unloading | Reclaim eligible loader-associated definitions | What still retains the defining loader? |