Kestrel Exams kestrel logo KESTREL EXAMSPRACTICE TO PASS
Java19 min readCode verified

Scope, the stack and the heap: where a variable lives, and for how long

Every variable has a region of code where its name works and a stretch of time when it exists. This guide builds a vending machine class to show both, then follows its method calls onto the call stack and its objects onto the heap. Every listing was compiled and run on OpenJDK 21.

Practice this topic in the free app →All Java guides

A machine that remembers, a button press that forgets#

Picture a vending machine in a hallway. Its display shows your credit: drop in a quarter, go and find another, and the 25 cents is still there when you come back. Press a button and, on a good day, the machine works out your change on a scratch pad inside, drops the coins, and throws the scratch pad away.

Java variables come in those two kinds. Ask of each one: where in the code can I use its name? That region is its scope. When, while the program runs, does it exist? That stretch of time is its lifetime. Then ask where in memory it sits. One class, VendingMachine, grows through this whole guide, and every step shows the real output.

Scope: the curly braces decide#

You already declare a variable as a type, then a name: int credit = 100;. The one new idea is that a declaration has a reach. A pair of curly braces, { and }, marks a block. A variable declared inside a block can be used from its declaration down to that block’s closing curly brace, and nowhere else. This is the block rule.

The machine’s first version lives in main. It sells a 75-cent can and works out the change inside an if block.

Try it first

The last line prints change, which was declared inside the if block. Will it compile? If not, which line will the compiler point at?

Show the answer

No. change stops existing at the closing curly brace of the if. The compiler points at line 9, the line that uses the name.

VendingMachine.java
public class VendingMachine {
    public static void main(String[] args) {
        int credit = 100;                    // cents inserted so far
        int price = 75;                      // one can
        if (credit >= price) {
            int change = credit - price;     // change is born here...
            System.out.println("Vend! Change: " + change);
        }                                    // ...and its scope ends here
        System.out.println("Coins returned: " + change);
    }
}
VendingMachine.java:9: error: cannot find symbol
        System.out.println("Coins returned: " + change);
                                                ^
  symbol:   variable change
  location: class VendingMachine
1 error

cannot find symbol means “I do not know this name here”, and the caret, ^, points at the use. The variable was real; it was just out of scope. The fix is to declare change in main’s own block, before the if, and only assign it inside:

VendingMachine.java
public class VendingMachine {
    public static void main(String[] args) {
        int credit = 100;
        int price = 75;
        int change = 0;                      // declared in main's block
        if (credit >= price) {
            change = credit - price;         // assigned, not declared again
            System.out.println("Vend! Change: " + change);
        }
        System.out.println("Coins returned: " + change);
    }
}
Vend! Change: 25
Coins returned: 25

Notice there is no int inside the if: a second declaration would fail with variable change is already defined in method main(String[]). And change starts at 0 because a variable needs a value before it is read, and the if might be skipped (int change; alone fails with variable change might not have been initialized). The usual convention is to declare each variable in the smallest block that needs it, so fewer lines can misuse it. Declaring everything at the top of the method also works.

Trap check: which closing brace ends it?

For every use of a variable, find its declaration and follow it down to the closing curly brace of that block. A variable declared in a for loop’s parentheses belongs to the loop. So in for (int i = 0; i < 3; i++) { ... }, the name i is gone after the loop. Blocks nest: an inner block can use the outer block’s variables, never the other way round.

If you know Python, this is new: Python keeps a variable made inside an if for the rest of the function. Java ends it at the closing curly brace.

Parameters and locals belong to one method#

A real machine is an object, so VendingMachine becomes a class. The credit and the stock (cans inside) become instance variables, also called fields: declared in the class but outside every method, so each machine object gets its own. A parameter, like quarters in insertCoins(int quarters, int dimes), is filled in by each call and is in scope in its whole method body. A local variable, like count, is declared inside a method and follows the block rule.

Try it first

insertCoins and restock each declare a local called count. main inserts 3 quarters and 1 dime, restocks 2 boxes of 6 cans, then inserts 2 dimes. On the last line, is count 2 or 6? Is credit 20 or 105?

Show the answer

count is 2 and credit is 105. Each call gets a fresh count; credit is a field, so it keeps its value between calls.

