web-dev-qa-db-ja.com

このメソッドはなぜ4を出力するのですか?

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または他のシステムの他の何か。

110
flrnb

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

しましょう

  • Xは合計スタックサイズ
  • Mは、メインに初めて入るときに使用されるスタックスペースです。
  • Rは、メインに入るたびに増加するスタックスペースです。
  • Pは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:仮定

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

次に、C = floor((X-M)/ 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より大きい場合に増分を開始し、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

41
John Tseng

これは、悪い再帰呼び出しの犠牲者です。 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の値が異なる場合があります。

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

20
Sajal Dutta

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

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

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

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

Java呼び出しを-Xss 4Mで呼び出すと、41が得られます。したがって、相関関係です。

13
Jatin
  1. mainは、再帰の深さRでスタックからオーバーフローするまで、それ自体で再帰します。
  2. 再帰深度R-1のc​​atchブロックが実行されます。
  3. 再帰深度R-1のc​​atchブロックはcnt++を評価します。
  4. 深さR-1のc​​atchブロックはprintlnを呼び出し、cntの古い値をスタックに配置します。 printlnは、内部で他のメソッドを呼び出し、ローカル変数や物を使用します。これらすべてのプロセスにはスタックスペースが必要です。
  5. スタックは既に制限を超えており、printlnの呼び出し/実行にはスタックスペースが必要であるため、新しいスタックオーバーフローは、深度Rではなく深度R-1でトリガーされます。
  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ブロックが実行されずにプログラムが完了します。
6
Craig Gidney

表示される数は、_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呼び出しの後に行われるため、例外によってバイパスされます。

6
Kazaag

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

まず、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を出力します。

1
Jay

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

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

たとえば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を使用しました。

0
me_digvijay