この方法で4が出力されるのはなぜですか?


111

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つのセグメントでは不十分です)。Hotspot VMが適用する最適化(メソッドのインライン化)が原因である場合、別のVMで結果が異なるかどうか疑問に思います。

編集

出力は非常にJVM固有であると思われるため、
Java(TM)SEランタイム環境(ビルド1.6.0_41-b02)
Java HotSpot(TM)64ビットサーバーVM(ビルド20.14-b01、混合モード)を使用して結果4を取得します


この質問がJavaスタックの理解とは異なると思う理由:

私の質問はなぜcnt> 0があるのか​​(明らかにSystem.out.println()スタックサイズが必要でありStackOverflowError、何かが印刷される前に別のものをスローするため)ではなく、特定の値4、それぞれ0、3、8、55または他の何かがあるのはなぜですかシステム。


4
私の地域では、期待どおりに「0」が表示されます。
Reddy 2013

2
これは、多くのアーキテクチャ関連のものを処理する可能性があります。jdkバージョンを使用して出力を投稿することをお
勧めします。Formeの

3
私が得ている5638のJava 1.7.0_10と

8
@Elistは、基礎となるアーキテクチャを含むトリックを実行しているときは同じ出力にはなりません;)
m0skit0

3
@flrnb中括弧を並べるのに使用するスタイルです。これにより、条件と関数の開始と終了の場所がわかりやすくなります。必要に応じて変更できますが、私の意見では、この方法の方が読みやすくなっています。
syb0rg 2013

回答:


41

他の人はなぜcnt> 0なのかを説明するのに良い仕事をしたと思いますが、なぜcnt = 4なのか、そしてcntがさまざまな設定間で大きく変動するのかについて十分な詳細はありません。ここでその空白を埋めようとします。

しましょう

  • Xは合計スタックサイズ
  • Mは、メインに初めて入るときに使用されるスタックスペースです。
  • Rは、メインに入るたびに増加するスタックスペースです。
  • Pは実行に必要なスタックスペース System.out.println

最初にメインに入るとき、残ったスペースはXMです。再帰呼び出しごとに、より多くのメモリを消費します。したがって、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:仮定する

  • X = 100
  • M = 1
  • R = 2
  • P = 1

次に、C = floor((XM)/ R)= 49、およびcnt = ceiling((P-(X-M-C * R))/ R)= 0です。

例2:と仮定します

  • X = 100
  • M = 1
  • R = 5
  • P = 12

次に、C = 19、およびcnt = 2。

例3:と仮定します

  • X = 101
  • M = 1
  • R = 5
  • P = 12

次に、C = 20、cnt = 3。

例4:と仮定します

  • X = 101
  • M = 2
  • R = 5
  • P = 12

次に、C = 19、およびcnt = 2。

したがって、システム(M、R、およびP)とスタックサイズ(X)の両方がcntに影響することがわかります。

補足として、catch開始に必要なスペースの量は問題ではありません。のための十分なスペースがない限りcatch、cntは増加しないため、外部効果はありません。

編集

私が言ったことを取り戻しcatchます。それは役割を果たす。開始するにはTのスペースが必要だとします。cntは、残りのスペースがTより大きい場合に増分を開始し、残りのスペースがprintlnT + Pより大きい場合に実行されます。これにより、計算に追加のステップが追加され、すでに泥だらけの分析がさらに悪化します。

編集

ようやく、理論を裏付けるためにいくつかの実験を実行する時間を見つけました。残念ながら、理論は実験と一致していないようです。実際に起こることは非常に異なります。

実験のセットアップ:デフォルトのjavaおよびdefault-jdkを備えたUbuntu 12.04サーバー。Xssは1バイトで70,000から始まり、460,000に増加します。

:結果がでご利用いただけますhttps://www.google.com/fusiontables/DataSource?docid=1xkJhd4s8biLghe6gZbcfUs3vT5MpS_OnscjWDbM 私はすべての繰り返しのデータポイントが削除され、別のバージョンを作成しました。つまり、前回とは異なる点のみが表示されます。これにより、異常が見やすくなります。https://www.google.com/fusiontables/DataSource?docid=1XG_SRzrrNasepwZoNHqEAKuZlHiAm9vbEdwfsUA


良い要約をありがとうございました。結局のところ、M、R、Pに影響するものは何ですか(XはVMオプション-Xssで設定できるため)?
flrnb 2013

@flrnb M、R、およびPはシステム固有です。それらを簡単に変更することはできません。一部のリリースでも同様に異なると思います。
John Tseng 2013

それでは、なぜXss(別名X)を変更すると異なる結果が得られるのですか?M、R、Pが同じままであることを前提として、Xを100から10000に変更しても、式に従ってcntに影響を与えるべきではありませんか、それとも間違っていますか?
flrnb 2013

@flrnb Xだけでは、これらの変数の離散的な性質のためにcntが変更されます。例2と3はXのみが異なりますが、cntは異なります。
John Tseng 2013

