The StackOverflowError in Java – Java中的StackOverflowError

最后修改: 2017年 5月 15日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

StackOverflowError can be annoying for Java developers, as it’s one of the most common runtime errors we can encounter.

StackOverflowError对于Java开发者来说是很烦人的,因为它是我们最常遇到的运行时错误之一。

In this article, we’ll see how this error can occur by looking at a variety of code examples as well as how we can deal with it.

在这篇文章中,我们将通过看各种代码实例来了解这种错误是如何发生的,以及我们如何处理它。

2. Stack Frames and How StackOverflowError Occurs

2.堆栈框架和StackOverflowError是如何发生的

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

让我们从基础知识开始。当一个方法被调用时,在调用栈上会创建一个新的堆栈框架。这个堆栈框架保存着被调用方法的参数、其局部变量和方法的返回地址,即在被调用的方法返回后应继续执行该方法的位置。

The creation of stack frames will continue until it reaches the end of method invocations found inside nested methods.

堆栈框架的创建将继续进行,直到它到达嵌套方法内发现的方法调用的终点。

During this process, if JVM encounters a situation where there is no space for a new stack frame to be created, it will throw a StackOverflowError.

在这个过程中,如果JVM遇到没有空间创建新堆栈框架的情况,它将抛出一个StackOverflowError

The most common cause for the JVM to encounter this situation is unterminated/infinite recursion – the Javadoc description for StackOverflowError mentions that the error is thrown as a result of too deep recursion in a particular code snippet.

JVM遇到这种情况最常见的原因是无限递归StackOverflowError的Javadoc描述中提到,该错误是由于特定代码片断中递归过深而抛出。

However, recursion is not the only cause for this error. It can also happen in a situation where an application keeps calling methods from within methods until the stack is exhausted. This is a rare case since no developer would intentionally follow bad coding practices. Another rare cause is having a vast number of local variables inside a method.

然而,递归并不是造成这种错误的唯一原因。它也可能发生在应用程序不断从方法中调用方法,直到堆栈被耗尽的情况下。这是一种罕见的情况,因为没有开发者会故意遵循不良的编码实践。另一个罕见的原因是在一个方法内有大量的局部变量

The StackOverflowError can also be thrown when an application is designed to have cyclic relationships between classes. In this situation, the constructors of each other are getting called repetitively which causes this error to be thrown. This can also be considered as a form of recursion.

当应用程序被设计成有c类之间的循环关系时,也会抛出StackOverflowError。在这种情况下,彼此的构造函数会被重复调用,从而导致这个错误被抛出。这也可以被认为是一种递归的形式。

Another interesting scenario that causes this error is if a class is being instantiated within the same class as an instance variable of that class. This will cause the constructor of the same class to be called again and again (recursively) which eventually results in a StackOverflowError.

另一个有趣的情况是,如果一个类在同一个类中被实例化为该类的一个实例变量,就会引起这个错误。这将导致同一个类的构造函数被反复调用(递归),最终导致StackOverflowError.

In the next section, we’ll look at some code examples that demonstrate these scenarios.

在下一节,我们将看一些演示这些情况的代码例子。

3. StackOverflowError in Action

3.StackOverflowError在行动

In the example shown below, a StackOverflowError will be thrown due to unintended recursion, where the developer has forgotten to specify a termination condition for the recursive behavior:

在下面的例子中,一个StackOverflowError将被抛出,这是由于非故意的递归,开发者忘记为递归行为指定一个终止条件。

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

Here, the error is thrown on all occasions for any value passed into the method:

在这里,对于传入方法的任何值,都会抛出错误。

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 a termination condition is specified but is never being met if a value of -1 is passed to the calculateFactorial() method, which causes unterminated/infinite recursion:

然而,在下一个例子中,指定了一个终止条件,但如果向calculateFactorial()方法传递一个-1的值,则永远不会被满足,这将导致未终止的/无限的递归。

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, the error could have been completely avoided if the termination condition was simply put as:

在这个特殊的案例中,如果终止条件简单地写成这样,就可以完全避免这个错误了。

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

