1概要


StackOverflowError

は、私たちが遭遇する可能性がある最も一般的なランタイムエラーの1つであるため、Java開発者にとっては厄介なことです。

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


2スタックフレームと

StackOverflowError

の発生方法

基本から始めましょう。

メソッドが呼び出されると、新しいスタックフレームが呼び出しスタック上に作成されます

このスタックフレームは、呼び出されたメソッドのパラメータ、そのローカル変数、およびメソッドの戻りアドレス、つまりメソッドの実行を継続するポイント呼び出されたメソッドが戻りました。

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

  • このプロセス中に、JVMが新しいスタックフレームを作成するためのスペースがないという状況に遭遇すると、

    StackOverflowError

    をスローします。

JVMがこの状況に遭遇する最も一般的な原因は

未定/無限再帰



StackOverflowError

のJavadocの説明には、特定のコードスニペットでの再帰が深すぎるためにエラーがスローされることが述べられています。

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


StackOverflowError

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

このエラーが発生するもう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);
    }
}

ただし、次の例では終了条件が指定されていますが、

calculateFactorial()

メソッドに

-1

の値が渡された場合は満たされないため、終了しない/無限の再帰が発生します。

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


ClassOne

のコンストラクタは

ClassTwo、

のインスタンスを生成しており、

ClassTwo

のコンストラクタは再び

ClassOneをインスタンス化しているため、これは

StackOverflowError__になります。そして、これがスタックをオーバーフローするまで繰り返し発生します。

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

次の例に示すように、

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

が発生したときに行う最善のことは、スタックトレースを慎重に検査して行番号の繰り返しパターンを識別することです。これにより、問題のある再帰があるコードを見つけることができます。

先ほど見たコード例によるいくつかのスタックトレースを調べましょう。


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.<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コードがそれを引き起こす可能性がある方法と、それを診断して修正する方法を含めます。

この記事に関連するソースコードはhttps://github.com/eugenp/tutorials/tree/master/core-java-lang[over on GitHub]にあります。