JavaでStackOverflowError
を処理する方法は?
「ハンドル」の意味がわかりません。
あなたは確かにそのエラーをキャッチすることができます:
public class Example {
public static void endless() {
endless();
}
public static void main(String args[]) {
try {
endless();
} catch(StackOverflowError t) {
// more general: catch(Error t)
// anything: catch(Throwable t)
System.out.println("Caught "+t);
t.printStackTrace();
}
System.out.println("After the error...");
}
}
しかし、何をしているのか正確に知らない限り、それはおそらく悪い考えです。
おそらく無限の再帰が起こっています。
つまり自分自身を何度も呼び出すメソッド
public void sillyMethod()
{
sillyMethod();
}
これを処理する1つは、コードを修正して、再帰が永遠に続くのではなく終了するようにすることです。
Raymond Chen の投稿をご覧ください スタックオーバーフローをデバッグするときは、繰り返し再帰部分に注目したい 。抽出物:
これが既知の問題であるかどうかを確認するために欠陥追跡データベースを探し回る場合、スタックの上位の関数を検索しても、興味深いものは何も見つかりません。これは、スタックオーバーフローが再帰のランダムなポイントで発生する傾向があるためです。各スタックオーバーフローは、同じスタックオーバーフローであっても、表面的には他のスタックオーバーフローとは異なって見えます。
歌FrèreJacquesを歌っていると仮定します。ただし、各詩を前の詩より数音高い歌います。結局、あなたはあなたの歌声の範囲のトップに到達します、そしてそれが起こる正確な場所はあなたのボーカルの限界がメロディーに対してどこに並ぶかによります。メロディーでは、最初の3つのノートはそれぞれ新しい「レコード高」であり(つまり、これまでに歌われた他のどのノートよりも高い)、3番目の小節の3つのノートに新しいレコード高が表示され、最後のレコードが5番目の小節の2番目のノートで高い。
メロディーがプログラムのスタック使用量を表す場合、スタックオーバーフローがプログラムの実行中の5つの場所のいずれかで発生する可能性があります。言い換えれば、同じ基本的な暴走再帰(音楽的にメロディのより高い表現によって音楽的に表される)は、5つの異なる方法で現れることができます。このアナロジーでの「再帰」はかなり速く、ループが繰り返される前のわずか8小節でした。実際には、ループはかなり長くなる可能性があり、スタックオーバーフローが発生する可能性のある数十の潜在的なポイントにつながります。
スタックオーバーフローに直面した場合は、スタックの先頭を無視する必要があります。これは、ボーカルの範囲を超えた特定のノートに焦点を合わせているためです。同じ根本原因ですべてのスタックオーバーフローに共通するのは、メロディ全体を見つけたい場合です。
「-Xss」オプションがJVMでサポートされているかどうかを確認したい場合があります。その場合は、値を512k(32ビットのWindowsおよびUnixではデフォルトは256k)に設定して、それが機能するかどうかを確認することをお勧めします(StackOverflowExceptionまで長く座る以外は)。これはスレッドごとの設定なので、多くのスレッドを実行している場合は、ヒープ設定を増やすこともできます。
正解はすでに与えられたものです。おそらくa)コードにバグがあり、通常、診断と修正が非常に簡単な無限再帰が発生するか、またはb)非常に深い再帰につながる可能性があるコード(たとえば、不均衡なバイナリツリーを再帰的にたどる)のいずれかです。後者の状況では、スタックに情報を割り当てないように(つまり、再帰しないように)コードを変更する必要がありますが、代わりにヒープに情報を割り当てます。
たとえば、不均衡なツリートラバーサルの場合、再検討する必要があるノードをスタックデータ構造に格納できます。順序トラバーサルの場合は、左のブランチをループダウンして、アクセスしたときに各ノードを押しながら、リーフに到達するまで処理します。その後、処理し、スタックのトップからノードをポップして処理し、ループを再起動します。右側の子(ループ変数を右側のノードに設定するだけで)。これは、スタック上にあったすべてのものをStackデータ構造のヒープに移動することにより、一定量のスタックを使用します。ヒープは通常、スタックよりもはるかに豊富です。
通常は非常に悪い考えですが、メモリの使用が非常に制限されている場合に必要なものとして、ポインタの反転を使用できます。この手法では、トラバースする構造にスタックをエンコードします。トラバースするリンクを再利用することにより、追加のメモリなしで、または大幅に少ないメモリでこれを実行できます。上記の例を使用すると、ループ時にノードをプッシュするのではなく、直接の親を覚えておく必要があります。反復ごとに、現在の親に移動したリンクを設定し、次に現在の親を離れるノードに設定します。葉に到達したら、それを処理してから、親に行き、難問を抱えています。左側のブランチを修正してこのノードを処理し、右側のブランチを続行するか、右側のブランチを修正して親に移動するかはわかりません。そのため、反復するときに追加の情報を割り当てる必要があります。通常、この手法の低レベルの実現では、そのビットはポインタ自体に格納され、追加のメモリがなく、全体的に一定のメモリになります。これはJavaのオプションではありませんが、他のものに使用されるフィールドでこのビットを削除することが可能かもしれません。最悪の場合でも、これは必要なメモリ量の少なくとも32または64分の1の削減です。もちろん、このアルゴリズムは非常に簡単に間違ってしまい、完全に混乱する結果になり、同時実行性で完全な大混乱を引き起こします。したがって、メモリの割り当てが受け入れられない場合を除いて、メンテナンスの悪夢に見舞われることはほとんどありません。典型的な例は、このようなアルゴリズムが一般的なガベージコレクターです。
しかし、私が本当に話したかったのは、StackOverflowErrorを処理したい場合です。つまり、JVMで末尾呼び出しの排除を提供します。 1つのアプローチは、末尾呼び出しを実行する代わりに、nullaryプロシージャオブジェクトを返すトランポリンスタイルを使用することです。値を返すだけの場合は、それを返します。 [注:これは、関数がAまたはBを返すと言う何らかの手段を必要とします。Javaでは、おそらくこれを行う最も簡単な方法は、1つの型を通常に返し、もう1つを例外としてスローすることです。]次に、メソッドを呼び出すたびに、値を取得するまで、nullaryプロシージャ(nullaryプロシージャまたは値を返す)を呼び出すwhileループを実行する必要があります。無限ループは、プロシージャオブジェクトを返すプロシージャオブジェクトを常に強制するwhileループになります。トランポリンスタイルの利点は、すべての末尾呼び出しを適切に削除した実装で使用するよりも定数係数だけ多くのスタックを使用することであり、通常のJava末尾以外の呼び出しにはスタックを使用し、変換は単純で、コードは(退屈な)一定の要因によってのみ増加します。欠点は、すべてのメソッド呼び出しでオブジェクトを割り当て(すぐにガベージになります)、これらのオブジェクトを消費すると、末尾呼び出しごとにいくつかの間接呼び出しが必要になることです。
理想的なことは、最初はこれらのnullプロシージャやその他のものを決して割り当てないことです。これは、テールコールの除去によって実現されることです。 Javaが提供するものを操作すると、コードを通常どおりに実行し、スタックが不足したときにのみこれらのnullプロシージャを作成することができます。今では、これらの不要なフレームを割り当てますが、そのため、ヒープではなくスタック上でそれらを一括で割り当て解除します。また、呼び出しは通常の直接Java呼び出しです。この変換を説明する最も簡単な方法は、最初にすべての複数呼び出しステートメントを書き直すことです。 2つの呼び出しステートメントを持つメソッドへのメソッド、すなわちfgh(){f(); g(); h();}はfgh(){f(); gh();}およびgh(){g();になります。簡単にするために、すべてのメソッドが末尾呼び出しで終了すると想定します。これは、メソッドの残りの部分を別のメソッドにパッケージ化するだけで調整できますが、実際には、これらを直接処理する必要があります。 。これらの変換の後、3つのケースがあります。メソッドに呼び出しがない場合は、何もする必要がありません。または、1つの(末尾)呼び出しがある場合は、同じメソッドのtry-catchブロックにラップします。 thの最後の呼び出しのためにe 2つのコールケース。最後に、非末尾呼び出しと末尾呼び出しの2つの呼び出しがある場合があります。その場合、例で示した次の変換を適用します(C#のラムダ表記を使用して、匿名の内部クラスで簡単に置き換えることができます)。
// top-level handler
Action tlh(Action act) {
return () => {
while(true) {
try { act(); break; } catch(Bounce e) { tlh(() => e.run())(); }
}
}
}
gh() {
try { g(); } catch(Bounce e) {
throw new Bounce(tlh(() => {
e.run();
try { h(); } catch(StackOverflowError e) {
throw new Bounce(tlh(() => h());
}
});
}
try { h(); } catch(StackOverflowError e) {
throw new Bounce(tlh(() => h()));
}
}
ここでの主な利点は、例外がスローされない場合です。これは、いくつかの追加の例外ハンドラーがインストールされているところから始めたのと同じコードです。テールコール(h()コール)はバウンス例外を処理しないので、その例外はそれらを通過して、それらの(不要な)フレームをスタックから巻き戻します。非テールコールは、例外をバウンスし、残りのコードを追加してそれらを再スローします。これにより、スタックが最上位まで巻き戻され、末尾呼び出しフレームが削除されますが、nullaryプロシージャの末尾以外の呼び出しフレームが記憶されます。トップレベルのバウンス例外では、すべての非テールコールフレームを再作成します。この時点で、スタックがすぐに不足すると、StackOverflowErrorハンドラーを再インストールしないため、キャッチされなくなります。本当にスタックが不足しているため、必要です。もう少し進んだ場合は、必要に応じて新しいStackOverflowErrorがインストールされます。さらに、進行してもスタックが不足している場合は、巻き戻しのメリットはありません。既に巻き戻したフレームなので、新しいトップレベルのハンドラーをインストールして、スタックはそれらまで巻き戻されるだけです。
このアプローチの最大の問題は、通常のJavaメソッドを呼び出す必要があり、実行時にスタックスペースがわずかに不足する可能性があるため、開始するのに十分なスペースがあり、終了しない可能性があることです。途中で再開することはできません。これには少なくとも2つの解決策があります。1つ目は、そのようなすべての作業を、独自のスタックを持つ別のスレッドに送信することです。これは非常に効果的で非常に簡単で、 (必要な場合を除いて)同時実行性を導入します。別のオプションは、通常のJavaメソッドを呼び出す前に、意図的にスタックを巻き戻すことです。それらの直前にStackOverflowErrorをスローするだけです。それでも、再開するときにスタックスペースを確保すると、最初から混乱していました。
同様のことが、ジャストインタイムで継続を行うためにも行うことができます。残念ながら、この変換はJavaで手動で行うには耐えられず、おそらくC#やScalaなどの言語の境界線です。したがって、このような変換は、JVMをターゲットとする言語によって行われる傾向があり、人によって行われることはありません。
StackOverflowError
を取得するほとんどの可能性は、再帰関数で[long/infinite]再帰を使用することです。
スタック可能なデータオブジェクトを使用するようにアプリケーションの設計を変更することで、関数の再帰を回避できます。再帰コードを反復コードブロックに変換するコーディングパターンがあります。以下の回答を見てください:
したがって、独自のデータスタックを使用して、劣性関数呼び出しのJavaによってメモリのスタックを回避します。
私はあなたができないと思います-またはそれは少なくともあなたが使うjvmに依存します。スタックオーバーフローは、ローカル変数を格納してアドレスを返す余地がないことを意味します。 jvmがなんらかの形でコンパイルする場合、jvmにもstackoverflowがあり、それを処理またはキャッチすることができません。 jvmを終了する必要があります。
そのような動作を可能にするjvmを作成する方法があるかもしれませんが、それは遅いでしょう。
私はjvmで動作をテストしていませんが、.netではstackoverflowを処理できません。キャッチしようとしても役に立たない。 Javaと.netは同じ概念に依存しているため(jitを備えた仮想マシン))Javaは同じように動作します。stackoverflow-exceptionの存在は.NETは、プログラムがそれをキャッチできるようにするvmがあるかもしれないと示唆していますが、通常はそうではありません。
Java.lang.Error javadoc:
ErrorはThrowableのサブクラスですこれは、合理的なアプリケーションがキャッチしようとしてはならない重大な問題を示します。このようなエラーのほとんどは異常な状態です。 ThreadDeathエラーは、「通常の」条件ですが、ほとんどのアプリケーションがエラーをキャッチしようとしてはならないため、Errorのサブクラスでもあります。これらのエラーは発生してはならない異常な状態であるため、メソッドのスロー句で、メソッドの実行中にスローされてもキャッチされない可能性のあるErrorのサブクラスをメソッドで宣言する必要はありません。
だから、しないでください。コードのロジックのどこに問題があるかを見つけてください。この例外は、無限再帰のために非常に頻繁に発生します。
シンプル、
StackOverflowErrorが生成するスタックトレースを見て、コードのどこで発生するかを確認し、それを使用して、コードを書き換えて自分自身を再帰的に呼び出さないようにする方法(エラーの原因となる可能性があります)を確認します。再び起こります。
StackOverflowErrorsは、try ... catch句で処理する必要があるものではありませんが、ユーザーが修正する必要のあるコードのロジックの基本的な欠陥を示しています。
StackOverFlowエラー-Javaでメソッドを作成するときに、あるサイズのメモリ割り当てがスタックメモリに割り当てられます。無限ループ内にメソッドを作成すると、メモリ割り当てが作成されます'n'回。メモリ割り当てが超過すると、エラーが発生します。このエラーはStackOverFlowエラーと呼ばれます。
このエラーを回避する場合は、実装時のスタックメモリサイズを最初から検討してください。
場合によっては、stackoverflowerrorをキャッチできないことがあります。あなたがしようとするたびに、あなたは新しいものに遭遇します。なぜならJava vm。 Andrew Bullockが言った のような再帰的なコードブロックを見つけるのは良いことだからです。
スタックトレースは、問題の性質を示す必要があります。スタックトレースを読み取ると、明らかなループが発生するはずです。
バグでない場合は、再帰が深くなりすぎてスタックオーバーフローが発生する前に、カウンターまたはその他のメカニズムを追加して再帰を停止する必要があります。
この例としては、DOMモデルでネストされたXMLを再帰呼び出しで処理していて、XMLが非常に深くネストされているため、ネストされた呼び出しでスタックオーバーフローが発生する場合があります(可能性は低いですが、可能です)。ただし、スタックオーバーフローを発生させるには、これをかなり深くネストする必要があります。
このスレッドで多くの人が述べたように、これの一般的な原因は、終了しない再帰的なメソッド呼び出しです。可能な場合はスタックオーバーフローを回避します。これをテストで行う場合は、ほとんどの場合これを深刻なバグと見なす必要があります。場合によっては、Javaでスレッドスタックサイズを大きくして、特定の状況(ローカルスタックストレージで管理されている大きなデータセット、長い再帰呼び出し)を処理できるようにする)を構成できますが、これによりメモリ全体が増加しますVMで使用可能なスレッド数の問題につながる可能性のあるフットプリント。通常、この例外が発生した場合、スレッドとこのスレッドへのローカルデータはトーストと見なされ、使用されない(つまり、疑わしく破損している可能性があります)。