The JVM throws a StackOverflowError when a thread’s stack has no room left for one more method call. Every method call adds a frame (a small block of memory for that call’s local variables) to the stack, and a call chain that never ends fills the stack in a fraction of a second. The full name is java.lang.StackOverflowError, and it is an Error, not an Exception.
Almost every StackOverflowError comes from a recursive method (a method that calls itself) that never stops, or from two objects whose toString() or hashCode() methods call each other, such as a parent and a child entity. So the fix belongs in our code, not in the JVM settings.
The following example sums the numbers from 1 to n with recursion. The first method has no base case (the condition that stops the recursion), so it keeps calling itself with 4, 3, 2, 1, 0, -1 and so on until the stack is full.
// No base case: the calls never stop
static long sumToNoBase(int n) {
return n + sumToNoBase(n - 1);
}
// Base case: the calls stop at 0
static long sumTo(int n) {
if (n <= 0) {
return 0;
}
return n + sumTo(n - 1);
}
long broken = sumToNoBase(5); // java.lang.StackOverflowError
long total = sumTo(5); // 15
The input is only 5, so the error comes from the missing base case, not from a large input.
Next, we read the repeating stack trace and fix the common causes. A bigger stack with -Xss comes last, because it rarely helps.
1. How the Thread Stack Fills Up
Each thread in the JVM has its own stack. For every method call, the JVM puts a new frame on top of that stack, and it removes the frame when the method returns or throws. If a thread needs a larger stack than is permitted, the JVM throws a StackOverflowError.
We set the maximum stack size with the -Xss option. The default stack size on Linux and macOS is 1024 KB on x64 (Intel and AMD) and 2048 KB on AArch64 (64-bit ARM).

The thread stack is separate from the heap. A full stack gives StackOverflowError, whereas a full heap gives OutOfMemoryError, which the OutOfMemoryError guide covers.
2. Spotting the Loop in the Repeated Frames
In a StackOverflowError trace, the same line repeats hundreds of times. When we run sumToNoBase(5) without catching the error, the JVM prints this trace.
Exception in thread "main" java.lang.StackOverflowError
at com.howtodoinjava.stackoverflow.Sums.sumToNoBase(Sums.java:11)
at com.howtodoinjava.stackoverflow.Sums.sumToNoBase(Sums.java:11)
at com.howtodoinjava.stackoverflow.Sums.sumToNoBase(Sums.java:11)
... 1021 more lines with Sums.java:11
The lines that repeat tell us where to look.
- One line that repeats, such as Sums.java:11, means a method calls itself, and its base case is missing or wrong.
- A repeating block of two or more methods means that objects call each other in a loop, as we will see in section 3.3. We look for our own classes inside the block.
- A block with only framework classes, such as Jackson or Hibernate, means the framework keeps following a loop between our objects.
By default, the JVM prints only the top 1024 lines of a trace, so the method that started the recursion is often cut off. The option -XX:MaxJavaStackTraceDepth=0 removes the limit. The last line of the trace then shows the first caller, here CrashDemo.main(CrashDemo.java:10).
java -XX:MaxJavaStackTraceDepth=0 -cp target/classes com.howtodoinjava.stackoverflow.CrashDemo
3. Common Causes of StackOverflowError and Their Fixes
The examples use JDK 25, Jackson 3.2.3 and Lombok 1.18.48. The Maven project reproduces each cause in a JUnit 6.1.3 test.
3.1. Recursion Without a Reachable Base Case
A recursive method needs a base case that every input reaches. When only a few inputs skip the base case, the bug can hide for months. For example, if (n == 0) works for sumTo(5), but sumTo(-1) goes -2, -3, -4 and never hits 0. The check if (n <= 0) stops every input, as in the fixed sumTo() from the intro.
3.2. Deep but Correct Recursion
Correct recursion can still overflow on deep data. Say a file-sync app counts the folders in a tree, and one customer has a generated folder chain that is 100,000 levels deep. The recursive countRecursive() stops at a folder without children, yet it needs one frame per level. So countRecursive(Folder.chain(100_000)) throws StackOverflowError.
The fix keeps the folders left to visit in an ArrayDeque used as a stack. The deque is on the heap, so it can grow as large as the heap allows.
static int countWithDeque(Folder root) {
Deque<Folder> pending = new ArrayDeque<>();
pending.push(root);
int count = 0;
while (!pending.isEmpty()) {
Folder folder = pending.pop();
count++;
for (Folder child : folder.children()) {
pending.push(child);
}
}
return count;
}
int folders = countWithDeque(Folder.chain(100_000)); // 100000
When the depth of the recursion depends on user data, we replace the recursion with a loop or an explicit Deque. Recursion is fine when the depth has a small known limit, such as a balanced tree.
3.3. Cycles Between toString(), equals() and hashCode()
When two classes point at each other, their objects form a cycle. Here, a Playlist holds a list of Song objects, and each Song points back to its Playlist. When both classes print the other side in toString(), the calls never end.
// Playlist
public String toString() {
return "Playlist[name=" + name + ", songs=" + songs + "]";
}
// Song
public String toString() {
return "Song[title=" + title + ", playlist=" + playlist + "]";
}
Playlist playlist = new Playlist("Road Trip");
playlist.addSong(new Song("Song A")); // also sets song.playlist
String text = playlist.toString(); // java.lang.StackOverflowError
In the trace, a block of seven lines repeats, switching between Song.toString() and Playlist.toString() with JDK methods in between.
java.lang.StackOverflowError
at com.howtodoinjava.stackoverflow.cycle.broken.Song.toString(Song.java:26)
at java.base/java.lang.String.valueOf(String.java:4530)
at java.base/java.lang.StringBuilder.append(StringBuilder.java:173)
at java.base/java.util.AbstractCollection.toString(AbstractCollection.java:459)
at java.base/java.lang.String.valueOf(String.java:4530)
at com.howtodoinjava.stackoverflow.cycle.broken.Playlist.toString(Playlist.java:30)
at java.base/java.lang.String.valueOf(String.java:4530)
at com.howtodoinjava.stackoverflow.cycle.broken.Song.toString(Song.java:26)

