とは何ですかStackOverflowError
、何が原因で、どのように対処すればよいですか?
15 に答える
パラメーターとローカル変数はスタックに割り当てられます(参照型の場合、オブジェクトはヒープ上に存在し、スタック内の変数はヒープ上のそのオブジェクトを参照します)。スタックは通常、アドレス空間の上端に存在し、使い尽くされると、アドレス空間の底(つまりゼロ) に向かいます。
プロセスには、プロセスの最下部にあるheapもあります。メモリを割り当てると、このヒープはアドレス空間の上限に向かって大きくなる可能性があります。ご覧のとおり、ヒープがスタックと「衝突」する可能性があります (構造プレートに少し似ています!!!)。
スタック オーバーフローの一般的な原因は、不正な再帰呼び出しです。通常、これは再帰関数に正しい終了条件がない場合に発生するため、それ自体が永遠に呼び出されてしまいます。または、終了条件に問題がない場合でも、条件を満たす前に再帰呼び出しが多すぎることが原因である可能性があります。
ただし、GUI プログラミングでは、間接再帰を生成することができます。たとえば、アプリがペイント メッセージを処理していて、それらの処理中に、システムに別のペイント メッセージを送信させる関数を呼び出す場合があります。ここでは、明示的に自分自身を呼び出していませんが、OS/VM が呼び出しています。
それらに対処するには、コードを調べる必要があります。自分自身を呼び出す関数がある場合は、終了条件があることを確認してください。持っている場合は、関数を呼び出すときに少なくとも引数の1つを変更したことを確認してください。そうしないと、再帰的に呼び出された関数に目に見える変更はなく、終了条件は役に立ちません。また、有効な終了条件に達する前にスタック領域がメモリ不足になる可能性があることにも注意してください。そのため、メソッドがより多くの再帰呼び出しを必要とする入力値を処理できることを確認してください。
明らかな再帰関数がない場合は、(上記の暗黙的なケースのように)関数を間接的に呼び出すライブラリ関数を呼び出しているかどうかを確認してください。
これを説明するには、まず、ローカル変数とオブジェクトがどのように格納されるかを理解しましょう。
ローカル変数はスタックに格納されます:
画像を見れば、物事がどのように機能しているかを理解できるはずです。
関数呼び出しが Java アプリケーションによって呼び出されると、呼び出しスタックにスタック フレームが割り当てられます。スタック フレームには、呼び出されたメソッドのパラメーター、そのローカル パラメーター、およびメソッドのリターン アドレスが含まれます。戻りアドレスは、呼び出されたメソッドが戻った後にプログラムの実行が続行される実行ポイントを示します。新しいスタック フレーム用のスペースがない場合StackOverflowError
、Java 仮想マシン (JVM) によって がスローされます。
Java アプリケーションのスタックを使い果たす可能性がある最も一般的なケースは、再帰です。再帰では、メソッドは実行中に自分自身を呼び出します。再帰は強力な汎用プログラミング手法と見なされていますが、StackOverflowError
.
a をスローする例をStackOverflowError
以下に示します。
StackOverflowErrorExample.java:
public class StackOverflowErrorExample {
public static void recursivePrint(int num) {
System.out.println("Number: " + num);
if (num == 0)
return;
else
recursivePrint(++num);
}
public static void main(String[] args) {
StackOverflowErrorExample.recursivePrint(1);
}
}
この例ではrecursivePrint
、整数を出力し、次の連続する整数を引数として自身を呼び出すという再帰メソッドを定義します。パラメータとして渡すまで、再帰は終了0
します。ただし、この例では、1 からパラメーターを渡し、そのフォロワーが増加しているため、再帰は決して終了しません。
スレッド スタックのサイズを 1 MB に指定するフラグを使用した実行例を-Xss1M
以下に示します。
Number: 1
Number: 2
Number: 3
...
Number: 6262
Number: 6263
Number: 6264
Number: 6265
Number: 6266
Exception in thread "main" java.lang.StackOverflowError
at java.io.PrintStream.write(PrintStream.java:480)
at sun.nio.cs.StreamEncoder.writeBytes(StreamEncoder.java:221)
at sun.nio.cs.StreamEncoder.implFlushBuffer(StreamEncoder.java:291)
at sun.nio.cs.StreamEncoder.flushBuffer(StreamEncoder.java:104)
at java.io.OutputStreamWriter.flushBuffer(OutputStreamWriter.java:185)
at java.io.PrintStream.write(PrintStream.java:527)
at java.io.PrintStream.print(PrintStream.java:669)
at java.io.PrintStream.println(PrintStream.java:806)
at StackOverflowErrorExample.recursivePrint(StackOverflowErrorExample.java:4)
at StackOverflowErrorExample.recursivePrint(StackOverflowErrorExample.java:9)
at StackOverflowErrorExample.recursivePrint(StackOverflowErrorExample.java:9)
at StackOverflowErrorExample.recursivePrint(StackOverflowErrorExample.java:9)
...
JVM の初期設定によって結果が異なる場合がありますが、最終的にStackOverflowError
は がスローされます。この例は、注意して実装しないと、再帰がどのように問題を引き起こす可能性があるかを示す非常に良い例です。
StackOverflowError の処理方法
最も簡単な解決策は、スタック トレースを注意深く調べて、行番号の繰り返しパターンを検出することです。これらの行番号は、再帰的に呼び出されるコードを示します。これらの行を検出したら、コードを注意深く調べて、再帰が終了しない理由を理解する必要があります。
再帰が正しく実装されていることを確認したら、より多くの呼び出しを許可するために、スタックのサイズを増やすことができます。インストールされている Java 仮想マシン (JVM) に応じて、デフォルトのスレッド スタック サイズは512 KB または 1 MB になります。
-Xss
フラグを使用して、スレッド スタック サイズを増やすことができます。このフラグは、プロジェクトの構成またはコマンド ラインを介して指定できます。-Xss
引数 の形式は次の とおりです。-Xss<size>[g|G|m|M|k|K]
次のような関数がある場合:
int foo()
{
// more stuff
foo();
}
その後、foo() は自分自身を呼び出し続け、どんどん深くなり、現在の関数を追跡するために使用されるスペースがいっぱいになると、スタック オーバーフロー エラーが発生します。
スタック オーバーフローとは、まさにそれを意味します: スタック オーバーフロー。通常、プログラムには、ローカル スコープの変数と、ルーチンの実行が終了したときに戻るアドレスを含むスタックが 1 つあります。そのスタックは、メモリ内のどこかで固定されたメモリ範囲になる傾向があるため、値を含めることができる量が制限されます。
スタックが空の場合、ポップできません。ポップすると、スタック アンダーフロー エラーが発生します。
スタックがいっぱいの場合、プッシュできません。そうすると、スタック オーバーフロー エラーが発生します。
そのため、スタックに割り当てすぎた場所にスタック オーバーフローが発生します。たとえば、前述の再帰で。
一部の実装では、いくつかの形式の再帰を最適化します。特に末尾再帰。末尾再帰ルーチンは、再帰呼び出しがルーチンの最終処理として現れるルーチンの形式です。このようなルーチン呼び出しは単純にジャンプに縮小されます。
一部の実装では、再帰用に独自のスタックを実装しているため、システムがメモリ不足になるまで再帰を続行できます。
最も簡単な方法は、可能であればスタック サイズを増やすことです。それができない場合は、2 番目に良い方法は、明らかにスタック オーバーフローの原因となるものがあるかどうかを調べることです。ルーチンへの呼び出しの前後に何かを出力してみてください。これは、失敗したルーチンを見つけるのに役立ちます。
スタック オーバーフローは、通常、関数呼び出しの入れ子が深すぎる (特に、再帰を使用する場合、つまり自分自身を呼び出す関数の場合に簡単です) か、ヒープを使用する方が適切なスタックに大量のメモリを割り当てることによって呼び出されます。
あなたが言うように、いくつかのコードを表示する必要があります。:-)
スタック オーバーフロー エラーは通常、関数呼び出しのネストが深すぎる場合に発生します。これがどのように発生するかの例については、 Stack Overflow Code Golfスレッドを参照してください(ただし、その質問の場合、回答は意図的にスタック オーバーフローを引き起こします)。
StackOverflowError
OutOfMemoryError
ヒープと同じようにスタックにも適用されます。
無制限の再帰呼び出しにより、スタック領域が使い果たされます。
次の例では、次のものが生成されStackOverflowError
ます。
class StackOverflowDemo
{
public static void unboundedRecursiveCall() {
unboundedRecursiveCall();
}
public static void main(String[] args)
{
unboundedRecursiveCall();
}
}
StackOverflowError
未完了のメモリ内呼び出しの合計 (バイト単位) がスタック サイズ (バイト単位) を超えないように再帰呼び出しが制限されている場合は回避できます。
スタック オーバーフローの最も一般的な原因は、深すぎる再帰または無限再帰です。これが問題である場合は、Java 再帰に関するこのチュートリアルが問題の理解に役立つ可能性があります。
これは、単一リンクリストを逆にするための再帰的アルゴリズムの例です。ラップトップ(仕様4 GBメモリ、Intel Core i5 2.3 GHz CPU64ビットおよびWindows7)では、この関数は、10,000に近いサイズのリンクリストに対してStackOverflowエラーが発生します。
私のポイントは、常にシステムの規模を考慮して、再帰を慎重に使用する必要があるということです。
多くの場合、再帰は反復プログラムに変換でき、スケーリングが向上します。(同じアルゴリズムの1つの反復バージョンがページの下部に示されています。これは、9ミリ秒でサイズ100万の単一リンクリストを反転します。)
private static LinkedListNode doReverseRecursively(LinkedListNode x, LinkedListNode first){
LinkedListNode second = first.next;
first.next = x;
if(second != null){
return doReverseRecursively(first, second);
}else{
return first;
}
}
public static LinkedListNode reverseRecursively(LinkedListNode head){
return doReverseRecursively(null, head);
}
同じアルゴリズムの反復バージョン:
public static LinkedListNode reverseIteratively(LinkedListNode head){
return doReverseIteratively(null, head);
}
private static LinkedListNode doReverseIteratively(LinkedListNode x, LinkedListNode first) {
while (first != null) {
LinkedListNode second = first.next;
first.next = x;
x = first;
if (second == null) {
break;
} else {
first = second;
}
}
return first;
}
public static LinkedListNode reverseIteratively(LinkedListNode head){
return doReverseIteratively(null, head);
}
これが例です
public static void main(String[] args) {
System.out.println(add5(1));
}
public static int add5(int a) {
return add5(a) + 5;
}
基本的に、StackOverflowError は、何かを実行しようとしたときに発生し、おそらくそれ自体が呼び出され、無限に (または StackOverflowError が発生するまで) 続きます。
add5(a)
自分自身を呼び出してから、再び自分自身を呼び出します。