StackOverflowError errors in Java

Introduction

StackOverflowError can be annoying for Java developers as it is one of the most common runtime errors we may encounter.

In this article, we will learn how this error occurs by looking at various code examples and how to handle it.

Stack Frames and StackOverflowerError Occur

Let’s start with the basics. When a method is called, a new stack frame is created on the call stack. This stack frame contains the parameters of the called method, its local variables and the return address of the method, i.e. the point at which execution of the method should continue after the called method returns.

The creation of the stack frame will continue until the end of the method call in the nested method is reached.

During this process, if the JVM encounters a situation where there is no room to create a new stack frame, it will throw a StackOverflower error.

The most common reason the JVM encounters this is unterminated/infinite recursion - the Javadoc description of StackOverflowerr mentions that the error is triggered by the recursion being too deep in a particular code segment.

However, recursion is not the only cause of this error. It can also occur in cases where the application keeps calling methods from within methods until the stack is depleted. This is a rare case because no developer would intentionally follow poor coding practices. Another rare cause is when there are a large number of local variables in a method.

StackOverflowError can also be thrown when the application is designed to have a circular relationship between classes, in which case repeated calls to each other’s constructors are made, raising this error. This can also be considered as a form of recursion.

Another interesting scenario that causes this error is if a class is instantiated in the same class as an instance variable of that class. This will result in calling the constructor of the same class again and again (recursively), eventually leading to a stack overflow error.

StackOverflowerError is running

In the example shown below, a StackOverflowError error will be thrown due to accidental recursion where the developer forgot to specify a termination condition for the recursive behavior.

public class UnintendedInfiniteRecursion {
    public int calculateFactorial(int number) {
        return number * calculateFactorial(number - 1);
    }
}

Here, for any value passed into the method, an error is raised in any case: the

public class UnintendedInfiniteRecursionManualTest {
    @Test(expected = StackOverflowError.class)
    public void givenPositiveIntNoOne_whenCalFact_thenThrowsException() {
        int numToCalcFactorial= 1;
        UnintendedInfiniteRecursion uir 
          = new UnintendedInfiniteRecursion();
        
        uir.calculateFactorial(numToCalcFactorial);
    }
    
    @Test(expected = StackOverflowError.class)
    public void givenPositiveIntGtOne_whenCalcFact_thenThrowsException() {
        int numToCalcFactorial= 2;
        UnintendedInfiniteRecursion uir 
          = new UnintendedInfiniteRecursion();
        
        uir.calculateFactorial(numToCalcFactorial);
    }
    
    @Test(expected = StackOverflowError.class)
    public void givenNegativeInt_whenCalcFact_thenThrowsException() {
        int numToCalcFactorial= -1;
        UnintendedInfiniteRecursion uir 
          = new UnintendedInfiniteRecursion();
        
        uir.calculateFactorial(numToCalcFactorial);
    }
}

However, in the next example, the termination condition is specified, but if the value -1 is passed to the calculateFactorial() method, the termination condition is never met, which would result in unterminated/infinite recursion:.

public class InfiniteRecursionWithTerminationCondition {
    public int calculateFactorial(int number) {
       return number == 1 ? 1 : number * calculateFactorial(number - 1);
    }
}

This set of tests demonstrates this scenario.

public class InfiniteRecursionWithTerminationConditionManualTest {
    @Test
    public void givenPositiveIntNoOne_whenCalcFact_thenCorrectlyCalc() {
        int numToCalcFactorial = 1;
        InfiniteRecursionWithTerminationCondition irtc 
          = new InfiniteRecursionWithTerminationCondition();

        assertEquals(1, irtc.calculateFactorial(numToCalcFactorial));
    }

    @Test
    public void givenPositiveIntGtOne_whenCalcFact_thenCorrectlyCalc() {
        int numToCalcFactorial = 5;
        InfiniteRecursionWithTerminationCondition irtc 
          = new InfiniteRecursionWithTerminationCondition();

        assertEquals(120, irtc.calculateFactorial(numToCalcFactorial));
    }

    @Test(expected = StackOverflowError.class)
    public void givenNegativeInt_whenCalcFact_thenThrowsException() {
        int numToCalcFactorial = -1;
        InfiniteRecursionWithTerminationCondition irtc 
          = new InfiniteRecursionWithTerminationCondition();

        irtc.calculateFactorial(numToCalcFactorial);
    }
}

In this particular case, if the termination condition is simply expressed as

public class RecursionWithCorrectTerminationCondition {
    public int calculateFactorial(int number) {
        return number <= 1 ? 1 : number * calculateFactorial(number - 1);
    }
}

The following test shows this in practice.

