The Java Memory Model (JMM) defines how threads interact through memory — specifying when writes by one thread are visible to another and what reorderings the JVM and CPU may perform.

Memory Areas Recap

Area Scope Stores GC
Heap All threads Objects, arrays Yes
Stack Per thread Local vars, references No (frame popped)
Metaspace All threads Class metadata Yes (class unloading)
PC Register Per thread Current instruction No
  public void example() {
    int x = 10;           // x on stack
    String s = "hello";   // reference on stack, object on heap
    int[] arr = new int[5]; // reference on stack, array on heap
}
  

Heap Structure

  ┌─────────────────────────────────┐
│              Heap                │
│  ┌───────────────────────────┐  │
│  │     Young Generation      │  │
│  │  ┌───────┐ ┌─────┬─────┐  │  │
│  │  │ Eden  │ │ S0  │ S1  │  │  │
│  │  └───────┘ └─────┴─────┘  │  │
│  └───────────────────────────┘  │
│  ┌───────────────────────────┐  │
│  │     Old Generation        │  │
│  └───────────────────────────┘  │
└─────────────────────────────────┘
  

New objects allocate in Eden. After surviving GC cycles, they promote to Old Generation.

Visibility Problem

Without synchronization, one thread’s writes may not be visible to another:

  // Thread 1
ready = true;
value = 42;

// Thread 2
if (ready) {
    System.out.println(value); // May print 0 due to reordering/caching
}
  

Happens-Before Rules

The JMM guarantees visibility through happens-before relationships:

  1. Program order — Each action in a thread happens-before every subsequent action in that thread
  2. Monitor lock — Unlock happens-before subsequent lock of the same monitor
  3. volatile — Write to volatile happens-before subsequent read
  4. Thread startThread.start() happens-before any action in the started thread
  5. Thread join — All actions in a thread happen-before Thread.join() returns
  6. Transitivity — If A happens-before B and B happens-before C, then A happens-before C

volatile

Ensures visibility and prevents reordering:

  private volatile boolean flag = false;
private int value = 0;

// Writer thread
value = 42;
flag = true;  // write visible to all threads

// Reader thread
if (flag) {
    System.out.println(value); // guaranteed to see 42
}
  

volatile does not provide atomicity for compound operations (count++).

final Fields

Properly constructed final fields are visible to all threads without synchronization:

  public class ImmutablePoint {
    private final int x;
    private final int y;

    public ImmutablePoint(int x, int y) {
        this.x = x;
        this.y = y;
    }
    // safe to read x, y from any thread after construction
}
  

Stack vs Heap — Common Interview Point

  public void method() {
    int a = 1;                    // stack
    Integer b = 2;                // reference stack, Integer object heap (cached -128 to 127)
    Person p = new Person("A");   // reference stack, Person object heap
}
  

OutOfMemoryError Types

Error Cause
Java heap space Heap full — too many objects
Metaspace Too many classes loaded
Unable to create new native thread Too many threads
Direct buffer memory Too many direct ByteBuffers

Best Practices

  • Use volatile for simple flags and status fields
  • Use synchronized or java.util.concurrent for compound operations
  • Prefer immutable objects to avoid visibility issues
  • Do not assume program order implies visibility across threads
  • Understand happens-before before writing lock-free code