VendingMachine.java
public class VendingMachine {
    private int credit;                      // cents inserted, not yet spent
    private int stock;                       // cans inside

    public VendingMachine(int stock) {
        this.stock = stock;
    }

    public void insertCoins(int quarters, int dimes) {
        int count = quarters + dimes;        // coins in this handful
        credit = credit + quarters * 25 + dimes * 10;
        System.out.println(count + " coins in, credit " + credit);
    }

    public void restock(int boxes) {
        int count = boxes * 6;               // cans added this time
        stock = stock + count;
        System.out.println(count + " cans added, stock " + stock);
    }

    public static void main(String[] args) {
        VendingMachine lobby = new VendingMachine(4);
        lobby.insertCoins(3, 1);
        lobby.restock(2);
        lobby.insertCoins(0, 2);
    }
}
4 coins in, credit 85
12 cans added, stock 16
2 coins in, credit 105

Does this make sense? Two dimes are 2 coins, and 85 plus 20 cents is 105. The two counts never meet, so you can name a local for what it means in this method without checking the others. Parameters work the same way: restock using quarters would get cannot find symbol, and so would main.

Instance variables: in scope across the whole class#

The methods use credit and stock, yet nobody passed them in. A field’s scope is the whole class, wherever in the class it is written: a field declared at the very bottom is still usable in a method above it. In every constructor and every instance method (one without static), the plain name credit means this object’s credit.

The catch is a static method, such as main. It runs without any particular machine, so a plain credit inside it fails with non-static variable credit cannot be referenced from a static context. Notice the error is not cannot find symbol: the name is in scope, but Java asks which machine’s credit. Name one through a reference, as in lobby.getCredit(). The methods guide shows the same error for a method.

This pays off an old mystery: this.stock = stock; in the constructor. Two variables called stock are in scope there, the field and the parameter, and Java picks the nearest, the parameter. That is shadowing. this means “the object this method was called on”, so this.stock reaches the field. The class-writing guide shows what goes wrong without it.

Scope is where, lifetime is when#

Scope is about the text: which lines may mention a name. Lifetime is about time: from when the variable is created, while the program runs, until it is destroyed. You just saw both: count was born fresh on each call and gone after it, while credit lived on in the machine.

KindScope: where the name worksLifetime: when it exists
Local variableits declaration to the closing curly brace of its blockfrom when its declaration runs until its block ends
Parameterthe whole method bodyfrom the start of the call until the method returns
Instance variablethe whole class; in a static method, only through an object, as in lobby.getCredit()as long as the object that owns it
static fieldthe whole class, static methods includedone copy for the whole class, while the class is loaded (in a small program, the whole run)

The two can come apart. Soon buy will call makeChange. While makeChange runs, buy’s local change is out of scope, because makeChange cannot name it, yet it is still alive, waiting for buy to carry on. The call stack explains how.

The call stack: one frame per call#

Picture cafeteria trays: new ones go on top, and only the top one comes off. Each method call gets a stack frame, a block of memory holding that call’s parameters and local variables. A call pushes its frame onto the call stack. Returning pops it off, and its locals are gone. The caller’s frame waited underneath, untouched, and carries on.

Now buy calls makeChange to count out coins of one size. Each println is indented by call depth, so the output draws the stack sideways. The second purchase asks for coins worth 0 cents, on purpose, to make the deepest call fail.

Try it first

While makeChange runs, how many frames are on the stack, and which one is on top?

Show the answer

Three: main at the bottom, then buy, then makeChange on top. The two below are paused, waiting.

VendingMachine.java
public class VendingMachine {
    private int credit;
    private int stock;

    public VendingMachine(int stock) {
        this.stock = stock;
    }

    public void insertCoins(int quarters, int dimes) {
        credit = credit + quarters * 25 + dimes * 10;
    }

    public void buy(int price, int coin) {
        System.out.println("  buy(" + price + ", " + coin + ") starts");
        int change = credit - price;
        credit = 0;
        stock--;
        int coins = makeChange(change, coin);
        System.out.println("  buy: drop " + coins + " coins, change was " + change);
        System.out.println("  buy returns");
    }

    public int makeChange(int cents, int coin) {
        System.out.println("    makeChange(" + cents + ", " + coin + ") starts");
        int count = cents / coin;
        System.out.println("    makeChange returns " + count);
        return count;
    }

