Object Memory Layout in Java
Every Java object on the heap has a layout: a fixed header that the JVM uses for its own bookkeeping, followed by the object's fields, with padding added so that the total size is a multiple of 8 bytes. Arrays also store their length. Knowing the layout lets you estimate how much memory your objects really use, which matters when a program holds millions of them. In this lesson, you will learn each part of an object's layout and measure real sizes from a running program.
The Parts of an Object
| Part | Size (64-bit JVM, default settings) | Holds |
|---|---|---|
| Mark word | 8 bytes | Identity hash code, lock state, garbage collector age bits |
| Class pointer | 4 bytes (compressed) | Which class this object is |
| Array length | 4 bytes, arrays only | Number of elements |
| Fields | long/double 8, int/float 4, short/char 2, byte/boolean 1, references 4 (compressed) | The object's data |
| Padding | 0 to 7 bytes | Rounds the size up to a multiple of 8 |
On heaps under 32 GB, the JVM uses compressed references ("compressed oops"): object references take 4 bytes instead of 8. The JVM may also reorder fields (larger first) to reduce padding.
Example: Measuring Real Object Sizes
Let's see an example that creates 100,000 objects of each class, then asks the JVM's class histogram (the same data as jcmd <pid> GC.class_histogram) how many bytes each instance uses.
import java.lang.management.ManagementFactory;
import javax.management.ObjectName;
public class ObjectSizes {
static class Empty { }
record Point(int x, int y) { }
record Student(String name, int marks, boolean passed) { }
record Wide(long id, double score) { }
static long bytesEach(String className) throws Exception {
String histogram = (String) ManagementFactory.getPlatformMBeanServer()
.invoke(new ObjectName("com.sun.management:type=DiagnosticCommand"),
"gcClassHistogram", new Object[] {null},
new String[] {String[].class.getName()});
for (String line : histogram.split("\n")) {
String[] p = line.trim().split("\\s+");
if (p.length >= 4 && p[3].equals(className)) {
return Long.parseLong(p[2]) / Long.parseLong(p[1]);
}
}
return -1;
}
public static void main(String[] args) throws Exception {
Object[] keep = new Object[400_000];
for (int i = 0; i < 100_000; i++) {
keep[i] = new Empty();
keep[100_000 + i] = new Point(i, i);
keep[200_000 + i] = new Student("Navika", i, true);
keep[300_000 + i] = new Wide(i, i);
}
for (String name : new String[] {"Empty", "Point", "Student", "Wide"}) {
System.out.printf("%-8s %d bytes%n", name + ":",
bytesEach("ObjectSizes$" + name));
}
System.out.println(keep.length + " objects kept");
}
}Empty: 16 bytes
Point: 24 bytes
Student: 24 bytes
Wide: 32 bytes
400000 objects keptExplanation:
- Empty: a 12-byte header, no fields, padded to 16.
- Point: 12 + two
ints (8) = 20, padded to 24. - Student: 12 + a reference (4) + an
int(4) + aboolean(1) = 21, padded to 24. The"Navika"string is a separate shared object, not counted here. - Wide: 12 + a
long(8) + adouble(8) = 28, padded to 32.
Wrappers and Arrays
The same rules explain why boxed values are expensive: an Integer is a 12-byte header plus a 4-byte int, so 16 bytes to hold 4 bytes of data, plus a 4-byte reference wherever it's stored. An int[1000] is 16 bytes of header and length plus 4,000 bytes of data, while a List<Integer> of 1,000 values uses the list's array of references plus 1,000 separate Integer objects.
Common Mistakes
Assuming an object costs only the size of its fields. The header adds 12 bytes, and padding adds up to 7 more.
Counting referenced objects as part of an object's size. References are 4 bytes; the referenced objects are measured separately.
Assuming 8-byte references everywhere. Compressed references are the default for heaps under 32 GB.
Practice
- Easy: Predict the size of a record with one
longand oneint, then add it toObjectSizesto check. - Harder: Measure the size of
Integerobjects by keeping 100,000Integer.valueOf(i + 1000)values, and compare with 100,000ints in an array.
Key Takeaways
Was this tutorial helpful?