1. 概要

StackOverflowError は、発生する可能性のある最も一般的なランタイムエラーの1つであるため、Java開発者にとって煩わしい場合があります。

この記事では、さまざまなコード例とその対処方法を確認することで、このエラーがどのように発生するかを確認します。

2. スタックフレームとStackOverflowErrorの発生方法

基本から始めましょう。 メソッドが呼び出されると、呼び出しスタックに新しいスタックフレームが作成されます。このスタックフレームは、呼び出されたメソッドのパラメーター、そのローカル変数、およびメソッドのリターンアドレスを保持します。 呼び出されたメソッドが戻った後、メソッドの実行を続行するポイント。

スタックフレームの作成は、ネストされたメソッド内で見つかったメソッド呼び出しの最後に到達するまで続行されます。

このプロセス中に、JVMが新しいスタックフレームを作成するためのスペースがない状況に遭遇した場合、JVMはStackOverflowErrorをスローします。

JVMがこの状況に遭遇する最も一般的な原因は、無期限/無限再帰です。 StackOverflowError のJavadocの説明では、特定の再帰が深すぎるためにエラーがスローされると記載されています。コードスニペット。

ただし、このエラーの原因は再帰だけではありません。 また、スタックが使い果たされるまで、アプリケーションがメソッド内からメソッドを呼び出し続ける状況でも発生する可能性があります。 開発者が意図的に悪いコーディング慣行に従うことはないため、これはまれなケースです。 もう1つのまれな原因は、メソッド内に膨大な数のローカル変数があるです。

StackOverflowError は、アプリケーションがクラス間に cyclic関係を持つように設計されている場合にもスローされる可能性があります。 この状況では、互いのコンストラクターが繰り返し呼び出されるため、このエラーがスローされます。 これは、再帰の一形態と見なすこともできます。

このエラーの原因となるもう1つの興味深いシナリオは、クラスがそのクラスのインスタンス変数と同じクラス内でインスタンス化されている場合です。 これにより、同じクラスのコンストラクターが何度も(再帰的に)呼び出され、最終的にStackOverflowError。が発生します。

次のセクションでは、これらのシナリオを示すいくつかのコード例を見ていきます。

3. StackOverflowErrorの動作

以下に示す例では、意図しない再帰が原因で StackOverflowError がスローされます。この場合、開発者は再帰動作の終了条件を指定するのを忘れています。

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

ここでは、メソッドに渡された値に対してすべての場合にエラーがスローされます。

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

ただし、次の例では、終了条件が指定されていますが、 -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が発生するシナリオを見てみましょう。 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;
    }
}

ここで、このテストで見られるように、ClassOneをインスタンス化しようとしているとしましょう。

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

これは最終的に StackOverflowError のコンストラクター以来 ClassOne インスタンス化しています ClassTwo、 とのコンストラクタ ClassTwo 再びインスタンス化しています ClassOne。 そして、これはスタックをオーバーフローするまで繰り返し発生します。

次に、クラスがそのクラスのインスタンス変数と同じクラス内でインスタンス化されているときに何が起こるかを見ていきます。

次の例に示すように、AccountHolderはそれ自体をインスタンス変数jointAccountHolderとしてインスタンス化します。

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

4. StackOverflowErrorの処理

StackOverflowError が発生した場合の最善の方法は、スタックトレースを注意深く調べて、行番号の繰り返しパターンを特定することです。 これにより、再帰に問題のあるコードを見つけることができます。

前に見たコード例によって引き起こされたいくつかのスタックトレースを調べてみましょう。

このスタックトレースは、予想される例外宣言を省略した場合、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)

ここでは、行番号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)

このスタックトレースは、循環関係にある2つのクラスで問題を引き起こす行番号を示しています。 ClassTwoの行番号9とClassOneの行番号9は、他のクラスをインスタンス化しようとするコンストラクター内の場所を指しています。

コードが徹底的に検査され、次のいずれ(または他のコードロジックエラー)がエラーの原因でない場合:

  • 誤って実装された再帰(つまり、 終了条件なし)
  • クラス間の循環依存
  • そのクラスのインスタンス変数と同じクラス内のクラスをインスタンス化する

スタックサイズを増やしてみることをお勧めします。 インストールされているJVMに応じて、デフォルトのスタックサイズは異なる場合があります。

-Xss フラグを使用して、プロジェクトの構成またはコマンドラインからスタックのサイズを増やすことができます。

5. 結論

この記事では、 StackOverflowError について詳しく見ていきました。これには、Javaコードが原因となる可能性と、診断と修正の方法が含まれます。

この記事に関連するソースコードは、GitHubにあります。