Class loading is the process of finding bytecode for a class and making it available to the JVM. It happens lazily — classes are loaded when first referenced.

Loading Process

  1. Loading — Read .class file bytes, create Class<?> object
  2. Linking
    • Verify — Ensure bytecode is valid and safe
    • Prepare — Allocate memory for static fields, set default values
    • Resolve — Replace symbolic references with direct references (optional)
  3. Initialization — Execute static initializers and assign static fields
  public class Demo {
    static {
        System.out.println("Class initialized"); // runs once on first use
    }
}
  

A class is loaded when:

  • new Demo() is called
  • A static field or method is accessed
  • Class.forName("Demo") is invoked
  • A subclass is loaded (parent loads first)

Class Loader Hierarchy

  Bootstrap ClassLoader (null — loads core JDK classes)
        ↑
Platform/Extension ClassLoader (loads ext dirs, Java modules)
        ↑
Application/System ClassLoader (loads classpath)
        ↑
Custom ClassLoaders
  
  ClassLoader app = ClassLoader.getSystemClassLoader();
ClassLoader platform = app.getParent();
ClassLoader bootstrap = platform.getParent(); // null
  

Delegation Model

Class loaders follow the parent delegation model:

  1. Ask the parent to load the class
  2. If parent cannot find it, load it yourself

This ensures core classes (java.lang.String) are always loaded by the bootstrap loader — preventing spoofing.

  public class LoadDemo {
    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> clazz = Class.forName("java.util.ArrayList");
        System.out.println(clazz.getClassLoader()); // null (bootstrap)
    }
}
  

Custom Class Loader

Load classes from non-standard sources (network, encrypted files, plugins):

  public class PluginClassLoader extends ClassLoader {
    private final Path pluginDir;

    public PluginClassLoader(Path pluginDir, ClassLoader parent) {
        super(parent);
        this.pluginDir = pluginDir;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        Path classFile = pluginDir.resolve(name.replace('.', '/') + ".class");
        try {
            byte[] bytes = Files.readAllBytes(classFile);
            return defineClass(name, bytes, 0, bytes.length);
        } catch (IOException e) {
            throw new ClassNotFoundException(name, e);
        }
    }
}
  

Usage:

  PluginClassLoader loader = new PluginClassLoader(
    Path.of("plugins"), ClassLoader.getSystemClassLoader());
Class<?> pluginClass = loader.loadClass("com.example.MyPlugin");
Object plugin = pluginClass.getDeclaredConstructor().newInstance();
  

Class.forName vs ClassLoader.loadClass

  // Initializes the class
Class<?> c1 = Class.forName("com.example.Service");

// Loads but does NOT initialize
Class<?> c2 = ClassLoader.getSystemClassLoader()
    .loadClass("com.example.Service");
  

Common Issues

Issue Cause
ClassNotFoundException Class not on classpath
NoClassDefFoundError Class was found at compile time but missing at runtime
ClassCastException (same class name) Same class loaded by different class loaders
Metaspace OOM Too many classes loaded dynamically

Best Practices

  • Understand the delegation model before writing custom class loaders
  • Always specify a parent class loader for custom loaders
  • Use plugin architectures (OSGi, Java Module System) for complex class loading needs
  • Be cautious with dynamic class loading — it consumes metaspace