Understanding Java Class Loading and the Parent Delegation Model

The process of loading a class in Java involves three distinct phases: loading, linking, and initialization. These stages are typically executed sequentially by the JVM when a class is first referenced, and together they constitute the complete class loading lifecycle.

1. Loading

During this phase, the class loader reads the binary bytecode from a .class file using the fully qualified class name and loads it into the method area of the JVM. This results in the creation of a java.lang.Class instance representing the class.

2. Linking

Linking integrates the binary data into the runtime state of the JVM through three sub-stages: verification, preparation, and resolution.

2.1 Verification

Ensures that the class file conforms to the JVM specification and contains valid bytecode. It includes:

  • Format validation: Checks if the byte stream adheres to the class file format.
  • Semantic analysis: Verifies rules such as ensuring a final class has no subclasses or that a final method isn't overridden.
  • Operation validation: Confirms correct usage of operands on the operand stack and validates symbolic references in the constant pool.

2.2 Preparation

Allocates memory for static variables in the method area and initializes them to their default values (e.g., 0 for integers, null for references). Note that this does not apply to compile-time constants (which are initialized during compilation) or instance variables (allocated at object instantiation time).

2.3 Resolution

Replaces symbolic references in the constant pool with direct references. Symbolic references are abstract descriptions (like names), while direct references are pointers, offsets, or handles pointing direct to the target. This stage covers class/interface resolution, field resolution, method resolution, and interface method resolution.

3. Initialization

This is the final step where actual Java code in the class is executed. The JVM invokes the <clinit> method, which is generated by the compiler from static variable assignments and static blocks (static{}).

Consider the following example:

public class Singleton {
    private static Singleton instance = new Singleton();
    public static int counter1;
    public static int counter2 = 0;

    private Singleton() {
        counter1++;
        counter2++;
    }

    public static Singleton getInstance() {
        return instance;
    }
}

public class Test {
    public static void main(String[] args) {
        Singleton s = Singleton.getInstance();
        System.out.println("counter1=" + s.counter1);
        System.out.println("counter2=" + s.counter2);
    }
}

Execution flow:

  1. Calling Singleton.getInstance() triggers class initialization.
  2. During preparation, static fields are allocated and initialized to defaults: instance=null, counter1=0, counter2=0.
  3. In initialization, static fields are assigned values and static blocks are executed. The constructor runs, incrementing both counters to 1.
  4. Then, counter2 is explicitly assigned 0, so its final value becomes 0. counter1 remains unchanged at 1.

3.1 Order of Variable Initialization

When initializing a class hierarchy, the order is:

  1. Parent class static members and static blocks (in source order).
  2. Child class static members and static blocks (in source order).
  3. Parent class instance members and instance blocks (in source order).
  4. Parent class constructor.
  5. Child class instance members and instance blocks (in source order).
  6. Child class constructor.

This sequence can be remembered as: parent static → child static → parent instance → parent constructor → child instance → child constructor.

4. Class Loaders and the Parent Delegation Model

Java uses a hierarchical class loader system to load .class files:

  • Bootstrap ClassLoader: Built into the JVM, responsible for loading core classes from $JAVA_HOME/jre/lib/rt.jar. It is implemented in native code and cannot be accessed directly via Java.
  • Extension ClassLoader: Loads classes from $JAVA_HOME/jre/lib/ext/*.jar and is implemented as an inner class within sun.misc.Launcher.
  • Application ClassLoader: Loads classes from the classpath defined by the CLASSPATH environment variable or java.class.path system property.
  • Custom ClassLoader: User-defined loaders, such as those used in web servers like Tomcat. Most applications rely on the application class loader.

All class loaders except the bootstrap are subclasses of ClassLoader. Key methods include loadClass(String name, boolean resolve) and findClass(String name). The loadClass method implements the delegation mechanism, while findClass is intended to be overridden for custom loading logic.

5. Parent Delegation Mechanism

The principle: when a class loader receives a loading request, it delegates to its parent instead of attempting to load the class itself. This continues up the hierarchy until the bootstrap class loader is reached. Only if the parent fails to find the class does the current loader attempt to load it.

Purpose: Ensures that core classes like java.lang.Object are loaded consistently across all contexts. Without this model, a malicious or accidental user-defined java.lang.Object could cause type system instability.

5.1 Implementation of Delegation

The loadClass method in ClassLoader performs the delegation:

protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    Class<?> clazz = findLoadedClass(name);
    if (clazz == null) {
        try {
            if (parent != null) {
                clazz = parent.loadClass(name, false);
            } else {
                clazz = findBootstrapClassOrNull(name);
            }
        } catch (ClassNotFoundException e) {
            // Ignore; continue to try own loading
        }
        if (clazz == null) {
            clazz = findClass(name);
        }
    }
    if (resolve) {
        resolveClass(clazz);
    }
    return clazz;
}

The resolve paramter determines whether symbol resolution occurs during loading.

6. Custom Class Loader

To create a custom class loader, extend ClassLoader and override findClass(String name).

Here’s an example that compiles .java files on demand before loading:

public class DynamicCompilerClassLoader extends ClassLoader {
    private boolean compile(String sourceFile) throws IOException {
        System.out.println("Compiling " + sourceFile + "...");
        Process proc = Runtime.getRuntime().exec("javac " + sourceFile);
        try { proc.waitFor(); } catch (InterruptedException e) { e.printStackTrace(); }
        return proc.exitValue() == 0;
    }

    private byte[] readBytes(String fileName) throws IOException {
        File file = new File(fileName);
        byte[] buffer = new byte[(int) file.length()];
        try (FileInputStream fis = new FileInputStream(file)) {
            int bytesRead = fis.read(buffer);
            if (bytesRead != buffer.length) throw new IOException("Incomplete read");
        }
        return buffer;
    }

    @Override
    protected Class<?> findClass(String className) throws ClassNotFoundException {
        String path = className.replace('.', '/') + ".class";
        File classFile = new File(path);
        File sourceFile = new File(className.replace('.', '/') + ".java");

        if (sourceFile.exists() && (!classFile.exists() || sourceFile.lastModified() > classFile.lastModified())) {
            if (!compile(sourceFile.getPath()) || !classFile.exists()) {
                throw new ClassNotFoundException("Failed to compile: " + className);
            }
        }

        if (classFile.exists()) {
            try {
                byte[] bytes = readBytes(path);
                return defineClass(className, bytes, 0, bytes.length);
            } catch (Exception e) {
                throw new ClassNotFoundException(className, e);
            }
        }
        throw new ClassNotFoundException(className);
    }

    public static void main(String[] args) throws Exception {
        if (args.length < 1) {
            System.err.println("Usage: java DynamicCompilerClassLoader ClassName");
            return;
        }
        String mainClass = args[0];
        String[] programArgs = new String[args.length - 1];
        System.arraycopy(args, 1, programArgs, 0, args.length - 1);

        DynamicCompilerClassLoader loader = new DynamicCompilerClassLoader();
        Class<?> clazz = loader.loadClass(mainClass);
        Method mainMethod = clazz.getMethod("main", String[].class);
        mainMethod.invoke(null, (Object) programArgs);
    }
}

A simple test class:

public class Hello {
    public static void main(String[] args) {
        System.out.println("Hello from dynamically compiled class!");
    }
}

Run without prior compilation:

java DynamicCompilerClassLoader Hello

This approach enables dynamic loading of freshly compiled classes.

Tags: java Class Loading ClassLoader Parent Delegation JVM

Posted on Sat, 16 May 2026 10:27:57 +0000 by darence