Learnitweb

How to Implement a Custom ClassLoader for Hot-Reloading in Java

Hot-reloading is the ability to reload classes during runtime without restarting the JVM. It’s commonly used in development environments (like Spring Boot devtools, JRebel) or plugin systems where new or modified code needs to be picked up dynamically.

At the core of hot-reloading in Java is a custom ClassLoader.

1. Why Use a Custom ClassLoader for Hot-Reloading?

Java loads classes via the ClassLoader mechanism. Once a class is loaded by a classloader, it cannot be unloaded (unless the entire classloader is garbage collected).

1.1 Goal of Custom ClassLoader

To isolate class loading, we:

  • Create a new instance of our custom classloader every time we want to reload the classes.
  • Let the previous instance (and classes) become eligible for GC.

This allows us to:

  • Dynamically reload updated class files
  • Support plugin reloading or module updates

2. Key Concepts

Delegation Model

Java class loading uses a parent delegation model:

  • First, delegate to the parent.
  • If the parent doesn’t find the class, load it yourself.

To enable reloading, we must break this model for specific classes.

Garbage Collection

A class is eligible for GC only when its classloader is GC-ed, which means:

  • No references to the classloader or classes loaded by it must remain.

3. Custom ClassLoader for Hot-Reloading

Let’s write a basic HotReloadingClassLoader that loads .class files from a directory and reloads them when requested.

3.1 Directory Structure

/reloader-demo
  ├── ReloadableClass.java    // class to hot-reload
  ├── Main.java               // launcher with reloading logic
  └── hotload/                // compiled .class files will go here

3.2 Step-by-Step Code

Step 1: Custom ClassLoader

import java.io.*;
import java.nio.file.*;

public class HotReloadingClassLoader extends ClassLoader {
    private final Path classDir;

    public HotReloadingClassLoader(Path classDir) {
        this.classDir = classDir;
    }

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

Explanation

  • Loads class bytes from a .class file in a given directory.
  • Uses defineClass() to define it in the JVM.

Step 2: Define the Reloadable Class

// ReloadableClass.java
public class ReloadableClass {
    public void execute() {
        System.out.println("Original version of ReloadableClass");
    }
}

Compile this class and put the .class file into the hotload/ directory.

Step 3: Main Application with Reload Logic

import java.lang.reflect.Method;
import java.nio.file.Path;
import java.util.Scanner;

public class Main {
    public static void main(String[] args) throws Exception {
        Path classDir = Path.of("hotload");
        Scanner scanner = new Scanner(System.in);

        while (true) {
            System.out.println("Press Enter to reload ReloadableClass...");
            scanner.nextLine();

            // New classloader for each reload
            HotReloadingClassLoader loader = new HotReloadingClassLoader(classDir);

            Class<?> clazz = loader.loadClass("ReloadableClass");
            Object instance = clazz.getDeclaredConstructor().newInstance();

            Method method = clazz.getMethod("execute");
            method.invoke(instance);
        }
    }
}

How it Works

  1. On each Enter key press:
    • A new HotReloadingClassLoader is created.
    • It loads the latest .class file.
    • The old classloader and its classes are eligible for GC.
  2. You can recompile ReloadableClass with changes while the program is running.
    • The changes will reflect after the next reload.

4. Try It Out

Initial Version:

public void execute() {
    System.out.println("Version 1: Hello World");
}

Compile:

javac -d hotload ReloadableClass.java

Run Main:

java Main

Now Modify ReloadableClass.java:

public void execute() {
    System.out.println("Version 2: Hot-reloaded!");
}

Recompile:

javac -d hotload ReloadableClass.java

Press Enter in running Main:

You’ll see the new message, confirming hot-reloading.

5. Important Considerations

a. Class Identity

Even if the class name is the same, Java treats the class loaded by different classloaders as different types.

class1.getClass() != class2.getClass()

b. Memory Leaks

To prevent memory leaks:

  • Do not store references to classes loaded by old classloaders.
  • Clear static variables or singletons.
  • Avoid holding references in long-lived threads.

c. Reload Strategy

Only reload specific packages or classes, not core classes.

6. Advanced Enhancements

a. File Watcher for Auto-Reloading

Use WatchService to detect changes and reload automatically.

b. Selective Delegation

Override loadClass() to delegate selectively:

@Override
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    if (name.startsWith("Reloadable")) {
        return findClass(name);
    }
    return super.loadClass(name, resolve);
}

c. Versioning and Isolation

For plugin systems, load entire JARs via separate classloaders.