← Runtimes
Runtime walkthrough 7 questions · 6 languages

Stack and heap

A correct model of calls, refined for how long values last.

A method call ending, the program losing its last way to reach an object, and the object’s memory being reclaimed are three different events. Frames explain the first. Reachability and the garbage collector explain the other two.

Before you begin

Why this matters

Knowing which values live in a method’s frame and which objects live on the heap explains why a method can return an object it created, and why memory can stay in use after you stop using an object.

What this helps you reason about

  • Predict whether an object is still usable after the method that created it returns.
  • Tell a local reference apart from the object it refers to.
  • Know what Java reclaims for you, and what you must close yourself.

The stack-and-heap picture is the model in most Java courses, and for Java it is also what the specification says. The specification of the Java Virtual Machine (JVM), the program that runs compiled Java code, gives each method call a frame and allocates every object and array in the heap. This lesson uses that model to predict behavior, then shows what a real JVM may do differently without changing any result.

Already know this? The short version
  • Each method call gets a frame holding its local variables, primitives and references. The frame is removed when the method returns.
  • Objects and arrays live on the heap, and static fields belong to the class, not to any frame. Returning a reference keeps the object usable, and copied references share one object.
  • A lambda copies the values of the effectively final locals it uses, so a captured reference keeps its object alive after the frame is gone.
  • The garbage collector reclaims objects some time after they are unreachable from GC roots (locals in running frames, static fields), cycles included. Close files and sockets with try-with-resources.
  • The JIT compiler may skip creating an object that never leaves its method (scalar replacement). That changes speed only, so the specification’s rule still decides everything a program prints.

Go to the final check· Go to the practice tickets

Words in this lesson (8)
frame
The record of one method call that has not finished, also called a stack frame or activation record. It holds the method’s parameters and local variables and the JVM’s working values for that call, such as the two numbers an addition is about to add. It is created when the call starts and removed when the call returns.
call stack
The frames of all method calls in one thread that have not finished yet, with the newest on top. A new call adds a frame on top, and a return removes it. Only the newest call runs; each call below it waits for the call it made.
heap
The area of memory where all objects and arrays are created. It is shared by all threads and managed by the garbage collector.
primitive
A value of one of Java’s eight primitive types: byte, short, int, long, float, double, char, or boolean. A primitive variable holds the value itself, not a reference.
reference
A value that leads to an object, or null. A variable of a class or array type holds a reference, never the object itself, and copying the reference does not copy the object.
effectively final
A local variable that is never assigned again after it is first set. A lambda can use only local variables that are final or effectively final.
reachable
An object is reachable when the program can get to it by following references from GC roots: the local variables of running methods, static fields of loaded classes, and JNI references.
garbage collector
The part of the JVM that finds objects that are no longer reachable and reclaims their memory, at a time it chooses, so new objects can use it. An object is unreachable when no chain of references leads to it from a running method’s variables or a static field. Unreachable cycles are reclaimed too.

Examples are focused snippets; imports, enclosing functions, or classes may be omitted. C examples use C11; Haskell examples use Haskell 2010. Expand the notes for language-specific depth.

01 / The common picture

What do people mean by stack and heap?

Here is the picture most programmers learn, and for Java it is what the JVM specification says. Each method call gets a frame, a block of memory for its parameters and local variables. The frames of one thread are kept on its stack, also called the call stack. A new call adds a frame on top of the stack, and returning removes that frame.

Some values must stay usable after the method that created them returns, for example an object that the method gives back to its caller. A frame cannot hold such a value, because the frame is removed when the call returns. So objects are kept in a separate area of memory, called the heap, until the program no longer needs them.

The rule to remember, and to write in an exam: local variables, both primitives such as int and references to objects, live in the method’s frame on the stack. Every object and array is allocated on the heap. Static fields belong to the class, not to any frame. This is what the JVM specification promises, and it predicts everything a Java program prints.

Section 06 adds one implementation note: an optimizing JVM may skip some of this work, such as creating an object that never leaves its method, but only where no program could see the difference.

Frames hold one call’s local variables, including references; objects and arrays are on the heap.

Coming from C#: Java 17 has no user-defined value types like C# structs: every object of a class type is on the heap, and local variables hold only references to it.

Precisely: what a frame holds, and where static fields live