We break the cycle on the child side, so Song prints only the name of its playlist. The field that points back to the parent, here Song.playlist, is called the back-reference. The equals() and hashCode() methods must not follow it either.
public String toString() {
return "Song[title=" + title + ", playlist=" + (playlist == null ? null : playlist.getName()) + "]";
}
String text = playlist.toString(); // Playlist[name=Road Trip, songs=[Song[title=Song A, playlist=Road Trip]]]
3.4. Lombok @Data on Bidirectional JPA Entities
Lombok’s @Data generates toString(), equals() and hashCode() from all fields, so we get the same cycle without writing any code. JPA entities with a @OneToMany and @ManyToOne pair are the most common case. Logging an entity or adding it to a HashSet starts the recursion.
To fix it, we exclude the back-reference with @ToString.Exclude and @EqualsAndHashCode.Exclude. Without these exclusions, both playlist.toString() and playlist.hashCode() throw StackOverflowError.
@Data
public class Song {
private String title;
@ToString.Exclude
@EqualsAndHashCode.Exclude
private Playlist playlist;
}
String text = playlist.toString(); // Playlist(name=Road Trip, songs=[Song(title=Song A)])
For JPA entities, it is better to base equals() and hashCode() on the entity ID anyway.
3.5. Jackson Serialization of Bidirectional Relations
Jackson calls every getter when it writes JSON. For a Playlist, it writes songs, then each song.playlist, then songs again. Older Jackson versions kept going until the stack was full, whereas newer ones stop once the JSON is nested more than 500 levels deep. The cause is the same cycle.
| Jackson version | Error for the Playlist/Song cycle |
|---|---|
| 2.15.4 | JsonMappingException: Infinite recursion (StackOverflowError) |
| 3.2.3 | StreamConstraintsException: Document nesting depth (501) exceeds the maximum allowed (500 …) |
We mark the parent side with @JsonManagedReference and the child side with @JsonBackReference. Jackson writes the managed side and skips the back side. On reading, it sets song.playlist to the parent again. In Jackson 3, these annotations stay in the com.fasterxml.jackson.annotation package.
// Playlist
@JsonManagedReference
private List<Song> songs = new ArrayList<>();
// Song
@JsonBackReference
private Playlist playlist;
JsonMapper mapper = JsonMapper.builder().build();
String json = mapper.writeValueAsString(playlist); // {"name":"Road Trip","songs":[{"title":"Song A"}]}
Playlist read = mapper.readValue(json, Playlist.class);
boolean linked = read.getSongs().getFirst().getPlaylist() == read; // true
If we never read the JSON back, @JsonIgnore on Song.playlist gives the same output. A DTO (a class made only for the JSON response) without the back-reference works too.
4. Increasing the Stack Size With -Xss
A bigger stack helps only in the deep but correct case from section 3.2, and only when we cannot change the code, for example inside a third-party parser. It does nothing for a missing base case or an object cycle, because those calls never stop.
java -Xss2m -jar app.jar
The -Xss value applies to every platform thread (a regular thread backed by an OS thread), so it is better to give only the deep task a thread with a larger stack. The Thread constructor with a stackSize argument and Thread.ofPlatform().stackSize() both request a size. The Javadoc says the effect is highly platform dependent, and the JVM may ignore it.
Folder deep = Folder.chain(100_000);
AtomicInteger count = new AtomicInteger();
Thread worker = Thread.ofPlatform()
.name("folder-counter")
.stackSize(16L * 1024 * 1024)
.unstarted(() -> count.set(FolderCounter.countRecursive(deep)));
worker.start();
worker.join();
int folders = count.get(); // 100000 (no StackOverflowError on Linux x64)
In our runs on Linux x64 with JDK 25.0.4.1, a tiny recursive method reached 23,608 nested calls with the default 1 MB stack. It reached 49,822 with -Xss2m and 1,042,044 on a 16 MB thread. The numbers vary by platform, and a bigger stack only moves the limit.
5. Why Catching StackOverflowError Is Not a Fix
A catch (StackOverflowError e) block runs on a stack that is still almost full, so a log call inside it can throw a second StackOverflowError. The error can also hit JDK code halfway through updating a collection, so after the catch our objects may be in a broken state.
In a test, assertThrows() can expect the error, because the test only proves the bug and discards its objects. In production code, we fix the cause from section 3.
6. Questions Developers Ask About StackOverflowError
6.1. Is StackOverflowError a Checked or an Unchecked Exception?
Neither. StackOverflowError extends VirtualMachineError, which extends Error. Unlike with a checked exception, the compiler accepts code that neither catches nor declares it.
6.2. Does Java Optimize Tail Recursion?
No. The JVM keeps a frame for every call, even when the recursive call is the last thing the method does, so a tail-recursive method overflows at the same kind of depth as any other recursion. A plain loop has no depth limit.
static long sumToTail(int n, long total) {
if (n <= 0) {
return total;
}
return sumToTail(n - 1, total + n);
}
long large = sumToTail(1_000_000, 0); // java.lang.StackOverflowError
long loop = sumToLoop(1_000_000); // 500000500000
7. Conclusion
Repeating lines in a trace mean one thread made more nested calls than its stack can hold. The repeating block names the methods in the loop, and -XX:MaxJavaStackTraceDepth=0 shows the first caller.
Most cases come from a missing base case, or from two objects that toString() or Jackson keeps following back and forth. The fix is a base case for every input, or an excluded back-reference. Deep but correct recursion becomes a loop with a Deque, and a larger stack stays the last resort.
8. References
- StackOverflowError Javadoc (Java 25)
- JVM Specification, Run-Time Data Areas and Frames
- The java Command, -Xss option (Java 25)
- Thread Javadoc (Java 25)
- Jackson @JsonManagedReference Javadoc
- Jackson @JsonBackReference Javadoc
- Lombok @ToString
Happy Learning !!