public class RecursionWithCorrectTerminationConditionManualTest {
    @Test
    public void givenNegativeInt_whenCalcFact_thenCorrectlyCalc() {
        int numToCalcFactorial = -1;
        RecursionWithCorrectTerminationCondition rctc 
          = new RecursionWithCorrectTerminationCondition();

        assertEquals(1, rctc.calculateFactorial(numToCalcFactorial));
    }
}

Now let’s look at a scenario where the StackOverflowError error occurs due to a circular relationship between classes. Let’s consider ClassOne and ClassTwo, which instantiate each other in their constructors, thus creating a cyclic relationship.

public class ClassOne {
    private int oneValue;
    private ClassTwo clsTwoInstance = null;
    
    public ClassOne() {
        oneValue = 0;
        clsTwoInstance = new ClassTwo();
    }
    
    public ClassOne(int oneValue, ClassTwo clsTwoInstance) {
        this.oneValue = oneValue;
        this.clsTwoInstance = clsTwoInstance;
    }
}
public class ClassTwo {
    private int twoValue;
    private ClassOne clsOneInstance = null;
    
    public ClassTwo() {
        twoValue = 10;
        clsOneInstance = new ClassOne();
    }
    
    public ClassTwo(int twoValue, ClassOne clsOneInstance) {
        this.twoValue = twoValue;
        this.clsOneInstance = clsOneInstance;
    }
}

Now let’s suppose we try to instantiate ClassOne as shown in this test.

public class CyclicDependancyManualTest {
    @Test(expected = StackOverflowError.class)
    public void whenInstanciatingClassOne_thenThrowsException() {
        ClassOne obj = new ClassOne();
    }
}

This eventually leads to a StackOverflowError error because the constructor of ClassOne instantiates ClassTwo, and the constructor of ClassTwo instantiates ClassOne again. This happens repeatedly until it overflows the stack.

Next, we will look at what happens when a class is instantiated in the same class as an instance variable of that class.

As the next example shows, AccountHolder instantiates itself as the instance variable JointaCountHolder.

public class AccountHolder {
    private String firstName;
    private String lastName;
    
    AccountHolder jointAccountHolder = new AccountHolder();
}

When the AccountHolder class is instantiated, a StackOverflowError error is raised due to a recursive call to the constructor, as shown in this test.

public class AccountHolderManualTest {
    @Test(expected = StackOverflowError.class)
    public void whenInstanciatingAccountHolder_thenThrowsException() {
        AccountHolder holder = new AccountHolder();
    }
}

Resolving StackOverflowError

When encountering a StackOverflowError stack overflow error, the best practice is to carefully examine the stack trace to identify the repeating pattern of line numbers. This will allow us to locate the code with the problematic recurrence.

Let’s examine a few stack traces caused by the code examples we saw earlier.

If the expected exception declaration is ignored, this stack trace is generated by InfiniteCursionWithTerminationConditionManualTest.

java.lang.StackOverflowError

 at c.b.s.InfiniteRecursionWithTerminationCondition
  .calculateFactorial(InfiniteRecursionWithTerminationCondition.java:5)
 at c.b.s.InfiniteRecursionWithTerminationCondition
  .calculateFactorial(InfiniteRecursionWithTerminationCondition.java:5)
 at c.b.s.InfiniteRecursionWithTerminationCondition
  .calculateFactorial(InfiniteRecursionWithTerminationCondition.java:5)
 at c.b.s.InfiniteRecursionWithTerminationCondition
  .calculateFactorial(InfiniteRecursionWithTerminationCondition.java:5)

Here, you can see that line 5 is repeated. This is where the recursive call is made. Now it’s just a matter of checking the code to see if the recursion is completing in the right way.

Here is the stack trace we obtained by executing CyclicDependancyManualTest (again, without the expected exceptions).

java.lang.StackOverflowError
  at c.b.s.ClassTwo.<init>(ClassTwo.java:9)
  at c.b.s.ClassOne.<init>(ClassOne.java:9)
  at c.b.s.ClassTwo.<init>(ClassTwo.java:9)
  at c.b.s.ClassOne.<init>(ClassOne.java:9)

This stack trace shows the line numbers that are causing problems in both classes in the loop relationship. line 9 of ClassTwo and line 9 of ClassOne point to the location in the constructor where an attempt is made to instantiate another class.

After a thorough examination of the code, if any of the following (or any other code logic error) is not the cause of the error.

  • incorrectly implemented recursion (i.e., no termination condition)
  • circular dependencies between classes
  • Instantiation of a class within the same class as an instance variable of that class

It is a good idea to try to increase the stack size. Depending on the installed JVM, the default stack size may vary.

The -Xss flag can be used to increase the stack size from the project’s configuration or from the command line.

Conclusion

In this article, we took a closer look at the StackOverflower error, including how Java code can cause it, and how we can diagnose and fix it.