The JVM specification (section 2.6) gives each frame an array of local variables and an operand stack. The operand stack is where the JVM’s instructions put intermediate values, such as the two numbers that a + b adds; you do not need it to predict what a Java program prints. Static fields are not in any frame. The specification (section 2.5.4) keeps each class’s data in the method area, which it describes as logically part of the heap. HotSpot, the JVM in OpenJDK, has stored static field values on the heap, with the class’s java.lang.Class object, since JDK 7. Either way, a static field lasts as long as its class stays loaded.

02 / Frames

What happens to a call’s variables when it returns?

A frame exists only while its method is running. When inner returns, its frame is removed and doubled is gone. outer receives only the returned value.

Main.java
static int inner(int n) {
    int doubled = n * 2;
    return doubled;
}

A method’s local variables end when it returns; its return value does not.

Experiment

Watch a frame come and go

Each call gets a frame holding its parameters and local variables. A frame exists only while its call is running. When inner returns, its frame is removed; only the returned value reaches outer.

Why it matters Local variables belong to one call. Once the call returns, the only thing the caller has is the value that was returned.

Predict what prints. Step until inner is running, then step once more. What happens to doubled?

class Main {
    static int inner(int n) {
        int doubled = n * 2;
        return doubled;
    }

    static int outer() {
        int start = 4;
        int result = inner(start);
        return result + 1;
    }

    public static void main(String[] args) {
        System.out.println(outer());
    }
}
What to look for

Watch the frame list. outer appears, then inner on top of it. When inner returns, its frame and its doubled variable are gone, and outer continues with result.

Expected output

9

03 / Outliving a call

How can a value last longer than the call that made it?

The Variables lesson showed that copying a reference gives a second name for the same object. This section asks how long that object lasts.

new Counter() creates an object on the heap, and makeCounter returns a reference to it. The frame is removed when makeCounter returns, but the object is not part of the frame. The caller keeps using the object through the returned reference. A second call creates a second object.

Main.java
static Counter makeCounter() {
    return new Counter();
}
Counter first = makeCounter();
first.count = 9;
Counter second = makeCounter(); // a new Counter: count is 3

Two returned references. This is a model of what each reference reaches, not of memory addresses.

A conceptual model, not a physical memory layout or live code execution.

1 / 3

Create

makeCounter: reference
refers to A
A.count
3

makeCounter creates object A.

An object outlives the method that created it when the method returns a reference to it.

Experiment

Keep an object after its method returns

new Counter() creates an object that does not belong to the frame, and each call creates a new one. makeCounter returns a reference to it, and the object stays usable while any reference to it exists.

Why it matters A method can safely return objects it created, and each call gives a new one. The garbage collector reclaims an object some time after nothing the program can still reach refers to it.

Predict both numbers: does the second call give back the first call’s Counter, or a new one? Then change Counter second = makeCounter(); to Counter second = first; and predict again.

class Main {
    static class Counter {
        int count = 3;
    }

    static Counter makeCounter() {
        return new Counter();
    }

    public static void main(String[] args) {
        Counter first = makeCounter();
        first.count = 9;
        Counter second = makeCounter();
        System.out.println(first.count + " " + second.count);
    }
}
What to look for

Step past the first return. The makeCounter frame is gone, but first still refers to the Counter, with count 3 and then 9. The panel shows each object with a number after @, such as Main$Counter@47: the debugger’s ID for that object, not an address. second shows a different number with count 3, so it is a different object, and 9 3 prints. After the edit, second shows the same number as first, and 9 9 prints.

Expected output

9 3

04 / Refining the picture

Where does the picture need refining?

The picture fits Java well. A lambda adds one refinement: it can keep an object alive after the frame that created the object is gone.

In makeCounter below, count is a local variable that holds a reference to an array. When the lambda is created, the reference value in count is copied into the lambda object. The local count ends with the frame, but the lambda’s copy still refers to the array. So the array stays alive as long as the lambda does.

A lambda can use only local variables that are never assigned again after they are first set. Java calls these effectively final. That is why the counter is an array element, which can change, and not an int local.

Main.java
static IntSupplier makeCounter() {
    int[] count = {0};
    return () -> ++count[0];
}

An object stays alive as long as something reachable, including a lambda, still refers to it.

Coming from Python: A Python closure can assign to an outer variable with nonlocal; a Java lambda cannot assign to a captured local at all, which is why the counter lives in an array.

Precisely: escape analysis, an implementation note

The JIT compiler is the part of the JVM that translates often-used methods into machine instructions while the program runs. HotSpot’s JIT can prove that some objects never leave a method, and then skip creating them, keeping their fields in registers or in the frame instead; HotSpot calls this scalar replacement. It changes speed and memory use only. No program can see the difference, so the specification’s rule, every object and array on the heap, is still the right answer about what a program does.