    public static void main(String[] args) {
        System.out.println("main starts");
        VendingMachine lobby = new VendingMachine(4);
        lobby.insertCoins(5, 0);
        lobby.buy(75, 25);
        lobby.insertCoins(5, 0);
        lobby.buy(75, 0);                    // a coin worth 0 cents: fails on purpose
        System.out.println("main ends");
    }
}
main starts
  buy(75, 25) starts
    makeChange(50, 25) starts
    makeChange returns 2
  buy: drop 2 coins, change was 50
  buy returns
  buy(75, 0) starts
    makeChange(50, 0) starts
Exception in thread "main" java.lang.ArithmeticException: / by zero
	at VendingMachine.makeChange(VendingMachine.java:25)
	at VendingMachine.buy(VendingMachine.java:18)
	at VendingMachine.main(VendingMachine.java:36)

In the first purchase, makeChange returns 2, its frame is popped, and buy resumes with change still holding 50. Just before that return, the stack looked like this, top first:

FrameWhat it holds
makeChange (running)cents = 50, coin = 25, count = 2, this
buy (paused)price = 75, coin = 25, change = 50, coins (not assigned yet), this
main (paused)args, lobby

this is a hidden parameter every instance method gets: a reference to the object it was called on, which is how buy reaches credit. Notice the two coins. Each parameter is a copy of its argument, in its own frame, which is why a method cannot change its caller’s variable (see what a method cannot change).

The second purchase crashed: 50 / 0 is whole-number division by zero, which Java refuses with an ArithmeticException. Nothing caught it (the exceptions guide shows how), so Java printed a stack trace: the frames on the stack at that moment, top first, each with the line it had reached. makeChange is on top at line 25, buy waits at line 18, and main is at the bottom at line 36. main ends never prints: the uncaught exception popped every frame without letting it finish. The recursion guide piles up many frames of one method.

Trap check: is this division safe?

Every division: can the right side be 0? Whole-number division by 0 stops the program; floating-point division by 0 gives Infinity.

Trap check: which frame is on top?

The method running right now is always the top frame, the one called most recently. main is at the bottom, because it was called first. “main is on top” is a favorite wrong answer.

Shrink it to the smallest case. If main calls nothing, the stack holds one frame. When main returns, the stack is empty and the program is over.

Objects live on the heap#

Objects often must outlast the call that made them. So every object, whether made with new, an array, or a String literal, lives on the heap: a large area of memory, separate from the stack. VendingMachine lobby = new VendingMachine(4); splits in two. The object goes on the heap. The variable lobby goes in main’s frame and holds a reference to the object:

 STACK: main's frame             HEAP
 +-----------------------+        +-----------------------------+
 | lobby   o-------------+------->| a VendingMachine object     |
 +-----------------------+        |   credit   0                |
                                  |   stock    4                |
                                  +-----------------------------+

A reference is, in effect, the object’s address. Java never shows you the number; you can only follow it with a dot, as in lobby.insertCoins(4, 0). So a reference variable holds an address, never the object. Note that credit, an int, is on the heap, because it is part of the object. Where a variable is declared decides where it lives: locals and parameters in a frame, fields inside their object.

Try it first

sameOne = lobby copies lobby, and gym gets its own new. Four quarters go into sameOne. What credit do lobby and gym show?

Show the answer

100 and 0. Copying the variable copied the address, so lobby and sameOne are one machine; gym is a second.

VendingMachine.java
public class VendingMachine {
    private int credit;
    private int stock;

    public VendingMachine(int stock) {
        this.stock = stock;
    }

    public void insertCoins(int quarters, int dimes) {
        credit = credit + quarters * 25 + dimes * 10;
    }

    public int getCredit() {
        return credit;
    }

    public static void main(String[] args) {
        VendingMachine lobby = new VendingMachine(4);
        VendingMachine sameOne = lobby;               // copies the reference
        VendingMachine gym = new VendingMachine(4);   // a second new: a second machine
        sameOne.insertCoins(4, 0);
        System.out.println("lobby credit: " + lobby.getCredit());
        System.out.println("gym credit:   " + gym.getCredit());
        System.out.println(lobby == sameOne);
        System.out.println(lobby == gym);
    }
}
lobby credit: 100
gym credit:   0
true
false

