>  기사  >  Java  >  Java에서 StackOverflowError 오류 문제를 해결하는 방법

Java에서 StackOverflowError 오류 문제를 해결하는 방법

WBOY
WBOY앞으로
2023-05-13 20:49:043290검색

StackOverflowError 소개

StackOverflowError는 우리가 접할 수 있는 가장 일반적인 런타임 오류 중 하나이므로 Java 개발자에게는 짜증스러울 수 있습니다. 이번 글에서는 다양한 코드 예시를 살펴보며 이러한 오류가 어떻게 발생하는지, 어떻게 대처하는지 알아보겠습니다. 스택 프레임 및 StackOverflowerError가 발생하는 방법 기본부터 시작해 보겠습니다. 메서드가 호출되면 호출 스택에 새 스택 프레임이 생성됩니다. 스택 프레임에는 호출된 메서드의 매개변수와 해당 지역 변수가 포함되어 있습니다.

StackOverflowError는 우리가 접할 수 있는 가장 일반적인 런타임 오류 중 하나이므로 Java 개발자에게는 짜증스러울 수 있습니다.

이 글에서는 다양한 코드 예제를 살펴보고 이 오류가 어떻게 발생하는지, 어떻게 처리하는지 이해하겠습니다.

스택 프레임 및 StackOverflowerError가 발생하는 방법

기본부터 시작해 보겠습니다. 메서드가 호출되면 호출 스택에 새 스택 프레임이 생성됩니다. 이 스택 프레임에는 호출된 메서드의 매개 변수, 해당 지역 변수, 메서드의 반환 주소(호출된 메서드가 반환된 후 메서드 실행이 계속되어야 하는 지점)가 포함되어 있습니다.

스택 프레임 생성은 중첩된 메서드 내의 메서드 호출이 끝날 때까지 계속됩니다.

이 과정에서 JVM이 새 스택 프레임을 생성할 공간이 없는 상황에 직면하면 StackOverflower 오류가 발생합니다. StackOverflower 错误。

JVM遇到这种情况的最常见原因是未终止/无限递归——StackOverflowerr的Javadoc描述提到,错误是由于特定代码段中的递归太深而引发的。

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

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

另一个引起此错误的有趣场景是,如果一个类在同一个类中作为该类的实例变量实例化。这将导致一次又一次(递归)调用同一类的构造函数,最终导致堆栈溢出错误。

StackOverflowerError正在运行

在下面所示的示例中,由于意外递归,开发人员忘记为递归行为指定终止条件,将抛出StackOverflowError错误:

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

在这里,对于传递到方法中的任何值,在任何情况下都会引发错误:

public class UnintendedInfiniteRecursionManualTest {
    @Test(expected = <a href="https://javakk.com/tag/stackoverflowerror" rel="external nofollow"  rel="external nofollow"      title="查看更多关于 StackOverflowError 的文章" target="_blank">StackOverflowError</a>.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);
    }
}

但是,在下一个示例中,指定了终止条件,但如果将值 -1 传递给 calculateFactorial() 方法,则永远不会满足终止条件,这会导致未终止/无限递归:

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

这组测试演示了此场景:

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);
    }
}

在这种特殊情况下,如果将终止条件简单地表示为:

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

下面的测试在实践中显示了这种情况:

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

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

现在让我们来看一个场景,其中StackOverflowError错误是由于类之间的循环关系而发生的。让我们考虑 ClassOne 和 ClassTwo ,它们在其构造函数中相互实例化,从而产生循环关系:

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;
    }
}

现在让我们假设我们尝试实例化ClassOne,如本测试中所示:

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

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

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

如下一个示例所示, AccountHolder 将自身实例化为实例变量 JointaCountHolder :

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

当 AccountHolder 类实例化时,由于构造函数的递归调用,会引发StackOverflowError错误,如本测试中所示:

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

解决StackOverflowError

当遇到StackOverflowError堆栈溢出错误时,最好的做法是仔细检查堆栈跟踪,以识别行号的重复模式。这将使我们能够定位具有问题递归的代码。

让我们研究一下由我们前面看到的代码示例引起的几个堆栈跟踪。

如果忽略预期的异常声明,则此堆栈跟踪由 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)

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

下面是我们通过执行 CyclicDependancyManualTest

