How Java Code Executes on a Computer: The Role of JVM Components
Java source code is compiled into bytecode stored in .class files, which the Java Virtual Machine (JVM) executes. Consider this simple program:
public class Test {
public static void main(String[] args) {
int result = sum(1, 2);
System.out.println("result: " + result);
}
public static int sum(int x, int y) {
return x + y;
}
}
After compilation with javac Test.java, the resulting Test.class contains metadata and bytecode instructions. Tools like javap -v -l Test reveal its internal structure, including the constant pool and method bytecodes.
Program Initialization
Execution begins when the java Test command starts a JVM process. The JVM allocates memory for runtime data areas—heap, metaspace, and thread stacks—and loads essential system classes from the runtime library (rt.jar). It then loads the startup class (Test) and searches for the public static void main(String[]) method. Absence of this method results in a runtime error.
Class Loading
The class loader subsystem loads .class files into the method area, constructing runtime representations such as the runtime constant pool and internal class metadata. Simultaneously, a java.lang.Class object for Test is created in the heap. Class loading occurs lazily—triggered by:
- Instantiation (
new Test()) - Invocation of a static method
- Access to a static field
- Reflection (e.g.,
Class.forName())
Static initializers run during this phase, confirming that loading precedes main() execution.
Heap Memory
The heap is a shared memory region managed by the JVM, storing all object instances, arrays, and Class objects. Garbage collection operates primarily within this space.
Java Virtual Machine Stack
Each thread has a private JVM stack, created upon thread initiation. This stack manages method invocations through stack frames.
Stack Frame Structure
When main() is invoked, a new stack frame is pushed onto the main thread’s stack. Each frame contains:
- Local Variable Table: Holds method parameters and local variables. For
sum(int x, int y), slots 0 and 1 storexandy. - Operand Stack: Temporarily stores intermediate computation values during bytecode execution.
- Dynamic Linking: Resolves symbolic references (from the constant pool) to direct method addresses at runtime. This enables polymorphism—e.g., virtual method dispatch based on actual object type.
- Return Address: Points to the instruction following the method call in the caller, enabling proper control flow after method completion.
Program Counter (PC) Register
Each thread maintains a PC register that tracks the address of the next bytecode instruction to execute. It enables branching, loops, exception handling, and thread scheduling by directing the execution engine.
Execution Engine
The execution engine interprets or compiles bytecode into native machine instructions:
- The interpreter reads bytecode instructions one by one, translating them into platform-specific machine code for CPU execution.
- The Just-In-Time (JIT) compiler identifies frequently executed ("hot") methods and compiles them into optimized native code, caching the result for future calls to improve performance.
This dual approach ensures portability (via interpretation) and efficiency (via JIT compilation).
Native Method Interface and Stack
Java can invoke native methods written in C/C++ via the Java Native Interface (JNI). These calls use a separate native method stack, bypassing the JVM interpreter for direct hardware or OS interaction, which enhances performance for low-level operations.