首頁 > 軟體

Java中的StackOverflowError錯誤問題及解決方法

2022-07-11 14:01:40

StackOverflowError簡介

StackOverflowError可能會讓Java開發人員感到惱火,因為它是我們可能遇到的最常見的執行時錯誤之一。 在本文中,我們將通過檢視各種程式碼範例以及如何處理它來了解此錯誤是如何發生的。 Stack Frames和StackOverflowerError的發生方式 讓我們從基礎開始。呼叫方法時,將在呼叫堆疊上建立新的堆疊幀(stack frame)。該堆疊框架包含被呼叫方法的引數、其區域性變。

StackOverflowError 可能會讓Java開發人員感到惱火,因為它是我們可能遇到的最常見的執行時錯誤之一。

在本文中,我們將通過檢視各種程式碼範例以及如何處理它來了解此錯誤是如何發生的。

Stack Frames和StackOverflowerError的發生方式

讓我們從基礎開始。呼叫方法時,將在呼叫堆疊上建立新的堆疊幀(stack frame)。該堆疊框架包含被呼叫方法的引數、其區域性變數和方法的返回地址,即在被呼叫方法返回後應繼續執行方法的點。

堆疊幀的建立將繼續,直到到達巢狀方法中的方法呼叫結束。

在此過程中,如果JVM遇到沒有空間建立新堆疊幀的情況,它將丟擲 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 (同樣,沒有預期的異常)獲得的堆疊跟蹤:

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 標誌可以用於從專案的設定或命令列增加堆疊的大小。

結論

在本文中,我們仔細研究了StackOverflower錯誤,包括Java程式碼如何導致它,以及我們如何診斷和修復它。

與本文相關的原始碼可以在GitHub上找到: https://github.com/eugenp/tutorials/tree/master/core-java-modules/core-java-exceptions

到此這篇關於Java中的StackOverflowError錯誤的文章就介紹到這了,更多相關Java StackOverflowError錯誤內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com