Classloaders in Java
Introduction
In Java, ClassLoaders are responsible for loading Java classes into the Java Virtual Machine (JVM) dynamically at runtime. The Java runtime system does not need to know about files and file systems because of classloaders.
Classloaders operate on the principle of lazy loading—classes are not loaded all at once when the application starts, but rather on demand when they are first referenced by the executing program.
The ClassLoader Hierarchy
Java uses a delegation model for classloaders, meaning that when a classloader is asked to find and load a class, it typically delegates the request to its parent classloader before attempting to find the class itself.
The standard JVM classloader hierarchy consists of three main loaders:
1. Bootstrap ClassLoader
- Role: Loads core Java API classes (e.g.,
java.lang.*,java.util.*fromrt.jaror modules in Java 9+). - Implementation: Usually implemented in native code (C/C++) and acts as the root of the classloader hierarchy.
- Parent: None (or
nullin Java representations).
2. Platform (Extension) ClassLoader
- Role: Loads classes from the platform or extension directories (e.g.,
<JAVA_HOME>/lib/ext). - Implementation: Implemented in Java. In Java 9+, it is known as the Platform ClassLoader and loads Java SE platform APIs and their implementations.
- Parent: Bootstrap ClassLoader.
3. System (Application) ClassLoader
- Role: Loads classes from the application classpath (
-cpor-classpath), environment variables, or standard executable JARs. - Implementation: Implemented in Java. This is the default classloader for most user-defined classes.
- Parent: Platform ClassLoader.
How Class Loading Works
The process of loading a class involves three main phases:
1. Loading
The classloader reads the .class file (bytecode) and generates corresponding binary data. It creates a Class object in the heap memory to represent the class.
2. Linking
Linking connects the loaded class into the runtime state of the JVM. It has three sub-steps:
- Verification: Ensures the bytecode is structurally correct and safe to execute.
- Preparation: Allocates memory for static variables and initializes them to default values.
- Resolution: Transforms symbolic references in the constant pool into direct references.
3. Initialization
Executes the class initialization code (static blocks and static variable assignments). This step happens only once and is thread-safe.
The Delegation Principle (Parent-First)
When a class is requested, the JVM follows a strict delegation process:
- Check Cache: The classloader checks if the class is already loaded. If yes, it returns it.
- Delegate to Parent: If not loaded, it delegates the request to its parent classloader.
- Attempt to Load: If the parent fails to load the class (throws
ClassNotFoundException), the current classloader attempts to read the bytes and load the class.
// Example: Getting the ClassLoader of a class
public class ClassLoaderDemo {
public static void main(String[] args) {
// App/System ClassLoader
System.out.println(ClassLoaderDemo.class.getClassLoader());
// Platform/Ext ClassLoader (Parent of System)
System.out.println(ClassLoaderDemo.class.getClassLoader().getParent());
// Bootstrap ClassLoader (Parent of Ext, usually prints null)
System.out.println(String.class.getClassLoader());
}
}
Custom ClassLoaders
Sometimes the standard classloaders are not enough. You might need a custom classloader to:
- Load classes from a database, network, or encrypted files.
- Implement hot-swapping or module systems (like OSGi or Tomcat webapp classloaders).
Implementing a Custom ClassLoader
To create a custom classloader, you typically extend java.lang.ClassLoader and override the findClass(String name) method.
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
public class CustomNetworkClassLoader extends ClassLoader {
private String classPath;
public CustomNetworkClassLoader(String classPath) {
this.classPath = classPath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] b = loadClassData(name);
if (b == null) {
throw new ClassNotFoundException(name);
}
return defineClass(name, b, 0, b.length);
}
private byte[] loadClassData(String name) {
// Read file from disk, network, or decrypt it
String filePath = classPath + File.separator + name.replace('.', File.separatorChar) + ".class";
try (FileInputStream fis = new FileInputStream(filePath);
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
int data;
while ((data = fis.read()) != -1) {
baos.write(data);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
}
Common Exceptions
ClassNotFoundException: Thrown when an application tries to load in a class through its string name using methods likeClass.forName(), but no definition for the class with the specified name could be found.NoClassDefFoundError: Thrown if the JVM or a ClassLoader instance tries to load in the definition of a class (as part of a normal method call or as part of creating a new instance) and no definition of the class could be found. Usually happens when a class was present during compilation but missing at runtime.ClassCastException: Can happen in complex classloader environments (like application servers). If Class A is loaded by ClassLoader 1, and the same Class A is loaded by ClassLoader 2, they are considered completely different types by the JVM.
Summary
Understanding Java Classloaders is essential for diagnosing ClassNotFoundException and NoClassDefFoundError issues, designing plugin architectures, and understanding how application servers isolate deployments. The parent-delegation model ensures security and prevents core Java API classes from being overwritten by malicious code.