Overview of the Class Loading Subsystem
The JVM’s class loading subsystem is responsible for dynamically loading bytecode into memory, verifying its integrity, and preparing it for execution. It operates independently of the execution engine, which determines whether a class can be run. The subsystem does not handle object instantiation — that is the responsibility of the execution engine and heap allocation.
Core Responsibilities
- Loading: Locates and reads .class files from the filesystem, network, or other sources. These files are identified by the magic number
0xCAFEBAFEin their header. - Storage: Once loaded, class metadata — including constant pool data, field/method information, and runtime constants — is stored in the method area.
- Execution Control: The class loader only loads bytecode. Whether the class is executable depends on the execution engine.
- Object Allocation: Instances created via constructors are stored in the heap, not the method area.
Three-Phase Loading Process
1. Loading
- Obtain the binary byte stream of a class using its fully qualified name.
- Convert the byte stream into the JVM’s internal runtime data structure in the method area.
- Create a
java.lang.Classobject to serve as the entry point for accessing the class’s metadata.
2. Linking
Linking consists of three sub-phases: verification, preparation, and resolution.
Verification
Ensures the class file conforms to JVM specifications and poses no security risk. Four checks are performed:
- File format validation: Checks magic number, version, and structure.
- Metadata validation: Validates constant pool references and access flags.
- Bytecode verification: Ensures bytecoed instructions are type-safe and do not violate JVM constraints.
- Symbolic reference validation: Confirms referenced classes, fields, and methods exist.
Preparation
- Allocates memory for static variables and initializes them to default values (e.g.,
0forint,nullfor objects). - Excludes
static finalfields — these are initialized at compile time and assigned their literal values during preparation. - Instance variables are not allocated here; they are created per-object on the heap during instantiation.
Resolution
- Converts symbolic references in the constant pool (e.g.,
CONSTANT_Class_info,CONSTANT_Fieldref_info) into direct references — memory addresses, offsets, or handles. - Resolution may be deferred until first actual use, depending on JVM implementation.
3. Initialization
This phase executes the class initializer method <clinit>, which is automatically generated by the compiler.
<clinit>combines all static variable assignments and static initializer blocks in source order.- It is invoked only once per class, even in multi-threaded environments — the JVM ensures thread safety via synchronization.
- If a class has no static fields or static blocks, no
<clinit>method is generated.
Class Loaders in the JVM
Loader Hierarchy
The JVM supports two types of class loaders:
- Bootstrap ClassLoader: Implemented in native code (C/C++), embedded in the JVM. Loads core Java libraries (
rt.jar,resources.jar). Does not extendjava.lang.ClassLoader. - Custom ClassLoaders: All loaders extending
java.lang.ClassLoaderare considered custom, regardless of origin. Includes: - Extension ClassLoader: Loads libraries from
jre/lib/extorjava.ext.dirs. - Application ClassLoader (System ClassLoader): Loads classes from the classpath (
java.class.path). - User-defined ClassLoaders: Custom implementations for specialized loading needs.
Loader Relationships
Loaders form a parent-child hierarchy, not inheritance. The Bootstrap loader is the top parent. Extension and Application loaders are children of Bootstrap, with Application being the child of Extension.
Code Example: Loading Hierarchy Inspection
public class ClassLoaderDemo {
public static void main(String[] args) {
ClassLoader appLoader = ClassLoader.getSystemClassLoader();
System.out.println("AppClassLoader: " + appLoader);
ClassLoader extLoader = appLoader.getParent();
System.out.println("ExtClassLoader: " + extLoader);
ClassLoader bootLoader = extLoader.getParent();
System.out.println("Bootstrap Loader: " + bootLoader); // null
System.out.println("String class loader: " + String.class.getClassLoader()); // null
}
}
Core vs. Custom Class Loading
- Core Java APIs (
java.*,javax.*,sun.*) are always loaded by the Bootstrap loader. - User-defined classes are loaded by the Application ClassLoader unless overridden.
Bootstrap Loader Paths
The Bootstrap loader loads classes from:
$JAVA_HOME/jre/lib/rt.jar$JAVA_HOME/jre/lib/resources.jar- Other paths specified by
-Xbootclasspath
Example: Inspecting Bootstrap Paths
import sun.misc.Launcher;
import java.net.URL;
public class BootstrapPathDemo {
public static void main(String[] args) {
URL[] urls = Launcher.getBootstrapClassPath().getURLs();
for (URL url : urls) {
System.out.println(url);
}
}
}
Why Use Custom ClassLoaders?
- Isolate application modules (e.g., plugins or microservices).
- Load classes from non-standard sources (network, database, encrypted files).
- Modify class behavior at load time (e.g., bytecode transformation).
- Prevent reverse engineering by obfuscating or dynamically decrypting class files.
Parent Delegation Model
Class loading follows a hierarchical delegation pattern: when a class loader is asked to load a class, it first delegates to its parent. Only if the parenet fails does it attempt to load the class itself.
Why It Matters
- Prevents duplication: Core classes like
java.lang.Stringare loaded once by the Bootstrap loader. - Security: Prevents malicious code from overriding core APIs. Even if a hacker creates
java.lang.Integerin a remote JAR, the Bootstrap loader will load the real one first. - Package integrity: Two classes with the same name from different loaders are treated as distinct types. A class named
java.lang.Virusloaded by a custom loader cannot accessjava.langpackage internals because it belongs to a different runtime package.
Example: Attempting to Override String
Creating a class java.lang.String in your project will not override the system version. The Bootstrap loader will always load the original, making your version unreachable.
Thread Context ClassLoader in SPI
Some frameworks (e.g., JDBC) use Service Provider Interfaces (SPI) where core libraries need to load provider implementations. Since the core libraries are loaded by Bootstrap, they cannot load custom JARs. The solution is the Thread Context ClassLoader — typically the Application ClassLoader — which allows reverse delegation.
Class Identity in the JVM
In the JVM, two Class objects are considered identical only if:
- They have the same fully qualified name (including package).
- They are loaded by the same
ClassLoaderinstance.
Even if two classes originate from the same .class file, if loaded by different class loaders, they are treated as distinct types. The JVM stores the loader reference in the method area for type rseolution and linkage.
Active vs. Passive Class Usage
Class initialization occurs only during active use, which triggers the <clinit> method.
Active Use Cases (Trigger <clinit>)
- Creating an instance via
new. - Accessing or assigning a static field.
- Invoking a static method.
- Using
Class.forName(). - Initializing a subclass.
- Starting the JVM with a main class.
- Resolving a
MethodHandleto a static method (Java 7+).
Passive Use (No Initialization)
- Accessing
static finalconstants (compile-time constants). - Referencing a class via array declaration:
MyClass[] arr; - Using
MyClass.class(class literal). - Subclass referencing parent class without accessing static members.