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
- 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.
- A new
- 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.