On references, == asks “same object?”, which is why lobby == gym is false.

Trap check: how many news?

In any “what does this print?” question with objects, count the news. Each new makes exactly one object. b = a makes none: it copies a reference, so a change made through b shows up through a.

Strings are objects too, with one wrinkle worth meeting on purpose:

VendingMachine.java
public class VendingMachine {
    public static void main(String[] args) {
        String shelf = "Cola";
        String button = "Cola";
        String built = new String("Cola");
        System.out.println(shelf == button);
        System.out.println(shelf == built);
        System.out.println(shelf.equals(built));
    }
}
true
false
true

Java shares one copy of each identical String literal (text in double quotes in the code), kept in what is called the string pool, so shelf == button is true. new always builds a separate object, so shelf == built is false, while equals compares the letters. Text from a Scanner is usually a new object, so compare text with equals, as the Strings guide explains.

Passing an object to a method: still a copy#

Java passes every argument by value: the parameter gets a copy of what the argument holds. For an object, that is a copy of the reference. The object is not copied, and it is not passed “by reference” either. So a method can follow its copy and change the object, but it cannot make the caller’s variable point somewhere else.

Try it first

service sets the machine’s stock to 30, then points its parameter at a brand-new machine and sets that one’s stock to 99. Afterwards, is lobby’s stock 4, 30 or 99?

Show the answer

30. The first line followed the copied address to the lobby machine. The second moved only the method’s own copy.

VendingMachine.java
public class VendingMachine {
    private int credit;
    private int stock;

    public VendingMachine(int stock) {
        this.stock = stock;
    }

    public int getStock() {
        return stock;
    }

    public static void service(VendingMachine vm) {
        vm.stock = 30;                       // follows the copy to the real machine
        vm = new VendingMachine(0);          // moves only the method's own copy
        vm.stock = 99;                       // changes the new machine
    }

    public static void main(String[] args) {
        VendingMachine lobby = new VendingMachine(4);
        service(lobby);
        System.out.println("lobby stock: " + lobby.getStock());
    }
}
lobby stock: 30

lobby and vm were two variables in two frames holding one address. When service returned, the 99-can machine was left with no reference anywhere.

When a variable dies, and when an object can be collected#

A local or a parameter is gone, at the latest, when its method returns and its frame is popped. An instance variable lasts as long as its object. And an object stays on the heap while your running code can still reach it, however many methods return.

VendingMachine lobby = new VendingMachine(4);   // machine A on the heap
lobby = new VendingMachine(10);                 // machine B; nothing refers to A now
lobby = null;                                   // nothing refers to B either

null means “refers to nothing”. An object your running code can no longer reach, by following references from its variables, is unreachable, and the garbage collector, the part of the Java runtime that frees heap memory, may then reclaim it, fields and all. Java does not promise when, or even that it happens before the program ends; System.gc() is only a request. You never free memory yourself.

Type this one yourself

Do not paste it. Add a method refund() that returns the current credit and sets the credit back to 0. You need one local variable: work out why first. Test it with 3 quarters and 1 dime.

Show the answer

Save the credit in a local before clearing the field, or the value is lost:

VendingMachine.java
public class VendingMachine {
    private int credit;
    private int stock;

    public VendingMachine(int stock) {
        this.stock = stock;
    }

    public void insertCoins(int quarters, int dimes) {
        credit = credit + quarters * 25 + dimes * 10;
    }

    public int getCredit() {
        return credit;
    }

    public int refund() {
        int back = credit;                   // save it first
        credit = 0;                          // then clear the field
        return back;
    }

    public static void main(String[] args) {
        VendingMachine lobby = new VendingMachine(4);
        lobby.insertCoins(3, 1);
        System.out.println("Refund: " + lobby.refund());
        System.out.println("Credit now: " + lobby.getCredit());
    }
}
Refund: 85
Credit now: 0

The local back lives only for the call, which is all it needs: return hands its value over before the frame is popped.

