Java Module 16 – JVM internals, memory, GC and performance
Heap, stack, references and object headers
Where Java keeps locals and objects, what an object header costs with and without compact headers, measured, and how soft, weak and phantom references work.
What you will learn
- Explain heap versus stack storage for objects, references and local variables
- Describe an object header and measure the effect of compact object headers
- Estimate an object's size from its fields, then check the estimate with a class histogram
- Use soft, weak and phantom references and Cleaner, and replace finalize()
Before you start
What you’ll learn · Before you start
What you will learn
- Explain heap versus stack storage for objects, references and local variables
- Describe an object header and measure the effect of compact object headers
- Estimate an object's size from its fields, then check the estimate with a class histogram
- Use soft, weak and phantom references and Cleaner, and replace finalize()
Before you start
On this page
Every Java thread has a stack: one frame for each method call, holding that call’s local variables and its operand
stack. A local variable of a primitive type, such as an int, holds its value right in the frame. A local variable
of any other type holds a reference, the address of an object. The objects themselves, arrays included, always
live on the heap, which all threads share and the garbage collector manages. Every object on the heap starts with
an object header: a mark word, with the identity hash code, the age the collector tracks and the lock state, then a
pointer to the object’s class. On 64-bit HotSpot that header takes 12 bytes, and every object is padded to a multiple
of 8 bytes. Compact object headers shrink the header to 8 bytes: a product option in JDK 25 and the default in JDK 27.
Besides ordinary references, java.lang.ref offers soft, weak and phantom references, which let the collector reclaim
an object that you still point to.
Where the values of a program live
Points creates a million small records and measures them with the JVM’s class histogram, the report that
jcmd <pid> GC.class_histogram prints, taken before and after. The histogram first runs a full collection, so it
counts only objects that are still reachable:
import java.lang.ref.Reference;
/** A million small records on the heap, measured with the class histogram. */
public class Points {
record Point(int x, int y) {}
public static void main(String[] args) throws Exception {
Heap.histogram();
var before = Heap.histogram();
Point[] points = new Point[1_000_000];
for (int i = 0; i < points.length; i++) points[i] = new Point(i, -i);
Heap.growth(before, Heap.histogram(), "Points$Point", "[LPoints$Point;");
Reference.reachabilityFence(points); // keeps the array reachable until the histogram has counted it
}
} import java.lang.management.ManagementFactory;
import java.util.HashMap;
import java.util.Map;
import javax.management.ObjectName;
/**
* Measures reachable objects by class with the JVM's class histogram, the report that jcmd <pid> GC.class_histogram
* prints, read here through the DiagnosticCommand MBean. A histogram runs a full garbage collection first, so it
* counts only objects that are still reachable.
*/
final class Heap {
private Heap() {}
/**
* Class name to {objects, bytes}, for every class in the heap. The first call loads the classes that a histogram
* needs, so a program takes its "before" histogram twice and keeps the second.
*/
static Map<String, long[]> histogram() throws Exception {
String report = (String) ManagementFactory.getPlatformMBeanServer().invoke(
new ObjectName("com.sun.management:type=DiagnosticCommand"), "gcClassHistogram",
new Object[] { null }, new String[] { String[].class.getName() });
Map<String, long[]> rows = new HashMap<>();
for (String line : report.split("\n")) {
String[] cols = line.trim().split("\\s+");
if (cols.length >= 4 && cols[0].endsWith(":")) rows.put(cols[3], new long[] { Long.parseLong(cols[1]), Long.parseLong(cols[2]) });
}
return rows;
}
/** Prints what these classes gained between two histograms: objects, bytes, and bytes per object. */
static void growth(Map<String, long[]> before, Map<String, long[]> after, String... classNames) {
long total = 0;
for (String name : classNames) {
long[] b = before.getOrDefault(name, new long[2]);
long[] a = after.getOrDefault(name, new long[2]);
long objects = a[0] - b[0];
long bytes = a[1] - b[1];
total += bytes;
String each = objects > 1 ? String.format(" %,.1f bytes each", bytes / (double) objects) : "";
System.out.printf(" %-24s %,10d object%s %,12d bytes%s%n", name, objects, objects == 1 ? " " : "s", bytes, each);
}
System.out.printf(" %-24s %10s %,20d bytes%n", "in all", "", total);
}
} Where the values of the Points program live
Text description of the diagram
The diagram has two blocks, one above the other.
The first block is the stack of the thread main. It holds the frame of main(), with its local variables: i, an int with the value 999999, and points, a reference, beside two more references, args and before.
An arrow labelled "points" leads from that frame into the second block, the heap, which all threads share. The arrow points at the Point array: a 16-byte header, then 1,000,000 references of 4 bytes each.
An arrow labelled "points[3]" leads from the array to one Point record of 24 bytes: an 8-byte mark word, a 4-byte class pointer, the two int fields x and y of 4 bytes each, and 4 bytes of padding, because objects are sized in steps of 8 bytes.
A last arrow, labelled "compact headers", leads to the same record with compact object headers (-XX:+UseCompactObjectHeaders): an 8-byte header and the two fields, 16 bytes in all.
The program ran three times, on JDK 25 with its default headers, on JDK 25 with -XX:+UseCompactObjectHeaders, and on
JDK 27, where compact headers are the default:
from session import run
# The same program three times: JDK 25 with its default object headers, JDK 25 with compact object headers, and JDK 27,
# where compact object headers are the default.
run("""
javac -d out Heap.java Points.java
java -cp out Points
java -XX:+UseCompactObjectHeaders -cp out Points
""")
run("""
java -cp out Points
""", jdk="27") Output
# JDK in use: openjdk version "25.0.4.1" LTS $ javac -d out Heap.java Points.java $ java -cp out Points Points$Point 1,000,000 objects 24,000,000 bytes 24.0 bytes each [LPoints$Point; 1 object 4,000,016 bytes in all 28,000,016 bytes $ java -XX:+UseCompactObjectHeaders -cp out Points Points$Point 1,000,000 objects 16,000,000 bytes 16.0 bytes each [LPoints$Point; 1 object 4,000,016 bytes in all 20,000,016 bytes # JDK in use: openjdk version "27" $ java -cp out Points Points$Point 1,000,000 objects 16,000,000 bytes 16.0 bytes each [LPoints$Point; 1 object 4,000,016 bytes in all 20,000,016 bytes
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 points_session.py
The frame of main holds the loop counter i as an int, and three references: args, before and points. Every
object they lead to is on the heap, and the sizes follow from the headers:
| Object | Default header (JDK 25) | Compact header |
|---|---|---|
Point |
12 header + 4 x + 4 y = 20, padded to 24 bytes |
8 header + 4 + 4 = 16 bytes |
Point[] of 1,000,000 |
16 header and length + 1,000,000 × 4 = 4,000,016 bytes | 4,000,016 bytes |
| The program’s heap growth | 28 MB | 20 MB |
The million records shrank from 24 MB to 16 MB, a third less, while the array stayed the same size. JEP 450, which introduced compact headers, explains why small objects gain most: many workloads have objects of only 32 to 64 bytes on average, so the header is a large share of them.
What the header holds
The mark word comes first and takes 64 bits: it holds the identity hash code once one is computed, the age of the
object for the collector, and the bits that say whether the object is locked. The class word follows, a compressed
class pointer of 32 bits. Compact object headers fold a 22-bit class pointer into the mark word itself, which is how a
header fits in 64 bits. The java documentation says the option saves 4 bytes per object on average, and the garbage
collection tuning guide adds its one limit: with compact headers, at most about 4 million classes can be loaded. The
saving can vanish in padding. An Integer is 12 + 4 = 16 bytes with the default header and 8 + 4 = 12, padded back
to 16, with the compact one, which is why the lesson on
choosing collections measured a list of a million Integers at the
same size both ways.
Two more mechanisms decide what an object costs. Compressed references store each reference in 4 bytes while the heap is smaller than 32 GB. Escape analysis lets the JIT avoid allocating an object that never leaves its method.
Four strengths of reference
The garbage collector frees an object once nothing can reach it. The package java.lang.ref defines how strongly a
reference holds its object, and each kind has one job:
- A strong reference, the ordinary kind, keeps its object alive for as long as the reference itself is reachable.
- A soft reference lets the collector clear its object when memory runs short, and always before an
OutOfMemoryError. - A weak reference is cleared at the first collection that finds its object reachable only through weak references.
- A phantom reference never returns its object; it tells you, through a queue, that the object is gone.
On top of phantom references, Cleaner runs a cleanup action after an object becomes unreachable. Finalization, the
old way of doing the same with an overridden finalize(), is deprecated for removal by JEP 421.
Check yourself
5 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
Three layouts of a million points, estimated and then measured Pro: opens with a Pro, Premium or Ultimate pass
Object headers, padding and references add up to more than the data itself. This part estimates three ways of storing the same million points by hand, then measures them, and shows two settings that change the answer.
In this part:
- Estimating an object’s size by hand
- The three layouts, measured
- Which records gain from compact headers, measured
- References above 32 GB
- Allocations the JIT removes, measured
6 runnable examples (Java, Python) · about 4 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Soft, weak and phantom references and Cleaner, reproduced Pro: opens with a Pro, Premium or Ultimate pass
A reference that the collector may clear changes what a program can rely on. This part runs all four strengths through a collection and an out-of-memory error, then shows a cleanup that works, one that silently never runs, and how to test a program with finalization switched off.
In this part:
- Four references through a collection and an OutOfMemoryError
- A weak map that never forgets, reproduced
- Cleaner done right, and a cleanup that never runs
- Finalization switched off, to test before it is removed
8 runnable examples (Java, Python) · about 4 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
-
Fresher · Warm-up
Where do a method's local variables and the objects they refer to live in the JVM?
Each thread has a stack with one frame per method call, and the frame holds that call's local variables and its operand stack. A local of a primitive type, such as an int or a double, holds its value right in the frame. A local of any other type holds a reference, and the object it refers to lives on the heap, which all threads share and the garbage collector manages. Arrays are objects too, so they are always on the heap, even an int array. When the method returns, its frame and the local values go away, while the objects stay on the heap for as long as something still refers to them. The JIT can remove an allocation that never leaves its method, but HotSpot's documentation says it does not place such objects on the stack.
-
Mid to senior
You must keep 50 million sensor samples in memory on JDK 25, each an int sensor id and a float reading. Estimate the heap for an array of records, two parallel arrays and an ArrayList of records, and say what compact object headers change.
-
Senior to staff
A team caches rendered product pages in a map of SoftReferences "so the JVM frees them when it needs memory". Under load the heap stays full, the service spends much of its time in garbage collection and latency spikes in waves. Explain and propose a better design.
-
Senior to staff
A service releases native buffers through a Cleaner when their Java wrappers become unreachable. Its Java heap is stable, but the process's resident memory grows for days until the container is killed. What do you check, in what order, and how do you fix it?
Key takeaways
- Each thread’s stack holds one frame per call, with primitives and references; objects and arrays live on the heap.
- An object costs a 12-byte header by default, its fields, and padding to a multiple of 8; a million two-
intrecords took 24 MB. - Compact object headers cut the header to 8 bytes: the same records took 16 MB with
-XX:+UseCompactObjectHeaderson JDK 25, and by default on JDK 27. - Soft references are cleared when memory runs short, weak ones at the next collection, and phantom ones only report
that an object is gone;
Cleanerbuilds on them, andfinalize()is on its way out.
References
- The Java Virtual Machine Specification, Java SE 25: 2.5 Run-Time Data Areas (Oracle)
- JEP 450: Compact Object Headers (Experimental) (OpenJDK)
- JEP 519: Compact Object Headers (OpenJDK)
- JEP 534: Compact Object Headers by Default (OpenJDK)
- JEP 421: Deprecate Finalization for Removal (OpenJDK)
- java.lang.ref (Java SE 25 API): reachability (Oracle)
- SoftReference (Java SE 25 API) (Oracle)
- PhantomReference (Java SE 25 API) (Oracle)
- WeakHashMap (Java SE 25 API) (Oracle)
- Cleaner (Java SE 25 API) (Oracle)
- The java Command (JDK 25 tool reference) (Oracle)
- HotSpot Virtual Machine Garbage Collection Tuning Guide (JDK 25): Other Considerations (Oracle)
- Java HotSpot Virtual Machine Performance Enhancements (JDK 25): Escape Analysis (Oracle)
- HotSpot Glossary of Terms (OpenJDK)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress