The Java Memory Model (JMM)
Introduction
Understanding how Java manages memory is crucial for writing efficient, high-performance, and thread-safe applications. The Java Memory Model (JMM) describes how threads in the Java programming language interact through memory. It defines the rules for when changes made by one thread become visible to another.
Beyond concurrency, understanding memory allocation (Heap vs. Stack) and garbage collection is essential to prevent memory leaks and optimize application performance.
Core Memory Areas: Heap, Stack, and Metaspace
The JVM divides memory into several distinct regions, each serving a specific purpose during runtime.
1. The Stack
Each thread in a Java application has its own private Stack.
- What it stores: Local variables (primitives like
int,boolean), method calls (frames), and reference variables (pointers to objects on the Heap). - Behavior: Operates on a LIFO (Last-In-First-Out) basis. Memory is automatically allocated when a method is called and deallocated when the method returns.
- Characteristics: Very fast access, small in size, and thread-safe (since it's isolated to a single thread).
2. The Heap
The Heap is a shared memory area used by all threads in a Java application.
- What it stores: All objects and their instance variables (regardless of whether they are primitives or references) are stored here. Examples include arrays, custom classes, and standard library objects.
- Behavior: Memory allocation is dynamic. Since the Heap is shared, thread synchronization is necessary when multiple threads access the same object.
- Characteristics: Slower than the Stack, much larger in size, and managed by the Garbage Collector (GC).
3. Metaspace (formerly PermGen)
Introduced in Java 8 to replace Permanent Generation (PermGen).
- What it stores: Class metadata, static variables, constant pools, and method bytecodes.
- Behavior: Grows dynamically and uses native system memory rather than JVM heap memory, reducing
OutOfMemoryErrorissues associated with PermGen.
Concurrency and The Java Memory Model (JMM)
In multi-threaded programs, hardware optimizations like CPU caches and instruction reordering can cause subtle bugs. The JMM dictates how and when different threads can see values written to shared variables.
Key Concepts
- Visibility: When one thread modifies a variable, how quickly do other threads see that change? Due to CPU caches, a thread might cache a variable locally and never see updates from main memory unless explicitly instructed.
- Atomicity: Operations that complete entirely or not at all (e.g., reading/writing a 32-bit primitive is atomic, but
i++is not). - Instruction Reordering: The compiler or CPU may change the order of code execution to optimize performance, as long as it doesn't change the single-threaded outcome.
The volatile Keyword
The volatile keyword guarantees visibility and prevents instruction reordering around that variable.
public class SharedResource {
// Changes to this variable are immediately visible to all threads
private volatile boolean isRunning = true;
public void stop() {
isRunning = false;
}
public void runWork() {
while (isRunning) {
// Do work
}
}
}
The happens-before Relationship
The JMM relies on the "happens-before" rule to guarantee memory visibility. If action A happens-before action B, the results of A are visible to B. Examples:
- Releasing a monitor lock (exiting a
synchronizedblock) happens-before acquiring the same lock in another thread. - Writing to a
volatilevariable happens-before reading that same variable. - Thread
start()happens-before any code executed within that thread.
Garbage Collection Basics
Java abstracts manual memory management via Garbage Collection. The GC automatically reclaims memory by deleting objects that are no longer reachable from the GC Roots (active threads, static variables, local variables).
- Minor GC: Cleans the "Young Generation" space (Eden and Survivor spaces). Very fast.
- Major/Full GC: Cleans the "Old Generation" space. Slower and can cause application pauses ("stop-the-world").
Common Memory Pitfalls
- Memory Leaks: Even with GC, you can leak memory by holding onto object references longer than necessary (e.g., in a
HashMaporListthat continuously grows). - StackOverflowError: Occurs when a thread's stack runs out of space, usually due to infinite recursion.
- OutOfMemoryError (Heap Space): Occurs when the JVM cannot allocate an object because the heap is full and the GC cannot free up enough space.
Summary
- Stack is for fast, thread-local execution and primitives.
- Heap is for objects and is shared across threads.
- JMM ensures predictable behavior in concurrent applications via rules like
happens-beforeand keywords likevolatileandsynchronized. - Understanding memory helps you write performant code and debug production issues like
OutOfMemoryError.