What was new in this guide#

  1. Scope is where a name can be used; lifetime is how long the variable exists.
  2. The block rule: a local is in scope from its declaration to its block’s closing curly brace, or you get cannot find symbol.
  3. Parameters are in scope in their whole method, fields across the whole class (a static method reaches them through an object); a same-named parameter shadows a field, and this reaches past it.
  4. Each call pushes a stack frame of parameters and locals; returning pops it. A stack trace prints the frames.
  5. Objects live on the heap; a reference variable holds an address, so b = a shares one object. On references, == asks “same object?”; identical String literals share one object, and new String makes another.
  6. Java passes a copy of every argument, including a copy of a reference.
  7. Locals die when the method returns; unreachable objects may be garbage collected, at a time Java chooses.
One sentence to carry into the exam

Scope is where a name can be used, from its declaration to the closing curly brace of its block; lifetime is how long the variable exists. Locals and parameters live in their call’s stack frame and die when the method returns; objects live on the heap, reached through references, and once nothing can reach them the garbage collector may reclaim them, at a time Java chooses.

Where these go wrong#

  • Using a variable after the block that declared it. That is cannot find symbol. Declare it before the block and only assign it inside.
  • Expecting a local to remember its value between calls. Each call gets a fresh one. A value that must survive has to be a field.
  • Writing stock = stock; when the parameter is also called stock. The parameter shadows the field. Write this.stock = stock;.
  • Thinking b = a copies an object. It copies the reference, so both names share one object. Count the news.
  • Saying Java passes objects by reference. It passes a copy of the reference. Pointing the parameter at a new object does nothing to the caller.
  • Saying an object is destroyed when its method returns. Only the frame goes. An unreachable object may be reclaimed by the garbage collector, at a time Java chooses.

On the exam: For scope, slide your finger from the declaration down to the matching closing curly brace. For objects, count the news and draw one box per object. For the stack, write the frames bottom up in call order. Read the question twice, answer, then reread it to check every part.

Practice#

Answer each one on paper before you look at the solutions. Every answer that involves running code was checked by compiling and running it.

  1. This does not compile. Which line does the compiler point at, and why? Fix it so it prints the last value of coin and the total.
    int total = 0;
    for (int coin = 5; coin <= 25; coin = coin + 10) {
        total = total + coin;
    }
    System.out.println(coin + " " + total);
  2. What does this print? Count the news first.
    Locker mine = new Locker(3);
    Locker hers = mine;
    mine = new Locker(1);
    hers.items = 7;
    System.out.println(mine.items + " " + hers.items);
  3. In a FareCard class, tap(int fare) adds fare to a local sum (which starts at 0) and to a field total, then prints both. main calls mine.tap(5), mine.tap(3), then yours.tap(2) on a second card. Predict the three lines.
  4. main calls checkout(3). checkout declares a local subtotal, then calls tax(subtotal). While tax runs: list the frames from top to bottom, say whether tax can use the name subtotal, and say whether subtotal still exists.
  5. Write an instance method transferTo(VendingMachine other, int cans) that moves that many cans from this machine to other and returns true, or changes nothing and returns false if this machine does not have that many (or cans is negative).
  6. In main: VendingMachine m = new VendingMachine(4); then VendingMachine spare = m; then m = null;. Can the machine be garbage collected now? What changes after spare = null;, and when will its memory be freed?

