StackOverflowErrorをキャッチしようとすると、次のメソッドが思い浮かびました。
_class RandomNumberGenerator {
static int cnt = 0;
public static void main(String[] args) {
try {
main(args);
} catch (StackOverflowError ignore) {
System.out.println(cnt++);
}
}
}
_
今私の質問:
この方法で「4」が出力されるのはなぜですか?
多分それはSystem.out.println()
がコールスタック上に3つのセグメントを必要とするからだと思いましたが、私は3の数がどこから来るのかわかりません。 System.out.println()
のソースコード(およびバイトコード)を見ると、通常、メソッドの呼び出しは3つよりはるかに多くなります(したがって、呼び出しスタックの3つのセグメントでは不十分です)。ホットスポットVMが適用される最適化が原因である場合(メソッドのインライン化))、別のVMでは結果が異なるかどうか疑問に思います。
編集する:
出力は非常にJVM固有であると思われるため、次のコマンドを使用して結果を取得します4
Java(TM)SEランタイム環境(ビルド1.6.0_41-b02)
Java HotSpot(TM)64ビットサーバーVM(ビルド20.14-b01、混合モード)
この質問が Javaスタックを理解する とは異なると思う理由についての説明:
私の質問はなぜcnt> 0があるのか(明らかにSystem.out.println()
がスタックサイズを必要とし、何かが出力される前に別のStackOverflowError
をスローするため)ではありませんが、なぜそれが4の特定の値を持つのかそれぞれ0、3、8、55または他のシステムの他の何か。
他の人はなぜcnt> 0であるかを説明するのに良い仕事をしたと思いますが、なぜcnt = 4であるのか、またcntがさまざまな設定間で大きく変動するのかについて十分な詳細はありません。ここでその空白を埋めようとします。
しましょう
System.out.println
の実行に必要なスタックスペース最初にメインに入るとき、残ったスペースはX-Mです。再帰呼び出しごとに、より多くのメモリを消費します。したがって、1回の再帰呼び出し(元の呼び出しより1つ多い)の場合、メモリ使用量はM + Rです。Cの再帰呼び出しが成功した後にStackOverflowErrorがスローされたとします。つまり、M + C * R <= XおよびM + C *(R + 1)>X。最初のStackOverflowErrorの時点で、X-M-C * Rのメモリが残っています。
System.out.prinln
を実行できるようにするには、スタックにP分のスペースが必要です。 X-M-C * R> = Pとなると、0が出力されます。 Pがより多くのスペースを必要とする場合、スタックからフレームを削除し、cnt ++を犠牲にしてRメモリを獲得します。
println
が最終的に実行可能になると、X-M-(C-cnt)* R> = Pとなります。したがって、特定のシステムでPが大きい場合、cntは大きくなります。
これをいくつかの例で見てみましょう。
例1:仮定
次に、C = floor((X-M)/ R)= 49、およびcnt = ceiling((P-(X-M-C * R))/ R)= 0です。
例2:と仮定します
次に、C = 19、およびcnt = 2。
例3:と仮定します
次に、C = 20、cnt = 3。
例4:と仮定します
次に、C = 19、およびcnt = 2。
したがって、システム(M、R、およびP)とスタックサイズ(X)の両方がcntに影響することがわかります。
補足として、catch
を開始するために必要なスペースは問題ではありません。 catch
のための十分なスペースがない限り、cntは増加しないため、外部への影響はありません。
[〜#〜]編集[〜#〜]
私がcatch
について言ったことを取り戻します。それは役割を果たす。開始するにはTのスペースが必要だとします。 cntは、残りのスペースがTより大きい場合に増分を開始し、println
は残りのスペースがT + Pより大きい場合に実行されます。これにより、計算に追加のステップが追加され、すでに濁った分析がさらに悪化します。
[〜#〜]編集[〜#〜]
ようやく、理論を裏付けるためにいくつかの実験を実行する時間を見つけました。残念ながら、理論は実験と一致していないようです。実際に起こることは非常に異なります。
実験のセットアップ:デフォルトJavaおよびdefault-jdkのUbuntu 12.04サーバー。Xssは70,000から始まり、1バイトずつ460,000に増加します。
結果は次の場所にあります: https://www.google.com/fusiontables/DataSource?docid=1xkJhd4s8biLghe6gZbcfUs3vT5MpS_OnscjWDbM すべての繰り返されるデータポイントが削除される別のバージョンを作成しました。つまり、前回と異なる点のみを表示しています。これにより、異常が見やすくなります。 https://www.google.com/fusiontables/DataSource?docid=1XG_SRzrrNasepwZoNHqEAKuZlHiAm9vbEdwfsUA
これは、悪い再帰呼び出しの犠牲者です。 cntの値が変化する理由を不思議に思っているのは、スタックサイズがプラットフォームに依存しているためです。 Java SE 6のデフォルトのスタックサイズは、32ビットで320kですVMおよび64ビットVMで1024k。詳細は- ここ 。
異なるスタックサイズを使用して実行でき、スタックがオーバーフローする前にcntの異なる値が表示されます
Java -Xss1024k RandomNumberGenerator
cntの値が1より大きい場合でも、複数回印刷されることはありません。印刷ステートメントもエラーをスローしているため、デバッグして確認できます。 Eclipseまたは他のIDE。
必要に応じて、コードを次のように変更して、ステートメントごとの実行をデバッグできます。
static int cnt = 0;
public static void main(String[] args) {
try {
main(args);
} catch (Throwable ignore) {
cnt++;
try {
System.out.println(cnt);
} catch (Throwable t) {
}
}
}
UPDATE:
これがもっと注目を集めているので、物事をより明確にする別の例を見てみましょう-
static int cnt = 0;
public static void overflow(){
try {
overflow();
} catch (Throwable t) {
cnt++;
}
}
public static void main(String[] args) {
overflow();
System.out.println(cnt);
}
overflowという名前の別のメソッドを作成して不適切な再帰を実行し、printlnステートメントをcatchブロックから削除して、開始しないようにしました印刷しようとすると、別のエラーセットがスローされます。これは期待どおりに機能します。上記のcnt ++の後にSystem.out.println(cnt);ステートメントを置いてコンパイルしてみてください。その後、複数回実行します。プラットフォームによっては、cntの値が異なる場合があります。
コードのミステリーは空想的ではないため、これがエラーを検出しない理由です。
動作はスタックサイズに依存します(Xss
を使用して手動で設定できます。スタックサイズはアーキテクチャ固有です。JDK7以降 ソースコード :
// Windowsのデフォルトのスタックサイズは、実行可能ファイル(Java.exe
//のデフォルト値は320K/1MB [32ビット/ 64ビット])。 Windowsのバージョンに応じて、変更
// ThreadStackSizeをゼロ以外にすると、メモリ使用量に大きな影響を与える可能性があります。
// os_windows.cppのコメントを参照してください。
したがって、StackOverflowError
がスローされると、エラーはcatchブロックでキャッチされます。ここでprintln()
は、再度例外をスローする別のスタックコールです。これは繰り返されます。
何回繰り返しますか?-まあ、それはJVMがスタックオーバーフローではなくなったと判断するタイミングに依存します。そして、それは各関数呼び出しのスタックサイズ(見つけるのが難しい)とXss
に依存します。上記のように、デフォルトの合計サイズと各関数呼び出しのサイズ(メモリページサイズなどに依存)はプラットフォーム固有です。したがって、異なる動作。
Java
呼び出しを-Xss 4M
で呼び出すと、41
が得られます。したがって、相関関係です。
main
は、再帰の深さR
でスタックからオーバーフローするまで、それ自体で再帰します。R-1
のcatchブロックが実行されます。R-1
のcatchブロックはcnt++
を評価します。R-1
のcatchブロックはprintln
を呼び出し、cnt
の古い値をスタックに配置します。 println
は、内部で他のメソッドを呼び出し、ローカル変数や物を使用します。これらすべてのプロセスにはスタックスペースが必要です。println
の呼び出し/実行にはスタックスペースが必要であるため、新しいスタックオーバーフローは、深度R
ではなく深度R-1
でトリガーされます。R-2
です。R-3
です。R-4
です。R-5
です。println
を完了するのに十分なスタックスペースがたまたまある場合があります(これは実装の詳細であり、異なる場合があることに注意してください)。cnt
は、深さR-1
、R-2
、R-3
、R-4
、最後にR-5
でポストインクリメントされました。 5番目のポストインクリメントは4を返しました。これは印刷されたものです。main
が深さR-5
で正常に完了すると、スタック全体が巻き戻され、catchブロックが実行されずにプログラムが完了します。表示される数は、_System.out.println
_呼び出しがStackoverflow
例外をスローした回数だと思います。
おそらく、println
の実装と、その中で行われるスタッキング呼び出しの数によって異なります。
例として:
main()
呼び出しは、呼び出しiでStackoverflow
例外をトリガーします。 mainのi-1呼び出しは例外をキャッチし、2番目のprintln
をトリガーするStackoverflow
を呼び出します。 cnt
は1に増加します。メインキャッチのi-2呼び出しは例外になり、println
を呼び出します。 println
では、3番目の例外をトリガーするメソッドが呼び出されます。 cnt
は2に増加します。これは、println
が必要なすべての呼び出しを行い、最後にcnt
の値を表示できるようになるまで続きます。
これは、println
の実際の実装に依存します。
JDK7の場合、循環呼び出しを検出して例外を先にスローするか、スタックリソースを保持し、制限に達する前に例外をスローして、修復ロジックのための余地を与えるか、println
実装が呼び出しを行わないか、 ++操作はprintln
呼び出しの後に行われるため、例外によってバイパスされます。
しばらく掘り下げてみると、答えが出たとは言えませんが、もうかなり近いと思います。
まず、StackOverflowError
がスローされるタイミングを知る必要があります。実際、a Javaスレッドのスタックは、メソッドを呼び出して再開するために必要なすべてのデータを含むフレームを格納します。 Java言語仕様Java 6 、メソッドを呼び出すとき、
そのようなアクティブ化フレームを作成するために利用できる十分なメモリがない場合、StackOverflowErrorがスローされます。
次に、「このようなアクティベーションフレームを作成するのに十分なメモリがない」とは何かを明確にする必要があります。 Java仮想マシン仕様Java 6 によると、
フレームにはヒープが割り当てられる場合があります。
したがって、フレームが作成されるとき、スタックフレームを作成するための十分なヒープスペースと、フレームがヒープに割り当てられている場合に新しいスタックフレームを指す新しい参照を格納するための十分なスタックスペースが必要です。
さて、質問に戻りましょう。上記から、メソッドが実行されると、同じ量のスタックスペースが消費されるだけであることがわかります。また、System.out.println
を呼び出すには5レベルのメソッドを呼び出す必要があるため、5つのフレームを作成する必要があります。次に、StackOverflowError
がスローされると、5つのフレームの参照を格納するのに十分なスタックスペースを確保するために、5回戻る必要があります。したがって、4が出力されます。なぜ5ではないのですか? cnt++
を使用しているためです。 ++cnt
に変更すると、5になります。
そして、スタックのサイズが高レベルになると、50になることもあります。これは、利用可能なヒープ領域の量を考慮に入れる必要があるためです。スタックのサイズが大きすぎると、スタックの前にヒープ領域が不足する可能性があります。 (おそらく)System.out.println
のスタックフレームの実際のサイズはmain
の約51倍であるため、51倍戻り、50を出力します。
これは質問に対する正確な回答ではありませんが、出くわした元の質問と、問題の理解方法に何かを追加したかっただけです。
元の問題では、可能な場合に例外がキャッチされます。
たとえばjdk 1.7では、最初に発生した場所で捕捉されます。
しかし、jdkの以前のバージョンでは、例外が最初に発生した場所でキャッチされていないように見えるため、4、50などです。
次のようにtry catchブロックを削除すると
public static void main( String[] args ){
System.out.println(cnt++);
main(args);
}
次に、cnt
とスローされた例外(jdk 1.7)のすべての値が表示されます。
Cmdはすべての出力とスローされた例外を表示しないため、出力を確認するためにnetbeansを使用しました。