At runtime, every object has a header that stores metadata about itself. In this header, there is an 8-byte section known as the “Mark Word”, within this section contains various metadata about the object. The layout of the data in the Mark Word varies depending on the last 2 bits.

Mark Word States

The structure of the bits in the Mark Word changes depending on the last two bits, this portion is called the Lock. We can think of the two bits of the Lock as denoting the state of the Mark Word.\

1. Normal/Unlocked (Lock bits: 01)

packet-beta
title 1. Normal/Unlocked State (Lock=01)
0-21: "Unused"
22-52: "Identity Hashcode"
53-56: "Unused Gap"
57-60: "Age"
61: "sfwd"
62-63: "Lock"

When an object is unlocked, the bits in the Mark Word contain the metadata relating to its instance.

  • Identity Hashcode - Stores Lazily calculated output of Object.hashcode() (if it was not Override’d).
  • Age - How many GC cycles survived
  • sfwd (Self-Forward) - single bit, used by some GCs on evacuation failure.

2. Lightweight Lock (Lock bits: 00)

packet-beta
title 2. Lightweight Lock (Lock=00)
0-61: "Pointer to Lock Record on Thread Stack"
62-63: "Lock"

When only a single thread has synchronized on the object, a Compare-and-Swap operation is used to flip the lock bits, marking it as locked and the metadata of object is copied into the stack frame of the thread, and the first 62 bits becomes the pointer to that stack frame. This is necessary to preserve the metadata of the object, as well as indicate to the JVM which thread has ownership of the object.

Introduced in JDK 21 and default in 23, a new Lightweight Lock scheme called Fast-Locking is used instead. Instead of the “stack-locking” scheme as depicted above, this new scheme keeps the metadata intact, so the only thing that changes when an object is locked is the last bit gets flipped from 1 to 0.
The thread itself then stores a pointer to the object in a structure called the lock-stack, and the JVM queries this structure to check ownership.

3. Heavyweight Lock (Lock bits: 10)

packet-beta
title 3. Heavyweight Lock (Lock=10)
0-61: "Pointer to ObjectMonitor Struct"
62-63: "Lock"

When a thread contends for an object while it is locked by another thread, the Mark Word for that object moves from the Lightweight Lock state to a Heavyweight Lock state. This is called inflation.
In this state, an ObjectMonitor struct is created and the object’s metadata is copied into it.

Inflation can also happen when a single thread that has a lightweight lock calls wait(). This causes inflation as an ObjectMonitor is needed to store the thread into the Wait Set and release the lock on the object.

In JDK 24, ObjectMonitorTable is a new optional alternative to the heavyweight lock scheme. Similar to the Fast-Locking introduced in JDK21, this new scheme leaves the metadata bits intact and instead relies on a concurrent hashmap managed by the JVM to store the pointer to the ObjectMonitor Struct.
object hashcode -> object's ObjectMonitor Struct

4. GC Marking (Lock bits: 11)

packet-beta
title 4. GC Marking (Lock=11)
0-61: "Forward Pointer"
62-63: "Lock"

During garbage collection if the object is live (not collected), the GC sets the lock bits to 11 to indicate that it has visited the object. and it sets the Forward Pointer to the new address of the object in the heap.
If the object cannot be moved to a new memory location, the Forward Pointer bits are reset to the metadata bits with the Self-Forward bit set to 1.
Once the GC is done, all the bits are restored to their pre-GC-cycle state.

Depending on the GC, especially modern GCs, a Forward Pointer may not even be used and the GC will leave the first 62 bits intact, and use an external data structure much like the other cases above.