Stack vs Heap Memory in Java and Object References
Introduction
Why does changing an object through one variable change it for another? Why can a method not swap two of its caller's variables? Why do deep recursion and huge collections fail with different errors? The answers all come from how Java uses two areas of memory - the stack and the heap - and from what a reference really is. This tutorial explains stack vs heap memory in Java, and object references. We will start with what each memory area holds. We will then trace a small program through both, and see how references let several variables share one object. Next we will see what happens when each area runs out -
(Note - This tutorial builds on Variables in JavaClick to read tutorial: Variables in Java: Declaration, Initialization and Scope and the call stack from Java MethodsClick to read tutorial: Java Methods: Defining, Calling and Returning Values.) Two areas of memory
OUTPUT of the above code
While
Explanation of the code
References, null and reassignment
A reference variable can be pointed at a different object, or at no object -
OUTPUT of the above code
Assigning one primitive variable to another copies the value, so
OUTPUT with the default stack size (the exact depth varies)
OUTPUT with -Xss8m, an 8 MB stack
A bigger stack allowed many more nested calls - but the real fix for a
When the heap runs out - OutOfMemoryError
If a program keeps references to more objects than the heap can hold, the garbage collector cannot free them, and the JVM throws an
OUTPUT with -Xmx16m
Only 7 arrays fitted in the 16 MB heap: the default G1 garbage collector divides a heap this small into 1 MB regions, and each array - 1 MB of data plus a small object header - needed two of them. The objects could not be collected because the list still referred to them. In real programs, this kind of problem - a memory leak - usually comes from collections or caches which only ever grow. (The program catches the error only to print a message; normally an
Note - This is the model the Java language guarantees. Behind the scenes, the JIT compiler may optimize - for example, an object which never escapes a method can be broken up into local values and never put on the heap at all (escape analysis). Your program behaves exactly as if the model above were followed. Class definitions and static fields live in a separate area, the method area (called Metaspace in HotSpot) - see JDK vs JRE vs JVMClick to read tutorial: JDK vs JRE vs JVM - Differences Explained with Examples. Summary In this tutorial we first saw that each thread's stack holds method frames with local variables, while the shared heap holds every object and array. We then traced a program through both, and saw that a reference variable on the stack points to an object on the heap - so several variables, and method parameters, can share one object. Next we reassigned references and set them to
StackOverflowError and OutOfMemoryError. Lastly, we will compare the two areas side by side.(Note - This tutorial builds on Variables in JavaClick to read tutorial: Variables in Java: Declaration, Initialization and Scope and the call stack from Java MethodsClick to read tutorial: Java Methods: Defining, Calling and Returning Values.) Two areas of memory
- The stack: Every thread has its own stack. Each time a method is called, a stack frame is pushed onto it, holding the method's parameters and local variables; when the method returns, the frame is popped and its variables disappear. Stack memory is fast and managed automatically, but small - typically around 1 MB per thread.
- The heap: One large area shared by all threads, where every object and every array lives - created with
new, or by literals such as"text". Objects stay on the heap as long as something refers to them, and are removed later by the garbage collector. Its maximum size is set with the-Xmxoption, and is usually a quarter of the machine's memory by default.
- A local variable (including a parameter) always lives in its method's stack frame.
- If its type is a primitiveClick to read tutorial: Java Primitive Data Types and Their Ranges with Examples, the variable holds the value itself -
int count = 5stores 5 on the stack. - If its type is a class, interface or array, the variable holds a reference - something like the address of an object - and the object itself is on the heap.
- Fields (instance variables) are part of their object, so they live on the heap, inside it.
A program to trace through stack and heap
//MemoryTrace.java
public class MemoryTrace {
static class Account {
String owner; // field: a reference, stored inside the Account object on the heap
double balance; // field: a primitive, also inside the object on the heap
Account(String owner, double balance) { this.owner = owner; this.balance = balance; }
}
static double interest(Account account, double rate) { // account and rate: in interest()'s frame
double result = account.balance * rate; // result: in interest()'s frame
return result;
}
public static void main(String[] args) {
int years = 2; // primitive: in main()'s frame
Account toms = new Account("Tom", 1000); // reference in main()'s frame, object on the heap
Account alias = toms; // a second reference to the SAME object
alias.balance += 500;
double earned = interest(toms, 0.05) * years;
System.out.println(toms.owner + " has " + toms.balance + ", earns " + earned + " in " + years + " years");
System.out.println("toms == alias: " + (toms == alias));
}
}
Tom has 1500.0, earns 150.0 in 2 years toms == alias: true
interest() is running, memory looks like this -
STACK (main thread)
interest() frame - top
account = ref → Account#1
rate = 0.05
result = 75.0
account = ref → Account#1
rate = 0.05
result = 75.0
main() frame
args = ref → String[]
years = 2
toms = ref → Account#1
alias = ref → Account#1
args = ref → String[]
years = 2
toms = ref → Account#1
alias = ref → Account#1
HEAP (shared)
Account#1
owner = ref → "Tom"
balance = 1500.0
owner = ref → "Tom"
balance = 1500.0
String "Tom"
String[] args (empty)
- There is only one
Accountobject.toms,aliasand the parameteraccountare three references to it, so the deposit made throughaliaswas seen throughtoms, andtoms == aliasistrue. - Passing
tomstointerest()copied the reference into the new frame - not the object. This is why Java is pass-by-valueClick to read tutorial: Is Java Pass-by-Value or Pass-by-Reference?, yet methods can change the objects they are given. - When
interest()returned, its frame - withaccount,rateandresult- was popped and gone. TheAccountobject stayed on the heap, becausemain()still referred to it.
null. Neither affects the objects themselves, only which object the variable refers to. An object which no variable refers to any more can never be used again, and becomes garbage.
Reassigning references
//References.java
import java.util.ArrayList;
import java.util.List;
public class References {
public static void main(String[] args) {
List<String> a = new ArrayList<>(List.of("Tom")); // object #1
List<String> b = a; // b refers to object #1 too
b.add("Harry");
System.out.println("a = " + a + ", b = " + b);
b = new ArrayList<>(List.of("Ethan")); // b now refers to a new object #2
System.out.println("a = " + a + ", b = " + b);
a = null; // nothing refers to object #1 any more
System.out.println("a = " + a + " - [Tom, Harry] can now be garbage collected");
int x = 10;
int y = x; // primitives: y gets a copy of the value
y++;
System.out.println("x = " + x + ", y = " + y);
}
}
a = [Tom, Harry], b = [Tom, Harry] a = [Tom, Harry], b = [Ethan] a = null - [Tom, Harry] can now be garbage collected x = 10, y = 11
x and y are independent. Assigning one reference to another copies the reference, so both point to one object. How unused objects are cleaned up is covered in Garbage Collection BasicsClick to read tutorial: Java Garbage Collection Basics and Object Lifecycle.
When the stack runs out - StackOverflowError
Each method call adds a frame, so very deep - or endless - recursionClick to read tutorial: Recursion in Java with Examples eventually fills the thread's stack, and the JVM throws a StackOverflowError. The stack size can be changed with -Xss.
Filling the stack with recursion
//StackDepth.java
public class StackDepth {
static int depth = 0;
static void dive() {
depth++;
dive(); // no base case - every call adds a frame
}
public static void main(String[] args) {
try {
dive();
} catch (StackOverflowError e) {
System.out.println("StackOverflowError after " + depth + " nested calls");
}
}
}
StackOverflowError after 20692 nested calls
StackOverflowError after 207786 nested calls
StackOverflowError is almost always a missing base case, or rewriting very deep recursion as a loop.
OutOfMemoryError. Here, the heap is limited to 16 MB with -Xmx16m, and the program keeps adding 1 MB arrays to a list.
Filling the heap
//HeapFull.java
import java.util.ArrayList;
import java.util.List;
public class HeapFull {
public static void main(String[] args) {
System.out.println("Max heap: " + Runtime.getRuntime().maxMemory() / (1024 * 1024) + " MB");
List<byte[]> kept = new ArrayList<>();
try {
while (true) {
kept.add(new byte[1024 * 1024]); // 1 MB, kept reachable through the list
}
} catch (OutOfMemoryError e) {
int count = kept.size();
kept = null; // release them, so we can carry on
System.out.println("OutOfMemoryError after keeping " + count + " MB: " + e.getMessage());
}
}
}
Max heap: 16 MB OutOfMemoryError after keeping 7 MB: Java heap space
OutOfMemoryError should not be caught.)
Stack vs heap - comparison
| Stack | Heap | |
| Holds | Method frames: parameters, local variables (primitive values and references) | All objects and arrays, including their fields |
| Shared? | No - one per thread | Yes - shared by all threads |
| Lifetime | Until the method returns | Until no longer referenced, then removed by the garbage collector |
| Size | Small (around 1 MB per thread), set with -Xss | Large, set with -Xmx and -Xms |
| When full | StackOverflowError | OutOfMemoryError |
| Speed | Very fast - just moving a pointer | Fast allocation, plus garbage collection work |
Note - This is the model the Java language guarantees. Behind the scenes, the JIT compiler may optimize - for example, an object which never escapes a method can be broken up into local values and never put on the heap at all (escape analysis). Your program behaves exactly as if the model above were followed. Class definitions and static fields live in a separate area, the method area (called Metaspace in HotSpot) - see JDK vs JRE vs JVMClick to read tutorial: JDK vs JRE vs JVM - Differences Explained with Examples. Summary In this tutorial we first saw that each thread's stack holds method frames with local variables, while the shared heap holds every object and array. We then traced a program through both, and saw that a reference variable on the stack points to an object on the heap - so several variables, and method parameters, can share one object. Next we reassigned references and set them to
null, leaving objects unreachable. We then filled the stack to get a StackOverflowError and the heap to get an OutOfMemoryError, and lastly compared the two areas. In the next tutorial, we will see how unreachable objects are removed, in Garbage Collection BasicsClick to read tutorial: Java Garbage Collection Basics and Object Lifecycle.