Understanding JVM Memory Structure and the Java Memory Model (JMM): volatile, synchronized and Concurrency Basics
This article explains the differences between JVM memory layout and the Java Memory Model, clarifies how volatile and synchronized guarantee visibility, atomicity and ordering, and provides practical guidelines, code examples, and diagrams for mastering Java concurrency fundamentals.
JVM Memory and JMM Memory Model
In interviews and concurrent programming you often encounter volatile and synchronized . This article summarises the core knowledge points and interview focus, using text and diagrams to improve your concurrency skills.
Key Questions
What is the difference between JMM and JVM memory structure?
What is the Java Memory Model (JMM) and how does it relate to concurrency?
What are the most important aspects of the memory model: instruction reordering, atomicity, memory visibility?
What does volatile visibility mean, its use cases and common pitfalls?
How does synchronized work internally and what is its relationship with the monitor?
JVM Memory Structure
Java source files are compiled to .class files, loaded by the JVM class loader, and executed either by the interpreter or the JIT compiler on the underlying OS. The JVM abstracts the hardware, enabling Java to run on Linux, Windows, macOS, etc.
Since Java 8 the permanent generation has been removed and replaced by Metaspace, so flags like -XX:PermSize and -XX:MaxPermSize are obsolete.
The JVM divides memory into several runtime data areas:
Heap : stores objects and arrays; the main area for garbage collection.
Java Virtual Machine Stack : per‑thread stack of stack frames that hold local variables, operand stacks, dynamic linking information and return addresses.
Method Area (Metaspace) : stores class metadata, constant pools, field and method data.
Native Method Stack : used for native (C/C++) method execution.
Program Counter (PC) Register : points to the next bytecode instruction for each thread.
public
int add() {
int a = 1, b = 2;
return a + b;
}Each stack frame contains a local variable table, an operand stack, dynamic linking information and a return address.
Java Memory Model (JMM)
The JMM is a set of specifications that all JVM implementations must follow to guarantee consistent behavior of multithreaded programs across different hardware and compiler optimizations.
It addresses problems caused by CPU caches, instruction reordering, and compiler optimizations, ensuring that keywords such as volatile , synchronized and classes in java.util.concurrent work correctly.
The three most important concepts of JMM are:
Instruction Reordering
Atomicity
Memory Visibility
Instruction Reordering
Compilers, the JVM and CPUs may reorder instructions for performance, as long as single‑threaded semantics are preserved. In multithreaded code this can lead to unexpected results.
Reordering can reduce the number of loads and stores, improving speed, but it must not change the program order visible to a single thread.
Memory Visibility
When a thread writes to a volatile variable, the new value is immediately flushed to main memory, making it visible to other threads.
public
class Visibility {
int x = 0;
public void write() { x = 1; }
public void read() { int y = x; }
}If a thread updates x in its working memory but does not synchronize with main memory, another thread may still see the old value, causing a visibility problem.
Atomicity
Read/write of primitive types (except non‑volatile long and double ) are atomic. Compound actions like i++ are not atomic and require additional synchronization.
How JMM Solves These Problems
JMM defines two abstract memories:
Main Memory : shared by all threads.
Working Memory : a thread‑local copy of variables.
Eight atomic actions control the interaction between them:
read
load
store
write
use
assign
lock
unlock
These actions, together with memory barriers, guarantee the three JMM guarantees (reordering, atomicity, visibility).
volatile
volatile ensures that writes to a variable are immediately visible to other threads and prevents certain kinds of reordering.
Visibility : a write to a volatile variable happens‑before any subsequent read of that variable.
Prohibits Reordering : the compiler and CPU cannot reorder operations that cross a volatile read/write.
Correct Usage
Typical scenarios are boolean flags or single‑assignment fields where only simple reads/writes occur.
volatile
boolean shutdownRequested;
public void shutdown() { shutdownRequested = true; }
public void doWork() { while (!shutdownRequested) { /* do stuff */ } }Another common case is the double‑checked locking singleton pattern, where volatile prevents the instance reference from being observed before the constructor finishes.
class
Singleton {
private
volatile
static
Singleton instance =
null
;
private
Singleton() {}
public
static
Singleton getInstance() {
if
(instance ==
null
) {
synchronized
(Singleton.
class
) {
if
(instance ==
null
)
instance =
new
Singleton();
}
}
return
instance;
}
}Incorrect Usage
volatile does **not** make compound operations atomic. For example, using it for a++ in multiple threads leads to lost updates.
public
class
DontVolatile
implements
Runnable {
volatile
int
a;
public static
void
main(String[] args)
throws
InterruptedException {
Runnable r =
new
DontVolatile();
Thread t1 =
new
Thread(r);
Thread t2 =
new
Thread(r);
t1.start(); t2.start();
t1.join(); t2.join();
System.out.println(((DontVolatile) r).a);
}
@Override
public void run() {
for (int i = 0; i < 1000; i++) { a++; }
}
}The final value is usually less than 2000 because the increment is not atomic.
synchronized
synchronized provides mutual exclusion by acquiring a monitor (mutex) associated with an object. The monitor is implemented in the JVM using native OS mutexes.
When a thread enters a synchronized block, the JVM inserts the bytecode instructions monitorenter and monitorexit around the block.
Instance methods lock on this .
Static methods lock on the Class object.
Explicit synchronized blocks lock on the object specified in the parentheses.
Object Header and Lock States
Every Java object has a header consisting of a MarkWord and a Class Metadata pointer. The MarkWord stores hash code, GC age, and lock information. Depending on contention, the lock can be in one of four states:
No‑lock
Biased lock
Lightweight lock
Heavyweight lock
Lock escalation occurs as contention increases; the lock never downgrades.
The MarkWord changes its bits to reflect the current lock state (biased, lightweight, heavyweight, or unlocked).
Monitor Internals
ObjectMonitor maintains fields such as _owner (the owning thread), _WaitSet (threads waiting on wait() ), _EntryList (threads blocked trying to acquire the lock), _recursions (re‑entry count) and count (acquisition count).
Summary
The JVM memory structure is tied to the runtime data areas, while the Java Memory Model governs the semantics of concurrent Java programs. JMM abstracts away hardware‑level details, guaranteeing consistency, atomicity and ordering through memory barriers and eight atomic actions. volatile provides visibility and limited ordering guarantees, whereas synchronized offers full mutual exclusion, visibility, atomicity and ordering. Understanding these mechanisms is essential for writing correct and performant concurrent Java code.
Recommended Reading
Kafka Principles: Diagramming the Architecture
Architecture Design Methodology
Complete Kafka from an Interview Perspective
Database and Cache Write Consistency
Full-Stack Internet Architecture
Introducing full-stack Internet architecture technologies centered on Java
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.