JVM에서 이 문제가 발생하는 가장 일반적인 이유는 종료되지 않은/무한 재귀입니다. StackOverflowerr의 Javadoc 설명에서는 특정 코드 부분에서 너무 깊은 재귀로 인해 오류가 발생한다고 언급합니다. 🎜🎜 그러나 재귀가 이 오류의 유일한 원인은 아닙니다. 이는 스택이 소진될 때까지 애플리케이션이 메서드 내에서 메서드를 계속 호출하는 상황에서도 발생할 수 있습니다. 어떤 개발자도 의도적으로 열악한 코딩 관행을 따르지 않기 때문에 이는 드문 상황입니다. 또 다른 드문 이유는 메서드에 지역 변수가 많기 때문입니다. 🎜🎜 StackOverflowError는 애플리케이션이 클래스 간에 순환 관계를 가지도록 설계된 경우에도 발생할 수 있습니다. 이 경우 서로의 생성자가 반복적으로 호출되어 이러한 오류가 발생합니다. 이것도 일종의 재귀라고 볼 수 있다. 🎜🎜이 오류를 일으키는 또 다른 흥미로운 시나리오는 클래스가 해당 클래스의 인스턴스 변수와 동일한 클래스 내에서 인스턴스화되는 경우입니다. 이로 인해 동일한 클래스의 생성자가 계속해서(재귀적으로) 호출되어 결국 스택 오버플로 오류가 발생합니다. 🎜🎜StackOverflowError in action🎜🎜아래 예에서는 예상치 못한 재귀로 인해 개발자가 재귀 동작에 대한 종료 조건을 지정하는 것을 잊었고 StackOverflowError 오류가 발생합니다. 🎜
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)
🎜여기서 메서드에 전달된 모든 값에 대해 , 어떤 경우에도 오류가 발생합니다: 🎜rrreee🎜 그러나 다음 예에서는 종료 조건이 지정되었지만 -1 값이 calculateFacttorial() code> 메소드를 사용하면 종료 조건이 충족되지 않아 종료되지 않거나 무한 재귀가 발생합니다. 🎜rrreee🎜 이 테스트 세트는 다음 시나리오를 보여줍니다. 🎜rrreee🎜이 특별한 경우 종료 조건이 간단히 다음과 같이 표현됩니다. 🎜rrreee 🎜 아래 테스트는 실제로 이런 상황을 보여줍니다. 🎜rrreee🎜 이제 클래스 간의 순환 관계로 인해 StackOverflowError 오류가 발생하는 시나리오를 살펴보겠습니다. 생성자에서 서로 인스턴스화하여 순환 관계를 생성하는 <code>ClassOneClassTwo를 고려해 보겠습니다. 🎜rrreeerrreee🎜 이제 Shown과 같이 ClassOne을 인스턴스화하려고 한다고 가정해 보겠습니다. 이 테스트에서는: 🎜rrreee🎜ClassOne의 생성자가 ClassTwo를 인스턴스화하는 반면 ClassTwo의 생성자는 를 인스턴스화하기 때문에 궁극적으로 StackOverflowError가 발생합니다. >ClassOne 다시 한번. 이는 스택이 오버플로될 때까지 반복적으로 발생합니다. 🎜🎜다음으로, 클래스가 해당 클래스의 인스턴스 변수와 동일한 클래스에서 인스턴스화되면 어떤 일이 발생하는지 살펴보겠습니다. 🎜🎜다음 예에서 볼 수 있듯이 AccountHolder는 인스턴스 변수 JointaCountHolder로 자신을 인스턴스화합니다. 🎜rrreee🎜AccountHolder 클래스가 인스턴스화될 때, 이후 이 테스트에 표시된 것처럼 생성자에 대한 재귀 호출로 StackOverflowError가 발생합니다. 🎜rrreee🎜Resolving StackOverflowError🎜🎜StackOverflowError가 발생하면 스택 추적을 주의 깊게 검사하여 줄 번호 모델의 중복을 식별하는 것이 가장 좋습니다. 이를 통해 문제가 있는 재귀가 있는 코드를 찾을 수 있습니다. 🎜🎜앞서 본 코드 예제로 인해 발생한 몇 가지 스택 추적을 살펴보겠습니다. 🎜🎜예상된 예외 선언이 무시되면 InfiniteCursionWithTerminationConditionManualTest에 의해 이 스택 추적이 생성됩니다. 🎜rrreee🎜여기에서 5행이 반복되는 것을 볼 수 있습니다. 재귀 호출이 이루어지는 곳입니다. 이제 재귀가 올바른 방식으로 수행되었는지 확인하기 위해 코드를 확인하기만 하면 됩니다. 🎜🎜다음은 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)

该堆栈跟踪显示了在循环关系中的两个类中导致问题的行号。ClassTwo的第9行和ClassOne的第9行指向构造函数中试图实例化另一个类的位置。

彻底检查代码后,如果以下任何一项(或任何其他代码逻辑错误)都不是错误的原因:

  • 错误实现的递归(即没有终止条件)

  • 类之间的循环依赖关系

  • 在同一个类中实例化一个类作为该类的实例变量

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

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

위 내용은 Java에서 StackOverflowError 오류 문제를 해결하는 방법의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 yisu.com에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제