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 - 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 -Xmx option, and is usually a quarter of the machine's memory by default.
The rule which decides where a value goes is simple -
  • 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 = 5 stores 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.
Tracing a program through stack and heap
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));
  }
}
 OUTPUT of the above code
Tom has 1500.0, earns 150.0 in 2 years
toms == alias: true
While interest() is running, memory looks like this -
STACK (main thread)
interest() frame - top
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
HEAP (shared)
Account#1
owner = ref → "Tom"
balance = 1500.0
String "Tom"
String[] args (empty)
Explanation of the code
  • There is only one Account object. toms, alias and the parameter account are three references to it, so the deposit made through alias was seen through toms, and toms == alias is true.
  • Passing toms to interest() 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 - with account, rate and result - was popped and gone. The Account object stayed on the heap, because main() still referred to it.
References, null and reassignment A reference variable can be pointed at a different object, or at no object - 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);
  }
}
 OUTPUT of the above code
a = [Tom, Harry], b = [Tom, Harry]
a = [Tom, Harry], b = [Ethan]
a = null - [Tom, Harry] can now be garbage collected
x = 10, y = 11
Assigning one primitive variable to another copies the value, so 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");
    }
  }
}
 OUTPUT with the default stack size (the exact depth varies)
StackOverflowError after 20692 nested calls
 OUTPUT with -Xss8m, an 8 MB stack
StackOverflowError after 207786 nested calls
A bigger stack allowed many more nested calls - but the real fix for a StackOverflowError is almost always a missing base case, or rewriting very deep recursion as a loop.
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 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());
    }
  }
}
 OUTPUT with -Xmx16m
Max heap: 16 MB
OutOfMemoryError after keeping 7 MB: Java heap space
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 OutOfMemoryError should not be caught.) Stack vs heap - comparison
StackHeap
HoldsMethod frames: parameters, local variables (primitive values and references)All objects and arrays, including their fields
Shared?No - one per threadYes - shared by all threads
LifetimeUntil the method returnsUntil no longer referenced, then removed by the garbage collector
SizeSmall (around 1 MB per thread), set with -XssLarge, set with -Xmx and -Xms
When fullStackOverflowErrorOutOfMemoryError
SpeedVery fast - just moving a pointerFast 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.

Stack vs Heap Memory in Java and Object References - JavaBrahman