Two variables. One assignment. You change y, and x changes with it:

int[] x = {1, 2, 3};
int[] y = x;
y[0] = 99;

System.out.println(x[0]);  // 99. You never touched x.

Enter fullscreen mode Exit fullscreen mode

If that has ever bitten you, here is the missing picture. It is small, it fits in your head, and it predicts everything in this post.

A note, not a box

The lie we absorb early: "a variable is a box that holds your data." For an int, fine. For an array, an object, a list - no.

A variable is a note holding directions. int[] x = {1,2,3} creates the array somewhere else, and writes down where on the note called x. Then y = x does not copy the array. It copies the note. One array. Two arrows pointing at it.

y[0] = 99 follows y's arrow and edits the one shared array. Both prints change, because there was only ever one thing to change.

y = new int[]{9,9,9} is different. It does not touch the old array at all. It writes new directions on y's note. x still points where it always did.

That is the whole trick: mutation travels through the arrow; reassignment only turns your own note.

Where the note lives, and where the data lives

The note lives on the stack. Each method call gets a frame: your parameters, your local variables, and the line number to return to. Pushing a frame is one pointer move, which is why calls are fast. When the method returns, the frame pops, in reverse order, every time.

The data lives on the heap: a managed pool where objects go. Every object carries a hidden header (the tag the runtime sticks on), and the heap is shared by every thread in your program. That is why two threads can fight over one object: each thread has its OWN stack, but they all share ONE heap.

So int[] x is really: a small slot on the stack holding an arrow into the heap. Java copies the arrow, never the object. (Precise interview wording: Java is pass-by-value, always - for objects, the value that gets copied IS the reference. JavaScript and Python tell the same story.)

Private is not private

Same picture, senior-level consequence. Here is a wallet:

class Wallet {
    private int[] coins;
    Wallet(int[] c) { coins = c; }
    int[] getCoins() { return coins; }
}

Enter fullscreen mode Exit fullscreen mode

The field is private. The data is not safe, and the two doors are right there in the class:

int[] theirs = {10, 20};
Wallet w = new Wallet(theirs);
theirs[0] = 999;              // balance just became 1,019
int[] got = w.getCoins();
got[0] = 0;                   // balance just became 20

Enter fullscreen mode Exit fullscreen mode

The constructor STORED the caller's array, so the caller still holds an arrow to it (door in). The getter returned the real array, so now anyone does (door out). private protects the field - the note - not the object it points to.

The fix is boring and it works:

Wallet(int[] c) { coins = c.clone(); }          // copy what comes in
int[] getCoins() { return coins.clone(); }      // copy what goes out

Enter fullscreen mode Exit fullscreen mode

Copy at both doors. Now the wallet owns its data.

The ceiling

One more thing the stack does: it ends.

void goDeeper() {
    goDeeper();
}

Enter fullscreen mode Exit fullscreen mode

Every call pushes a frame. None of them pop. The stack is a fixed-size region, so this cannot go on forever - and it does not:

run 1 . frames before the ceiling: 44,036
run 2 . frames before the ceiling: 49,394
run 3 . frames before the ceiling: 49,394

Enter fullscreen mode Exit fullscreen mode

On my machine (Java 21, default stack size), java.lang.StackOverflowError at 49,394 frames. The number varies run to run - the first run dies earlier because the JIT has not warmed up - and it will differ on your machine. The point is not the number. The point is that the stack is fast BECAUSE it is fixed. One pointer move per call, nothing to clean up, and a hard ceiling as the price.

(Honesty note: in the video the climbing counter is an animation; the final number is this measured one. The two-rooms picture is the model that predicts behavior - real JVMs add optimizations underneath like escape analysis and TLABs, which change the how, not the what.)

One rule

Copy before you change. Or do not change what you did not create.

That rule is the arrow model compressed into one line. It saves you from the shared-array flip, and it is exactly why the wallet fix works.

One question stays open on purpose: that heap object, when the last arrow to it dies... who throws it away? That is the next episode: you freed the memory. It did not leave.


I make The Leap: 5-minute systems videos where every number on screen is measured on my machine, and every animation says so when it is not. If this picture finally made stack vs heap click, the video draws it frame by frame: https://youtu.be/cLmszTXI_sA