1
@JohnTseng私もあなたの答えを今までで最も理解しやすく完全であると考えています-とにかく、私はスタックがスローされた瞬間に実際にどのように見えるか、StackOverflowErrorそしてこれが出力にどのように影響するかに本当に興味があります。ヒープ上のスタックフレームへの参照のみが含まれている場合(Jayが示唆したとおり)、特定のシステムの出力は非常に予測可能です。
flrnb 2013

20

これは、悪い再帰呼び出しの犠牲者です。なぜcntの値が変化するのか不思議に思っているのは、スタックサイズがプラットフォームに依存しているためです。Windows上のJava SE 6のデフォルトのスタックサイズは、32ビットVMでは320k、64ビットVMでは1024kです。詳しくはこちらをご覧ください

さまざまなスタックサイズを使用して実行でき、スタックがオーバーフローする前にcntのさまざまな値が表示されます。

java -Xss1024k RandomNumberGenerator

値が1より大きい場合でも、cntの値が複数回印刷されることはありません。これは、printステートメントがエラーをスローしているため、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) {   

        }        
    }        
}

更新:

これはもっと注目されているので、物事をより明確にする別の例を見てみましょう-

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という名前の別のメソッドを作成し、catchブロックからprintlnステートメントを削除したため、印刷中に別のエラーセットがスローされなくなりました。これは期待どおりに機能します。System.out.println(cnt);を入れてみることができます。上記のcnt ++の後のステートメントとコンパイル。その後、複数回実行します。プラットフォームによっては、cntの値が異なる場合があります

コードのミステリーは空想的ではないため、これがエラーを検出しない理由です。


13

動作はスタックサイズに依存します(これはを使用して手動で設定できますXss。スタックサイズはアーキテクチャ固有です。JDK7 ソースコードから

// Windowsのデフォルトのスタックサイズは実行可能ファイルによって決まります(java.exe
のデフォルト値は320K / 1MB [32ビット/ 64ビット]です)。Windowsのバージョンによっては、
// ThreadStackSizeをゼロ以外に変更すると、メモリ使用量に大きな影響を与える可能性があります。
// os_windows.cppのコメントを参照してください。

したがって、StackOverflowErrorがスローされると、エラーはcatchブロックでキャッチされます。次に、println()例外を再度スローする別のスタックコールを示します。これは繰り返されます。

何回繰り返す?-まあそれは、JVMがスタックオーバーフローではなくなったと判断するタイミングに依存します。そして、それは各関数呼び出しのスタックサイズ(見つけるのが難しい)とに依存しますXss。上記のように、デフォルトの合計サイズと各関数呼び出しのサイズ(メモリページサイズなどに依存)はプラットフォーム固有です。したがって、異なる動作。

呼び出しjavaと呼び出しを-Xss 4M私に与えます41。したがって、相関関係です。


4
スタックサイズが結果に影響を与える理由はわかりません。cntの値を出力しようとすると、すでにそれを超えているためです。したがって、唯一の違いは、「各関数呼び出しのスタックサイズ」から生じる可能性があります。また、同じJVMバージョンを実行している2台のマシン間でこれが異なる理由を理解できません。
flrnb 2013

正確な動作は、JVMソースからのみ取得できます。しかし、理由はこれである可能性があります。でもcatchブロックであり、スタック上のメモリを占有することを忘れないでください。各メソッド呼び出しがどれだけのメモリを消費するかはわかりません。スタックがクリアされると、さらに1つのブロックが追加されますcatch。これは動作の可能性があります。これは単なる推測です。
ジャティン2013

また、スタックサイズは2つの異なるマシンで異なる場合があります。スタックサイズは多くのOSベースの要素、つまりメモリページサイズなどに依存します
Jatin

6

表示される数は、System.out.println呼び出しがStackoverflow例外をスローした回数だと思います。

おそらく、実装とそのprintln中で行われるスタッキング呼び出しの数に依存します。

例として:

main()コール・トリガーStackoverflow呼び出しiにおける例外。mainのi-1呼び出しが例外をキャッチしprintln、2番目をトリガーする呼び出しStackoverflowcnt1にインクリメントします。メインキャッチのi-2呼び出しは、例外とを呼び出しますprintln。ではprintln方法第三の例外をトリガと呼ばれています。 cnt2にインクリメントします。これはprintln、必要な呼び出しをすべて実行し、最後にの値を表示できるまで続きますcnt

これは、の実際の実装に依存しprintlnます。

JDK7の場合、循環呼び出しを検出して例外を先にスローするか、スタックリソースを保持し、制限に達する前に例外をスローして、修正ロジックのためのスペースを確保するか、println実装が呼び出しを行わないか、++操作が実行された後printlnしたがって、呼び出しは例外によってバイパスされます。


