JavaにおけるStackOverflowError

JavaのStackOverflowError

1. 概要

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

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

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

基本から始めましょう。 When a method is called, a new stack frame gets created on the call stack.このスタックフレームは、呼び出されたメソッドのパラメータ、そのローカル変数、およびメソッドの戻りアドレスを保持します。 呼び出されたメソッドが戻った後、メソッドの実行を継続するポイント。

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

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

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

ただし、このエラーの原因は再帰だけではありません。 また、アプリケーションがcalling methods from within methods until the stack is exhaustedを保持している状況でも発生する可能性があります。 これはまれなケースです。開発者が意図的に悪いコーディングプラクティスに従うことはないからです。 もう1つのまれな原因は、having a vast number of local variables inside a methodです。

StackOverflowErrorは、アプリケーションがcyclic relationships between classesを持つように設計されている場合にもスローされます。 この状況では、互いのコンストラクターが繰り返し呼び出されるため、このエラーがスローされます。 これは、再帰の形式と考えることもできます。

このエラーを引き起こすもう1つの興味深いシナリオは、class is being instantiated within the same class as an instance variable of that classの場合です。 これにより、同じクラスのコンストラクターが何度も(再帰的に)呼び出され、最終的に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();
    }
}

ClassOneのコンストラクターがClassTwo,をインスタンス化し、ClassTwoのコンストラクターが再びClassOne.をインスタンス化するため、これはStackOverflowErrorになります。これはオーバーフローするまで繰り返し発生します。スタック。

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

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

public class AccountHolder {
    private String firstName;
    private String lastName;

    AccountHolder jointAccountHolder = new AccountHolder();
}

AccountHolderクラスがインスタンス化されると,がスローされます。これは、次のテストで見られるように、コンストラクターが再帰的に呼び出されるためです。

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

4. StackOverflowErrorの処理

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

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

このスタックトレースは、expectedの例外宣言を省略した場合、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を実行して取得したスタックトレースは次のとおりです(ここでも、expectedの例外はありません)。

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

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

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

  • 誤って実装された再帰(つまり、 終了条件なし)

  • クラス間の循環依存

  • 同じクラス内のクラスを、そのクラスのインスタンス変数としてインスタンス化する

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

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

5. 結論

この記事では、Javaコードがどのようにそれを引き起こす可能性があるか、そしてどのようにそれを診断して修正することができるかなど、StackOverflowErrorを詳しく調べました。

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