A tail-recursive call is one where the recursive call is the last operation in the method — nothing is left to do with its result except return it. Because the caller's frame is no longer needed, a compiler could reuse it instead of pushing a new one, turning the recursion into a loop with O(1) stack. This is tail-call optimization (TCO).
// Tail-recursive: nothing happens after the recursive call.
long fact(int n, long acc) {
if (n <= 1) return acc;
return fact(n - 1, acc * n); // last action — a tail call
}
// NOT tail-recursive: the multiply happens AFTER the call returns.
long factNaive(int n) {
if (n <= 1) return 1;
return n * factNaive(n - 1); // still has work pending
}
Why Java does not do it
The JVM deliberately keeps every frame. The reasons are baked into the platform:
- Stack traces — Java guarantees a full, accurate stack trace for every exception. Eliding frames would lose those entries; debuggability was chosen over the optimization.
- Security model —
AccessControllerand stack-walking APIs inspect the actual call stack for caller-sensitive permission checks. Reusing frames would corrupt that view. javacdoesn't emit it, the JIT doesn't add it — the bytecode keeps theinvoke/returnpair, and the JIT does not rewrite tail calls into jumps. (TheMethodHandle.invokeExactmachinery has limited internal tail-call support, but it is not exposed to ordinary recursion.)
The consequence: even perfectly tail-recursive Java code costs O(depth) stack and will StackOverflowError on large input. The @tailrec you may know from Scala has no Java equivalent.
What to do instead
Rewrite the tail recursion as the loop it already is:
long fact(int n) {
long acc = 1;
for (int i = 2; i <= n; i++) acc *= i; // the accumulator becomes a local
return acc;
}
For non-tail (branching) recursion that goes too deep, simulate the call stack with an explicit Deque.