JVM Memory Layout
The Java Virtual Machine organizes its memory into several distinct regions during program execution, each with specific purposes and lifecycles.
1. Program Counter Register
Also known as the PC register, this area holds the address of the next instruction to execute for each thread. In a multi-threaded environment, each thread maintains its own independent counter to ensure correct execution flow upon context switching. This region is thread-local and never causes OutOfMemoryError.
2. Java Virtual Machine Stack
Each method invocation creates a stack frame containing:
- Local variables table: stores primitive values directly and object references as heap addresses
- Operand stack: used for expression evaluation and computation
- Runtime constant pool reference: links to class constants
- Return address: tracks where execution should resume after method completion
Stack frames are managed automatically by the JVM. Stack overflow occurs when recursion exceeds limits, while out-of-memory errors happen during stack expansion failures.
3. Native Method Stack
Similar to the JVM stack but serves native methods (implemented in C/C++). HotSpot combines this with the JVM stack for simplicity.
4. Heap Memory
The primary storage area for object instances and arrays. All threads share this region. The garbage collector manages memory allocation here, handling automatic deallocation through collection cycles.
5. Method Area
Shared across all threads, storing:
- Class metadata
- Static variables
- Runtime constant pool
This area also reports OutOfMemoryError when insufficient space exists.
Variable Storage Locations
Variables are categorized as:
- Instance variables: stored in heap memory, unique per object instance
- Static variables: stored in method area, shared among all class instances
- Local variables: stored in stack frames, limited to method scope
- Constants: stored in runtime constant pool, including final variables and string literals
Garbage Collection Mechanisms
Object Reachability Analysis
Rather than counting references, modern JVMs use reachability analysis starting from GC roots:
- Local variables in active stack frames
- Static fields in loaded classes
- Constant references in method areas
- JNI references in native methods
Objects unreachable from these root are eligible for collection.
Garbage Collection Process
Unreachable objects undergo two-step marking:
- First marking: identify objects with no GC root references
- Filtering: determine if finalize() should be called
Objects can override finalize() to prevent deletion, but this mechanism executes at most once per object.
public class FinalizeEscapeDemo {
public static FinalizeEscapeDemo SAVE_HOOK = null;
public void isAlive() {
System.out.println("still alive");
}
@Override
protected void finalize() throws Throwable {
super.finalize();
SAVE_HOOK = this;
}
public static void main(String[] args) throws Throwable {
SAVE_HOOK = new FinalizeEscapeDemo();
SAVE_HOOK = null;
System.gc();
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("dead");
}
SAVE_HOOK = null;
System.gc();
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("dead again");
}
}
}
Collection Algorithms
Mark-Sweep Algorithm
Basic approach: mark unreachable objects then sweep to reclaim space. Creates fragmentation issues.
Copying Algorithm
Divides memory into two spaces. Copies live objects to the other space when one fills up. Eliminates fragmentation but halves available memory.
Mark-Compact Algorithm
Marks objects, then moves survivors to one side before compacting memory. Prevents fragmentation but requires moving data.
Generational Collection
Separates heap into young and old ganerations based on objeect lifespan:
- Young generation uses copying algorithm due to high turnover
- Old generation uses mark-compact due to low turnover
Typical ratio: Eden:Survivor1:Survivor2 = 8:1:1