Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Advanced
  • 24 minutes
  • Examples run with Java 25.0.4.1 and Python 3.14.8
  • By MySmartCoPilot
Free core, Pro depth: 9 of 24 minutes free
Free: Where the values of a program live · What the header holds · Four strengths of reference. With a pass: 2 in-depth parts and 3 interview answers, about 15 minutes.

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:

Points.java Java · Points.java
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
    }
}
Heap.java Java · Heap.java
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);
    }
}
A stack frame holds a reference to an array on the heap, whose slots refer to Point records with object headers.Stack of the thread mainHeap, shared by all threadsFrame of main()local i: an int, 999999local points: a reference(and args and before)Point[] array: header 16 bytes,then 1,000,000 references of 4 bytesOne Point record, 24 bytes:mark word 8 · class 4 · x 4 · y 4 · padding 4With compact headers, 16 bytes:header 8 · x 4 · y 4pointspoints[3]compactheaders

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:

A million records, three ways Python · points_session.py
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.

Data Storage Converter Turn histogram byte counts such as 24,000,000 into MB and MiB, to compare them with heap sizes set in -Xmx.

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.

  1. Question 1 of 5 A method runs int count = 3; and Point p = new Point(1, 2);. Where are count, p and the Point object?

    Choose one answer.

    Show the answer to question 1

    Answer: count and the reference p are in the method's stack frame; the Point object is on the heap

    A frame holds the method's local variables: count with its int value, and p with a reference. The object that new created lives on the heap, where other threads and other methods can reach it through references. The JIT may remove an allocation that never leaves its method, but it does not move objects onto the stack.

  2. Question 2 of 5 On JDK 25 with default headers, how many bytes does one record Point(int x, int y) take on the heap?

    Type a number.

    Show the answer to question 2

    Answer: 24 bytes

    A 12-byte header plus two 4-byte fields is 20 bytes, and objects are padded to a multiple of 8, so 24. The class histogram measured exactly 24,000,000 bytes for a million of them.

  3. Question 3 of 5 The same Point with -XX:+UseCompactObjectHeaders: how many bytes?

    Type a number.

    Show the answer to question 3

    Answer: 16 bytes

    The compact header is 8 bytes, so 8 + 4 + 4 = 16 needs no padding. A million records went from 24 MB to 16 MB; the array of references stayed at 4,000,016 bytes.

  4. Question 4 of 5 You attach extra data to objects that another library owns, and the data should disappear when the library drops its objects. Which reference fits the keys?

    Choose one answer.

    Show the answer to question 4

    Answer: A weak reference, as WeakHashMap uses for its keys

    A weak reference does not keep its object alive, and it is cleared at the first collection that finds the object otherwise unreachable, which is what attached data needs. A soft reference would keep objects that the library already dropped, and a static map with strong references would keep them for ever.

  5. Question 5 of 5 What must a Cleaner action never do?

    Choose one answer.

    Show the answer to question 5

    Answer: Refer to the object it was registered for, because the object could then never become phantom reachable

    The cleaner keeps the action until it runs. If the action refers to the object, directly or through a lambda that uses this, the object stays reachable and the action is never invoked automatically, as the Cleaner documentation warns. clean() runs an action at most once by design.

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.

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.

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).

  1. 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.

  2. 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.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. 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.

    Model answer, Pro. First step and follow-ups

  4. 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?

    Model answer, Pro. First step and follow-ups

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-int records took 24 MB.
  • Compact object headers cut the header to 8 bytes: the same records took 16 MB with -XX:+UseCompactObjectHeaders on 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; Cleaner builds on them, and finalize() is on its way out.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.