The labs on this site need JavaScript to run. The same content is written out below.
Learn › Java › JVM
Stack and heap
What happens in memory each time a method is called
When a method is called, a frame is pushed onto the stack of that thread, and when the method returns, the frame is removed. An object created with new is placed on the heap, not on the stack, and a local variable in the frame refers to it.
Checked against real execution · Checked by running code on JDK 21
Concepts in this lab
- Run-time data areas
- These are the memory areas that the JVM uses while it runs a program. The areas that each thread has for itself are the pc register, the Java Virtual Machine stack, and the native method stack, and the areas shared by all threads are the heap and the method area. They are the basis for understanding which data is shared between threads.
- Java Virtual Machine stack
- This is a memory area that is created once for each thread. Each time a method is called, a frame that holds the method's local variables and intermediate values is pushed, and the frame is removed when the method ends. Because it is separate for each thread, local variables are not shared with other threads. If recursive calls go too deep, a StackOverflowError occurs.
- Heap memory
- This is the memory area where every object and array created with new is stored, and it is shared by all threads. The garbage collector manages it and reclaims objects that are no longer used. Its size is set with the -Xms (initial) and -Xmx (maximum) options, and an OutOfMemoryError occurs when it runs out of space.
- StackOverflowError and OutOfMemoryError
- A StackOverflowError occurs when method calls go so deep that they exceed the thread's stack space, and the usual cause is recursion with a wrong termination condition. An OutOfMemoryError occurs when memory such as the heap runs short and no more objects can be allocated, and common causes are a memory leak or loading too much data at once.
Try these
- Default settings — factorial(4). The frames pile up to 5 and are then removed in reverse order.
- Without the base case — The method keeps calling itself until the stack overflows.
- With a base case, but too deep — factorial(10) needs 11 frames. The stack holds only 8.
- With a larger stack — The same factorial(10) finishes normally.
- An object created on each call — The objects are created on the heap, and once a frame is removed, nothing refers to its object anymore.
Running with the default settings
- The program starts. A frame for main() is placed on the stack of the main thread. A frame holds the local variables of its method and the place to return to.
- factorial(4) is called. A new frame is pushed onto the top of the stack. The local variable n (value 4) exists only inside this frame.
- factorial(3) is called. A new frame is pushed onto the top of the stack. The local variable n (value 3) exists only inside this frame.
- factorial(2) is called. A new frame is pushed onto the top of the stack. The local variable n (value 2) exists only inside this frame.
- factorial(1) is called. A new frame is pushed onto the top of the stack. The local variable n (value 1) exists only inside this frame.
- The call to factorial(1) has finished. The return value is 1, because the base case applies. The frame is removed from the stack, and the local variables in it go away with it.
- The call to factorial(2) has finished. The return value is 2 × 1 = 2. The frame is removed from the stack, and the local variables in it go away with it.
- The call to factorial(3) has finished. The return value is 3 × 2 = 6. The frame is removed from the stack, and the local variables in it go away with it.
- The call to factorial(4) has finished. The return value is 4 × 6 = 24. The frame is removed from the stack, and the local variables in it go away with it.
- main() prints the result: 24. The frame that was pushed last was the first to leave the stack (last-in-first-out).
Result of this run
- A frame is pushed when a method is called and removed when it returns. At the deepest point there were 5 frames. A local variable lives exactly as long as its frame, so it needs no separate cleanup.
Code
static long factorial(int n) {
if (n <= 1) return 1; // base case
return n * factorial(n - 1);
}
public static void main(String[] args) {
System.out.println(factorial(4));
}
Assumptions of this model
This model simplifies the stack size to a number of frames. In reality the size is measured in bytes (-Xss, about 1MB by default on 64-bit Linux), and the size of a frame depends on how many local variables it has, so the number of frames that fit is not fixed. It also depends on whether the code has been JIT-compiled. That each thread has its own stack while the heap is shared, and that a frame is pushed on a call and removed on a return, matches the definitions in the JVM specification. This was checked by actually running code on JDK 21 on this PC. Recursion without a base case threw a StackOverflowError at a depth of about 17,000. Even with a base case, a depth of 200,000 threw a StackOverflowError with the default stack, and the same code finished normally once the stack was enlarged to 64MB (-Xss64m). After the stack of one thread overflowed, the other threads kept running.
Previous lab: Blocking, non-blocking, and virtual threads · Next lab: GC and memory leaks