Experiment

Keep an array alive through a lambda

When the lambda is created, the reference stored in count is copied into the lambda object. The local count ends with the frame; the array that the reference leads to lives on the heap. The array outlives the frame because the lambda still refers to it, and each call changes the same array.

Why it matters A Java lambda can capture only local variables that are never assigned again (Java calls them effectively final), so the counter lives in an array or object the lambda refers to.

Predict all three numbers. Then change int second = next.getAsInt(); to int second = makeCounter().getAsInt(); and predict again.

import java.util.function.IntSupplier;

class Main {
    static IntSupplier makeCounter() {
        int[] count = {0};
        return () -> ++count[0];
    }

    public static void main(String[] args) {
        IntSupplier next = makeCounter();
        int first = next.getAsInt();
        int second = next.getAsInt();
        int third = next.getAsInt();
        System.out.println(first + " " + second + " " + third);
    }
}
What to look for

After makeCounter returns, step into the lambda. The frame list names its frame Main.lambda in makeCounter. Its count is the lambda’s copy of the reference, not makeCounter’s local: that local ended with makeCounter’s frame. In main, next shows IntSupplier lambda@ and a number, then {arg$1: int[]@47 [0]} in this run. arg$1 is the same copy, under a name Java generates. count and arg$1 show the same number after int[]@: both refer to one array on the heap, which stays alive. After the edit, 1 1 2 prints: the middle call makes a new array.

Expected output

1 2 3

05 / Releasing memory

Who decides when memory can be reused?

You never free objects yourself in Java. Instead, the JVM has a garbage collector: a part of the JVM that finds objects the program can no longer reach and reclaims their memory, so new objects can use it.

An object is reachable when the program can get to it by following references from the local variables of methods that are still running, or from static fields. These starting points are called GC roots. The garbage collector reclaims the memory of objects that are no longer reachable, at a time it chooses. Objects that refer only to each other are not reachable, so they are reclaimed too.

Files, sockets, and other resources are not memory. Close them yourself, ideally with try-with-resources, which closes them even when an exception is thrown.

Memory is reclaimed after nothing the program can still reach refers to an object; resources still need closing.

Coming from Python: CPython frees most objects as soon as their last reference goes away; Java reclaims them only when the garbage collector runs, so never rely on it to close a file.

06 / The implementation

What may the compiler or runtime change?

The JVM specification says what every JVM must make a program do, not how one particular JVM does it. A real JVM, such as the OpenJDK 17 used by the stepper, first interprets your methods. Then its JIT compiler translates often-used methods into machine instructions.

That compiled code may keep values in registers, small storage places inside the processor itself, instead of in a frame. It may also skip creating an object, as the note in section 04 describes. A program like the ones in this lesson behaves the same in every case. (Programs whose threads share data without synchronization have extra rules, outside this lesson.)

Precisely: evidence from a real run

A heap dump or Java Flight Recorder profile shows which objects are alive and what refers to them, for one JVM and one workload. Counting new expressions in the source cannot tell you the real allocation rate.

07 / Try it

When can each value’s memory be released?

Predict the output, then follow each array: which one can the program still reach after line 13?

Main.java
import java.util.function.IntSupplier;

class Example {
    static IntSupplier makeCounter() {
        int[] count = {0};
        return () -> ++count[0];
    }

    public static void main(String[] args) {
        IntSupplier tally = makeCounter();
        tally.getAsInt();
        tally.getAsInt();
        tally = makeCounter();
        System.out.println(tally.getAsInt());
    }
}

What prints? After line 13, which objects can the program no longer reach, and when is their memory reclaimed?

Reveal the reasoning

1. Each call to makeCounter creates a new array and a new lambda object that holds a copy of the reference to it. Line 13 stores the second lambda in tally; its array starts at {0}, so 1 prints. After line 13, no running method’s variable and no static field leads to the first lambda, so it and its array, which had reached 2, are unreachable. The garbage collector reclaims them at a time it chooses, possibly never in a program this short. The local count ended with each makeCounter frame; the arrays outlived those frames through the lambdas.

Practice tickets

  1. STK-101 · Keep each shelf label as it was printed → Free
  2. STK-102 · Hand out a receipt snapshot that stays as it was → Sign in · Pro
  3. STK-103 · Open one connection and close it → Sign in · Pro

Keep exploring

Investigate a minimal reproduction →

Primary references