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」を出力するのはなぜですか?

コール スタックに 3 つのセグメントが必要だからかもしれないと思ったSystem.out.println()のですが、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

7 に答える 7

41

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

させて

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

最初に main に入ると、残っているスペースは XM です。各再帰呼び出しは R 個のメモリを消費します。したがって、再帰呼び出しが 1 回 (元の呼び出しよりも 1 回多い) の場合、メモリ使用量は M + R になります。 C の再帰呼び出しが成功した後、つまり、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 より大きい場合にインクリメントを開始し、残りの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

于 2013-07-24T13:58:17.480 に答える
19

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

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

Java -Xss1024k 乱数発生器

値が 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の値が異なる場合があります。

これが、コードのミステリーはファンタジーではないため、通常、エラーを検出しない理由です。

于 2013-07-24T08:32:52.113 に答える
13

動作は、スタック サイズ (を使用して手動で設定できますXss。スタック サイズはアーキテクチャ固有です。JDK 7ソース コードから:

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

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

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

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

于 2013-07-24T11:57:00.673 に答える
6
  1. main再帰の深さ でスタックがオーバーフローするまで、それ自体を再帰しますR
  2. 再帰の深さで catch ブロックR-1が実行されます。
  3. 再帰の深さでの catch ブロックは をR-1評価しcnt++ます。
  4. 深さの catch ブロックは をR-1呼び出し、の古い値をスタックprintlnに置きます。内部で他のメソッドを呼び出し、ローカル変数などを使用します。これらすべてのプロセスには、スタック領域が必要です。cntprintln
  5. スタックは既に制限を超えており、呼び出し/実行printlnにはスタック領域が必要であるため、新しいスタック オーバーフローが depthR-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. cntR-1深さ、R-2R-3R-4そして最後に でポストインクリメントされましたR-5。5 番目のポストインクリメントは 4 を返しました。これが出力されたものです。
  12. maindepth で正常に完了した場合R-5、スタック全体がアンワインドされ、それ以上 catch ブロックが実行されず、プログラムが完了します。
于 2013-07-24T16:59:45.817 に答える
6

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

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

例として:

この呼び出しは、呼び出し i で例外をmain()トリガーします。Stackoverflowmain の i-1 呼び出しは例外をキャッチしprintln、2 番目の呼び出しをトリガーしStackoverflowます。 cntインクリメントを 1 に取得します。main catch の i-2 呼び出しは、例外と call になりprintlnました。メソッドではprintln、3 番目の例外のトリガーが呼び出されます。 これは、必要なすべての呼び出しを行い、最終的に の値を表示できるようになるcntまで続きます。printlncnt

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

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

于 2013-07-24T11:06:02.753 に答える