How Does the JVM Resolve Dependencies During Class Loading?
When the JVM loads a class, it also needs to resolve the dependencies that the class relies on (e.g., other classes, interfaces, methods, and fields). This process occurs in the Linking phase, specifically during the Resolution step, where the JVM replaces symbolic references with direct references.
Dependency Resolution Process
1. Symbolic References in the Constant Pool
- Each compiled Java class contains a constant pool that stores symbolic references to other classes, methods, and fields.
- These references are not actual memory addresses but rather fully qualified class names, method names, and field names.
2. Class Loader Delegation Model
- When the JVM needs to resolve a dependency, it follows the parent delegation model:
- The current class loader first checks whether the class has already been loaded.
- If not found, it delegates the request to its parent class loader (up to the Bootstrap ClassLoader).
- If no parent can load the class, the current class loader attempts to load it from its source (e.g., classpath, module path).
- If the class is still not found, a
ClassNotFoundException
is thrown.
3. Verification (Ensuring Bytecode Integrity)
- Before linking dependencies, the JVM verifies that the bytecode is valid and follows Java specifications.
- The Bytecode Verifier checks for:
- Proper structure (e.g., valid method calls, correct operand stack usage).
- No illegal memory access.
- No stack overflow or underflow.
4. Resolution (Replacing Symbolic References)
- Once a class is found and verified, the JVM resolves its dependencies by replacing symbolic references with direct references:
- Class resolution: Finding the referenced class and ensuring it is loaded.
- Method resolution: Identifying the actual method implementation in the class hierarchy.
- Field resolution: Determining the memory location of a field in the class.
- Example:
public class A {
B obj = new B(); // JVM must resolve class B when loading A
}
When A
is loaded, the JVM does not immediately load B
.Only when A
first accesses B
(e.g., creating an instance), the JVM resolves and loads B
.
5. Class Initialization (Executing Static Blocks)
- Once dependencies are resolved, the JVM initializes the class:
- Static variables are assigned explicit values.
- Static blocks are executed.
Example of Dependency Resolution
Consider the following Java classes:
Class A (Depends on B)
public class A {
static {
System.out.println("Class A initialized");
}
B obj = new B();
}
Class B
public class B {
static {
System.out.println("Class B initialized");
}
}
Main Class
public class Main {
public static void main(String[] args) {
A a = new A(); // This triggers loading and resolving dependencies
}
}
Execution Flow
- The JVM loads
Main
, thenA
. - When
A
is loaded, it does not yet loadB
. - When
new B()
is executed inA
, the JVM resolves and loadsB
. - Both classes are initialized, and their static blocks execute.
Expected Output
Class A initialized
Class B initialized
This shows that B
was loaded and initialized only when needed.
Key Takeaways
- Class dependencies are resolved lazily (when they are first accessed).
- The delegation model ensures that higher-level class loaders (e.g., Bootstrap) get priority in loading classes.
- Resolution phase replaces symbolic references with direct memory references.
- Verification ensures security by checking bytecode integrity before linking.
- Initialization happens only once per class when it is first used.
Would you like a deep dive into a specific part of this process?