Solutions#

  1. The println line. coin was declared in the parentheses of the for, so its scope ends at the loop’s closing curly brace:Coins.java
    public class Coins {
        public static void main(String[] args) {
            int total = 0;
            for (int coin = 5; coin <= 25; coin = coin + 10) {
                total = total + coin;
            }
            System.out.println(coin + " " + total);
        }
    }
    Coins.java:7: error: cannot find symbol
            System.out.println(coin + " " + total);
                               ^
      symbol:   variable coin
      location: class Coins
    1 error
    Declare coin before the loop and only assign it in the loop header. The loop stops when coin reaches 35, the first value past 25:Coins.java
    public class Coins {
        public static void main(String[] args) {
            int total = 0;
            int coin;
            for (coin = 5; coin <= 25; coin = coin + 10) {
                total = total + coin;
            }
            System.out.println(coin + " " + total);
        }
    }
    35 45
  2. 1 7. Two news, so two lockers. hers = mine shares the first. Then mine moves to the second locker, so hers.items = 7 changes only the first:Lockers.java
    class Locker {
        int items;
    
        Locker(int items) {
            this.items = items;
        }
    }
    
    public class Lockers {
        public static void main(String[] args) {
            Locker mine = new Locker(3);
            Locker hers = mine;
            mine = new Locker(1);
            hers.items = 7;
            System.out.println(mine.items + " " + hers.items);
        }
    }
    1 7
  3. 5 5, 3 8, 2 2. The local sum starts at 0 on every call. total is a field, so it keeps growing for mine, while the second card has its own total:FareCard.java
    public class FareCard {
        private int total;
    
        public void tap(int fare) {
            int sum = 0;
            sum = sum + fare;
            total = total + fare;
            System.out.println(sum + " " + total);
        }
    
        public static void main(String[] args) {
            FareCard mine = new FareCard();
            mine.tap(5);
            mine.tap(3);
            FareCard yours = new FareCard();
            yours.tap(2);
        }
    }
    5 5
    3 8
    2 2
  4. From the top: tax, checkout, main. tax cannot use the name subtotal; it has only its own parameter, a copy of the value. But subtotal still exists in checkout’s paused frame. Out of scope but still alive.
  5. Plain stock is this machine’s field; other.stock follows the reference to the other machine. Code inside a class may use the private fields of any object of that class:VendingMachine.java
    public class VendingMachine {
        private int stock;
    
        public VendingMachine(int stock) {
            this.stock = stock;
        }
    
        public int getStock() {
            return stock;
        }
    
        public boolean transferTo(VendingMachine other, int cans) {
            if (cans < 0 || cans > stock) {
                return false;                    // cannot move what is not here
            }
            stock = stock - cans;                // this machine's field
            other.stock = other.stock + cans;    // the other machine's field
            return true;
        }
    
        public static void main(String[] args) {
            VendingMachine lobby = new VendingMachine(10);
            VendingMachine gym = new VendingMachine(2);
            System.out.println(lobby.transferTo(gym, 4));
            System.out.println(lobby.transferTo(gym, 40));
            System.out.println(lobby.getStock() + " " + gym.getStock());
        }
    }
    true
    false
    6 6
  6. Not yet: spare still refers to it. After spare = null; it is unreachable, so the garbage collector may reclaim it. Java makes no promise about when, or even that it happens before the program ends.

Test-readiness checklist#

These are the items from the course’s Exam 1 topic list that this guide covers. Tick each one only when you can do it without looking.

  • ☐  I can say where a local variable, a parameter and an instance variable are in scope.
  • ☐  I can explain scope versus lifetime.
  • ☐  I can draw the call stack for a chain of calls and say what each frame holds.
  • ☐  I can say what is in the stack frame and what is on the heap for a line like VendingMachine m = new VendingMachine(4);.
  • ☐  I can predict what prints when two references share one object, and say when an object can be collected.

Test yourself in the free Kestrel Exams app

18 practice questions on scope and memory, with every code answer checked by compiling and running it. Free, offline, no account.

Practice scope and memory →

Frequently asked questions#

What is variable scope in Java?

Scope is the region of code where a variable's name can be used. A local variable is in scope from its declaration to the closing curly brace of its block, a parameter in its whole method, and an instance variable in the whole class, though a static method such as main must reach it through an object. Outside its scope, the name gives the compile error cannot find symbol.

What is the difference between scope and lifetime in Java?

Scope is about the program text: which lines may use the name. Lifetime is about time: how long the variable exists while the program runs. While method a waits for method b, a's locals are out of scope in b but still alive.

What is the difference between the stack and the heap in Java?

The call stack holds one frame per unfinished method call, with that call's parameters and local variables. The heap holds every object, whether made with new, an array, or a String literal, with its fields. In VendingMachine m = new VendingMachine(4); inside a method, the variable m is in the stack frame and the object is on the heap.

Is Java pass by value or pass by reference?

Always pass by value. For an object, the parameter receives a copy of the reference, so the method can change the object, but assigning a new object to the parameter does not change the caller's variable.

When is an object garbage collected in Java?

Once running code can no longer reach it, an object is eligible. The garbage collector may then reclaim it, but Java does not promise when; System.gc() is only a request.

Suggest a change

Something here not clear? A topic you wish we covered? Tell us. We read every message, and a request is the fastest way to get a guide written: several of these exist because somebody asked.