skip to content
everything.basics.stackAndHeap

stack and heap

where your variables live vs where your objects live. the most important picture in java.

chapter
chapter 02.11
reading time
8 min read

every method call gets a frame on the stack for its local variables. every object lives on the heap. a variable of an object type holds a reference, an arrow to the object, never the object itself.

that's the whole chapter. the rest is pictures.

the picture

you know how int b = a makes a copy, but Point q = p somehow doesn't? that's because there are two kinds of memory in play, and the variable only ever holds what fits in its own little slot.

think of a coat check. the heap is the cloakroom (where the coats actually are). a reference is the ticket in your pocket. you can photocopy the ticket as many times as you want and there's still one coat. lose every ticket and the coat just sits there until staff clears it out (that's the garbage collector).

stack
main
age21
p
heap
Person
name
age36
String
"ada"
one frame, one object. blue is always a reference, everywhere on this site.

notice age appears twice and means two different things. the local age (21) sits in the frame. the field age (36) sits inside the object, on the heap, because fields are part of the object no matter what type they are. and name isn't text sitting in the Person, it's another arrow, because String is an object too.

walk through it

press step (or click the figure and use ← →). watch what = actually copies each time.

Main.java next to run
  1. 1class Point {
  2. 2 int x, y;
  3. 3 Point(int x, int y) { this.x = x; this.y = y; }
  4. 4}
  5. 5
  6. 6void main() {
  7. 7 int a = 7;
  8. 8 int b = a;
  9. 9 b = 9;
  10. 10 Point p = new Point(1, 2);
  11. 11 Point q = p;
  12. 12 q.x = 100;
  13. 13 p = new Point(5, 5);
  14. 14 q = null;
  15. 15 IO.println(a + " " + b + " " + p.x);
  16. 16}
stdout
·
stack
main
no locals yet
heap
nothing allocated yet.

main gets a frame on the stack. nothing in it yet.

step 1/12
step 5 is the one people miss: the object exists with zeroed fields before the constructor runs. step 10 shows an object becoming unreachable.

see? b = a copies a number. q = p copies an arrow. same =, same rule (copy what's in the slot), wildly different vibes because of what's in the slot.

predict the output
java
int[] a = {1, 2, 3};
int[] b = a;
b[0] = 99;
IO.println(a[0]);
predict the output
java
String s = "hi";
String t = s;
t = t + "!";
IO.println(s);

what lives where

thingwhere it lives
a local primitive (int n = 3)in the frame, on the stack
a local reference (Point p)the reference is in the frame. the object is on the heap
every object, every arraythe heap. always. no exceptions in the language
fields, even int onesinside their object, so on the heap
static fieldswith the class, which the JVM keeps on the heap too
method parametersin the callee's frame, as copies (more in pass-by-value)
string literalson the heap, deduplicated in the string pool

why two kinds of memory

because they die differently.

a frame dies the instant its method returns. the JVM doesn't clean up anything, it just moves a pointer back down, so allocating and freeing locals is basically free. but that only works because nothing outlives the call.

objects need to outlive the method that made them (you return them, store them in lists, hand them to other threads). so they go somewhere that doesn't care about calls: the heap. the price is that someone has to figure out when an object is actually dead. that someone is the garbage collector.

thread main · stackmain()handle(req)thread worker · stackrun()flush()heap · shared by every threadArrayListreachable from bothRequestlocals: private to their thread.objects: anyone with a reference can touch them.
each thread gets its own stack. there's one heap. anything reachable from two stacks can be touched by two threads at once, which is where every concurrency bug in part 09 comes from.

when the stack runs out

a stack is a fixed-size block (set with -Xss). every call pushes a frame, so recursion without a base case eventually runs out of room.

Main.java
public class Main {
    static int calls = 0;
    static void down() { calls++; down(); }
 
    public static void main(String[] args) {
        try {
            down();
        } catch (StackOverflowError e) {
            System.out.println("overflowed after " + calls + " calls");
        }
    }
}
thread stack
3% used
-Xss =
depth =
1,200
1,200 frames of down() on top of main().
room for ~45,424 more.
drag the depth, or let it run. the numbers are real: measured on jdk 25 on an arm mac, where the default stack is 2 MB.
one level deeper:why the same program overflows at different depths+

run that program twice in a row inside the same JVM and the second run goes deeper. on jdk 25 with -Xss1m it was 20,773 calls cold and 57,948 once the JIT had compiled down().

interpreted frames are generous: they carry the full local array, an operand stack and bookkeeping for the interpreter. compiled frames only keep what the optimized code needs. same method, smaller frames, more of them fit. so "how deep can i recurse" has no single answer, which is a good reason to never design code that depends on it.

the heap can run out too. that's OutOfMemoryError: Java heap space, and it's a whole different story (spoiler: usually a leak). see the OOM zoo.

one level deeper:objects that never touch the heap+

"every object lives on the heap" is the language's promise. the JIT is allowed to cheat as long as you can't tell.

if escape analysis proves an object never leaves the method that created it (never returned, never stored in a field, never passed somewhere that keeps it), C2 can skip the allocation entirely and keep the fields in registers. it's called scalar replacement. your code still looks like it allocated a Point, you just never paid for it. more in JIT optimizations.

keylines
  • every call gets a frame. the frame dies when the call returns.
  • objects live on the heap. variables hold arrows to them.
  • = always copies what's in the slot. for objects, that's the arrow.
  • two arrows to one object means changes through one show up through the other.
  • an object with no arrows left is garbage. the GC decides when to clean it up.