それが「System.out.printlnがコールスタックに3つのセグメントを必要とするためだと思った」という意味ですが、正確にこの数値である理由に戸惑いました。 (仮想)マシン
flrnb 2013

部分的には同意しますが、同意しないのは、「printlnの実際の実装に依存する」という文にあります。これは、実装ではなく、各jvmのスタックサイズに関係しています。
ジャティン2013

6
  1. main再帰の深さでスタックからオーバーフローするまで、再帰しRます。
  2. 再帰の深さでcatchブロックR-1が実行されます。
  3. 再帰深度でのcatchブロックは、をR-1評価しcnt++ます。
  4. 深度のcatchブロックはをR-1呼び出しprintlncntの古い値をスタックに配置します。println内部で他のメソッドを呼び出し、ローカル変数や物を使用します。これらすべてのプロセスにはスタックスペースが必要です。
  5. スタックはすでに制限を超えており、呼び出し/実行にprintlnはスタックスペースが必要であるため、新しいスタックオーバーフローはdepth R-1ではなくdepthでトリガーされRます。
  6. ステップ2から5が再び発生しますが、再帰の深さで発生しR-2ます。
  7. ステップ2から5が再び発生しますが、再帰の深さで発生しR-3ます。
  8. ステップ2から5が再び発生しますが、再帰の深さで発生しR-4ます。
  9. 手順2〜4は再び発生しますが、再帰の深さR-5です。
  10. たまたま、println完了するのに十分なスタックスペースがある場合があります(これは実装の詳細であり、異なる場合があることに注意してください)。
  11. cnt深さでポストインクリメントされたR-1R-2R-3R-4、そして最後にR-5。5番目のポストインクリメントは4を返しました。これは印刷されたものです。
  12. main深さで正常に完了しR-5、より多くのcatchブロックせずにスタック全体の巻き戻さを実行し、プログラムが完了しています。

1

しばらく掘り下げてみると、答えが出たとは言えませんが、もうかなり近いと思います。

まず、いつStackOverflowErrorスローされるかを知る必要があります。実際、Javaスレッドのスタックには、メソッドの呼び出しと再開に必要なすべてのデータを含むフレームが格納されています。Java 6のJava言語仕様によると、メソッドを呼び出すと、

そのようなアクティブ化フレームを作成するために利用できる十分なメモリがない場合、StackOverflowErrorがスローされます。

次に、「そのようなアクティブ化フレームを作成するのに十分なメモリがない」ということを明確にする必要がありますJAVA 6のJava仮想マシン仕様によると、

フレームにはヒープが割り当てられる場合があります。

したがって、フレームが作成されるとき、スタックフレームを作成するための十分なヒープスペースと、フレームがヒープに割り当てられている場合に新しいスタックフレームを指す新しい参照を格納するための十分なスタックスペースが必要です。

さて、質問に戻りましょう。上記から、メソッドが実行されると、同じ量のスタックスペースが消費されるだけであることがわかります。また、呼び出しにはSystem.out.println(5月の)5レベルのメソッド呼び出しが必要なので、5つのフレームを作成する必要があります。次に、StackOverflowErrorがスローされると、5つのフレームの参照を格納するのに十分なスタックスペースを確保するために、5回戻る必要があります。したがって、4が出力されます。なぜ5ではないのですか?あなたが使うのでcnt++。それを++cntに変更すると、5が表示されます。

そして、スタックのサイズが高いレベルになると、50になることもあります。そのため、利用可能なヒープ領域の量を考慮する必要があるためです。スタックのサイズが大きすぎると、スタックの前にヒープ領域が不足する可能性があります。(多分)のスタックフレームの実際のサイズSystem.out.printlnはの約51倍ですmain。したがって、51倍戻り、50を印刷します。


私の最初の考えは、メソッド呼び出しのレベルも数えることでした(そして、あなたが正しい、私はインクリメントcntを投稿するという事実に注意を払いませんでした)。およびVMの実装?
flrnb 2013

@flrnb異なるプラットフォームがスタックフレームのサイズに影響を与える可能性があり、jreの異なるバージョンがSystem.out.printメソッドの実装またはメソッド実行戦略に影響を与えるためです。上記のように、VMの実装は、スタックフレームが実際に格納される場所にも影響します。
2013

0

これは質問に対する正確な回答ではありませんが、出くわした元の質問と問題の理解方法に何かを追加したかっただけです。

元の問題では、可能な場合に例外がキャッチされます。

たとえばjdk 1.7では、最初に発生した場所で捕捉されます。

しかし、jdkの以前のバージョンでは、例外が最初に発生した場所でキャッチされていないように見えるため、4、50などです。

次のようにtry catchブロックを削除すると

public static void main( String[] args ){
    System.out.println(cnt++);
    main(args);
}

次にcnt、(jdk 1.7で)スローされた例外のantのすべての値が表示されます。

cmdはすべての出力とスローされた例外を表示しないため、出力を確認するためにnetbeansを使用しました。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.