Here’s the test that shows this scenario 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 happens as a result of cyclic relationships between classes. Let’s consider ClassOne and ClassTwo, which instantiate each other inside their constructors causing a cyclic relationship:

现在让我们来看看StackOverflowError是由于类之间的循环关系而发生的情况。让我们考虑一下ClassOneClassTwo,它们在构造函数中相互实例化,造成了循环关系。

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 say that we try to instantiate ClassOne as seen in this test:

现在让我们说,我们尝试实例化ClassOne,正如在这个测试中看到的那样。

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

This ends up with a StackOverflowError since the constructor of ClassOne is instantiating ClassTwo, and the constructor of ClassTwo again is instantiating ClassOne. And this repeatedly happens until it overflows the stack.

这最终导致了StackOverflowError,因为ClassOne的构造函数正在实例化ClassTwo,而ClassTwo的构造函数又在实例化ClassOne。而且这种情况反复发生,直到它溢出堆栈。

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

接下来,我们将看看当一个类在同一个类中被实例化为该类的一个实例变量时会发生什么。

As seen in the next example, AccountHolder instantiates itself as an instance variable jointAccountHolder:

正如在下一个例子中所看到的,AccountHolder将自己实例化为一个实例变量jointAccountHolder

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

When the AccountHolder class is instantiated, a StackOverflowError is thrown due to the recursive calling of the constructor as seen in this test:

AccountHolder类被实例化时一个StackOverflowError被抛出,原因是在这个测试中看到了构造函数的递归调用。

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

4. Dealing With StackOverflowError

4.处理StackOverflowError

The best thing to do when a StackOverflowError is encountered is to inspect the stack trace cautiously to identify the repeating pattern of line numbers. This will enable us to locate the code that has problematic recursion.

当遇到StackOverflowError时,最好的办法是谨慎地检查堆栈跟踪,找出行号的重复模式。这将使我们能够找到有问题的递归的代码。

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

让我们检查一下由我们前面看到的代码例子引起的一些堆栈痕迹。

This stack trace is produced by InfiniteRecursionWithTerminationConditionManualTest if we omit the expected exception declaration:

如果我们省略了InfiniteRecursionWithTerminationConditionManualTest的异常声明,就会产生这个堆栈跟踪。

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, line number 5 can be seen repeating. This is where the recursive call is being done. Now it’s just a matter of examining the code to see if the recursion is done in a correct manner.

这里,可以看到第5行在重复。这就是正在进行递归调用的地方。现在只需要检查一下代码,看看递归是否以正确的方式进行。

Here is the stack trace we get by executing CyclicDependancyManualTest (again, without expected exception):

这是我们通过执行CyclicDependancyManualTest(同样,没有预期异常)得到的堆栈跟踪。

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 cause the problem in the two classes that are in a cyclic relationship. Line number 9 of ClassTwo and line number 9 of the ClassOne point to the location inside the constructor where it tries to instantiate the other class.

这个堆栈跟踪显示了在两个处于循环关系的类中导致问题的行号。ClassTwo的第9行和ClassOne的第9行指向构造函数内部的位置,在那里它试图实例化其他类。

Once the code is being thoroughly inspected and if none of the following (or any other code logic error) is the cause of the error:

一旦代码被彻底检查,如果以下内容(或任何其他代码逻辑错误)都不是错误的原因。

  • Incorrectly implemented recursion (i.e. with no termination condition)
  • Cyclic dependency between classes
  • Instantiating a class within the same class as an instance variable of that class

It would be a good idea to try and increase the stack size. Depending on the JVM installed, the default stack size could vary.

尝试增加堆栈大小将是一个好主意。根据所安装的JVM,默认的堆栈大小可能有所不同。

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

-Xss标志可以用来增加堆栈的大小,可以从项目的配置或命令行中选择。

5. Conclusion

5.结论

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

在这篇文章中,我们仔细研究了StackOverflowError,包括Java代码如何导致它以及我们如何诊断和修复它。

Source code related to this article can be found over on GitHub.

与本文有关的源代码可以在GitHub上找到over