Java learning pathSection 4

Class Loading Mechanism

Follow loading, linking, initialization, parent delegation, dynamic loading, and eventual unloading, with practical examples and initialization caveats.

1

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.

Class lifecycle showing loading, the three linking activities, initialization, and possible later unloading
Linking groups verification, preparation, and resolution; resolution can be lazy.
2

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.

Example
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.
3

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.

Example
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.
4

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.

Example
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 created
5

Class 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.

Class Loader Types and Hierarchy
LoaderTypical responsibilityImportant detail
BootstrapCore Java classes such as String and ArrayListRepresented by null through Class.getClassLoader()
PlatformPlatform/JDK classesJava 9+ replacement for the extension loader
ApplicationApplication classpath and application classesUsually loads ordinary project code
CustomPlugin or other application-defined byte sourcesCan define an independent runtime type namespace
Example
System.out.println(String.class.getClassLoader()); // null
System.out.println(Main.class.getClassLoader());   // application loader, typically
6

Parent 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.

A custom loader delegates upward to application, platform, and bootstrap loaders; fallback returns toward the child
Ask the parent first; define locally only when the standard delegation policy falls back.
7

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.

Dynamic Loading: loadClass() versus Class.forName()
APIInitialization behaviorUseful for
loader.loadClass(name)Does not normally initializeLoading or inspecting a definition
Class.forName(name)Initializes the named classLoading with initialization side effects
Class.forName(name, false, loader)Explicitly suppresses initializationInspection with a chosen loader
Example
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);
8

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.

Example
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();
9

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.

Example
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 initialization
10

Class 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.

11

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.

Why This Matters: Phase-by-Phase Revision
PhasePrimary workQuestion to ask
LoadingFind bytes and create runtime class representationWhich loader and byte source supplied this type?
VerificationCheck class and bytecode constraintsIs the class structurally and type-correct?
PreparationCreate static storage and ordinary defaultsAm I confusing defaults with explicit initializers?
ResolutionBind symbolic referencesWhich referenced type/member must be resolved?
InitializationExecute static initializersWhich active use triggered the initializer?
UnloadingReclaim eligible loader-associated definitionsWhat still retains the defining loader?