A JVM object header is the first 8 or 16 bytes of any object, and it’s surprisingly complex.
Let’s see what’s actually in there. Imagine a simple Object instance.
public class MyObject {
int x;
String s;
}
When we create an instance, new MyObject(), the JVM doesn’t just allocate space for x and s. It prefixes the object’s data with a header. On a 64-bit JVM, this header is 16 bytes. On a 32-bit JVM, it’s 8 bytes.
Here’s what that 16-byte header on a 64-bit JVM often contains:
- Mark Word (8 bytes): This is the most dynamic part. It stores information about the object’s state, including:
- Hash code: If
hashCode()hasn’t been called yet, the Mark Word might store a compact representation of the hash code. OncehashCode()is called, the hash code is moved to the object header’s location, and the Mark Word’s bits are updated to reflect this. - GC state: Bits indicate which generation the object is in, or if it’s being marked during a garbage collection cycle.
- Locking information: When an object is locked (e.g., by
synchronized), the Mark Word stores a reference to the thread that holds the lock, or it might store a pointer to an object on the Java heap called a "thin lock" or "heavy lock" depending on contention.
- Hash code: If
- Klass Pointer (8 bytes): This is a pointer to the object’s class metadata. It tells the JVM what type of object it is, allowing it to look up methods, field offsets, and other class-specific information.
Consider this: a plain Object instance, with no fields, already consumes 16 bytes on a 64-bit JVM just for its header. The actual data fields come after this.
Let’s look at a real-world scenario. Suppose you have a HashMap that’s storing a million String keys. Each String object itself has a header, and then the HashMap entry (which is typically another object, often an internal Node) also has its own header.
Here’s a simplified Node class often used in HashMap:
static class Node<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
}
If you have a HashMap with 1,000,000 entries, and each entry is a Node object, and the keys are String objects, you’re looking at:
- 1,000,000
Stringobjects, each with its own 16-byte header. - 1,000,000
Nodeobjects, each with its own 16-byte header. - The
HashMapobject itself, plus its internal array of buckets (which are oftenNodes ornull).
This overhead is why primitives are often preferred over their wrapper types (e.g., int vs. Integer) when memory is a concern, and why using arrays of primitives is more memory-efficient than arrays of wrapper objects.
The most surprising thing about the JVM object header is its role in implementing intrinsic locks. When an object is first synchronized, its Mark Word is used to store the lock information directly. This avoids allocating a separate lock object for every synchronized object, which would be a massive performance and memory hit. Only when contention increases does the JVM "upgrade" the lock, moving the lock information off the header and into a separate object on the heap.
If you’re trying to optimize memory usage, understanding that even empty objects have a significant fixed cost due to their headers is crucial. You’ll often see tools like jmap or VisualVM showing object sizes that are multiples of 8 bytes, even if the declared fields are small. This is the header at work.
The next thing you’ll likely encounter is how object alignment and padding further increase memory consumption beyond just the header and fields.