Understanding Class Loading in JVM
While the parent delegation model is widely discussed in JVM class loading, less attention is given to how the JVM determines the starting point of class loading, especially when custom class loaders are involved. Two critical questions arise:
- How does the JVM decide which class loader to start with among multiple peer loaders?
- When a class references another class, which class loader is used to load the referenced class?
ClassLoader Mechanisms in JVM
The JVM class loading system is governed by three primary principles:
- Full Responsibility: When a class loader loads a class, it is also responsible for loading all classes referenced by that class unless explicitly loaded by another class loader.
- Parent Delegation: Before attempting to load a class, a class loader delegates the task to its parent. Only if the parent fails to load the class does the child atempt to load it.
- Caching: The JVM caches all loaded classes. If a class needs to be reloaded after modification, a JVM restart is required since the cache prevents reloading.
Code Verification
Two test classes, ClassA and ClassB, are used to explore class loader behavior:
package com.example;
public class ClassA {
public void printClassLoader() {
System.out.println("ClassA's class loader is: " + getClass().getClassLoader());
new ClassB().printClassLoader();
}
}
package com.example;
public class ClassB {
public void printClassLoader() {
System.out.println("ClassB's class loader is: " + getClass().getClassLoader());
}
}
Custom ClassLoader Implementation
A custom class loader overrides the loadClass method to bypass the default delegatoin model:
static class CustomClassLoader extends ClassLoader {
@Override
public Class> loadClass(String name) throws ClassNotFoundException {
try {
System.out.println("CustomClassLoader loading: " + name);
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
InputStream is = CustomClassLoader.class.getResourceAsStream(fileName);
if (is == null) {
return super.loadClass(name);
}
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
}
}
Main Test Program
The main method demonstrates class loading behavior using both the default and custom class loaders:
public static void main(String[] args) throws Exception {
new ClassA().printClassLoader();
System.out.println("================");
CustomClassLoader loader = new CustomClassLoader();
Class> loadedClass = loader.loadClass("com.example.ClassA");
Object instance = loadedClass.newInstance();
System.out.println(instance.getClass());
System.out.println("instanceof ClassA: " + (instance instanceof ClassA));
System.out.println("================");
Method method = loadedClass.getMethod("printClassLoader");
method.invoke(instance);
}
Execution Output
The output confirms that the custom class loader loads both ClassA and ClassB, adhering to the "full responsibility" rule:
ClassA's class loader is: sun.misc.Launcher$AppClassLoader@18b4aac2
ClassB's class loader is: sun.misc.Launcher$AppClassLoader@18b4aac2
================
CustomClassLoader loading: com.example.ClassA
CustomClassLoader loading: java.lang.Object
class com.example.ClassA
instanceof ClassA: false
================
CustomClassLoader loading: java.lang.System
CustomClassLoader loading: java.lang.StringBuilder
CustomClassLoader loading: java.lang.Class
CustomClassLoader loading: java.io.PrintStream
ClassA's class loader is: com.example.CustomClassLoader@78e03bb5
CustomClassLoader loading: com.example.ClassB
ClassB's class loader is: com.example.CustomClassLoader@78e03bb5