5.5 Call Stack, Stack Frames, and Stack Traces
Prerequisites: Method Parameters and Return Values Estimated time: 35 minutes You are ready to continue when: You can reverse-engineer the call chain from a stack trace and understand runtime optimization boundaries
"This is the model of a call stack: the most recently entered method returns first."
Each Thread Has Its Own JVM Stack
When a thread calls a method, the JVM creates a frame for the call. At the level of specification, a frame includes a local variable area, an operand stack, and references to the current class's constant pool. It also participates in returning control and exception propagation.
JVM Stack of a Thread (Conceptual Diagram)
┌──────────────┐
│ calculateTax │ ← Currently executing
├──────────────┤
│ checkout │
├──────────────┤
│ main │
└──────────────┘"The frame is just local variables + return address" is suitable for the first step, but it's incomplete. "Each frame is several to hundreds of bytes" is not a portable guarantee. Frame layout, stack size, and optimization are determined by the JVM implementation, method bytecode, and runtime parameters.
Observing Entry and Return with Logs
public class StackDemo {
static int add(int left, int right) {
System.out.println("2. enter add");
int result = left + right;
System.out.println("3. leave add");
return result;
}
public static void main(String[] args) {
System.out.println("1. enter main");
int result = add(3, 4);
System.out.println("4. result=" + result);
System.out.println("5. leave main");
}
}When calling add, main has not yet completed, and its execution context remains in the lower layer. add returns, and the call site receives the result, allowing main to continue.
Exception Propagation Along the Call Stack
static void load() {
parse();
}
static void parse() {
Integer.parseInt("not-a-number");
}
public static void main(String[] args) {
load();
}If there is no matching catch point, the exception propagates from parse to load, then to main, and the thread ultimately ends with an uncaught exception. The stack trace typically starts by showing the exception type and message, followed by the point of throwing and the caller:
NumberFormatException: ...
at ...parse(...)
at ...load(...)
at ...main(...)When debugging, first find the earliest relevant frame that belongs to your code, then read down through its callers to understand where the input originates. Frameworks, reflection, and asynchronous boundaries can either add or remove intuitive links; Caused by also represents the causal chain of the exception, which is not equivalent to a regular call hierarchy.
Recursion Adds Active Calls
static long factorial(int value) {
if (value < 0) {
throw new IllegalArgumentException("negative value");
}
if (value > 20) {
throw new ArithmeticException("factorial exceeds long: " + value);
}
if (value <= 1) {
return 1L;
}
return Math.multiplyExact(value, factorial(value - 1));
}Each layer remains active until the next layer returns. The absence of a reachable termination condition may trigger StackOverflowError.
Do not write "recursion depth exceeds a certain fixed number and it overflows." The available depth is influenced by thread stack configuration, JVM, platform, and compilation state, as well as the needs of each frame. StackOverflowError belongs to Error, and it is usually not advisable to catch it and pretend the program can safely continue; instead, fix the algorithm or input boundaries.
JIT Optimization and Conceptual Model
The JVM can just-in-time compile and inline small methods, so the physical machine stack does not necessarily correspond one-to-one with source code methods. The JVM must maintain the observable semantics of Java and can reconstruct or report sufficient state when needed. The stack diagram in the tutorial represents a language/JVM model of calls, local scopes, returns, and exceptions, not a real-time snapshot of processor registers and machine frames.
This also shows that the statement "writing a small method definitely adds an expensive stack call" is not valid. Write clearly and measure the hot spots first, then optimize based on the analysis results.
Each Thread's Stack Is Independent
When two threads call the same method, each has its own parameter and local variable frame. They do not share these local variables even if they have the same name. They may still influence each other through static fields, shared objects, files, or databases. Thread safety issues mainly arise from shared mutable state, not from the names of local variables.
Professional Quick Reference
| Target | Observation Point |
|---|---|
| Restore synchronous call chain | Stack trace frame order and source code line |
| Identify root cause | Exception type, first relevant business frame, Caused by chain |
| Determine local isolation | Per-thread independent frames; check if references point to shared objects |
| Recursion safety | Reachable base, input upper bound, stack depth not fixed, numeric overflow |
| Performance judgment | Do not guess from number of source code methods; use profiler/JFR etc. for measurement |
Official reference: JVMS 17 §2.5.2 Java Virtual Machine Stacks and §2.6 Frames.
Before Moving On
Draw the activation frame sequence of main → checkout → calculateTax, and separately mark the call chain and exception causality chain from a stack trace containing Caused by. Frame sizes, recursion limits, and the mapping from source methods to physical frames are not fixed across JVMs.