Blok Try-akhirnya mencegah StackOverflowError

331

Lihatlah dua metode berikut:

public static void foo() {
    try {
        foo();
    } finally {
        foo();
    }
}

public static void bar() {
    bar();
}

Menjalankan bar()dengan jelas menghasilkan StackOverflowError, tetapi menjalankan foo()tidak (program sepertinya berjalan tanpa batas). Mengapa demikian?

arshajii
sumber
17
Secara formal, program pada akhirnya akan berhenti karena kesalahan yang dilemparkan selama pemrosesan finallyklausa akan merambat ke tingkat berikutnya. Tapi jangan menahan nafas; jumlah langkah yang diambil akan sekitar 2 sampai (kedalaman tumpukan maksimum) dan melempar pengecualian juga tidak murah.
Donal Fellows
3
Akan "benar" untuk bar(), meskipun.
dan04
6
@ dan04: Java tidak melakukan TCO, IIRC untuk memastikan memiliki jejak tumpukan penuh, dan untuk sesuatu yang berkaitan dengan refleksi (mungkin juga ada hubungannya dengan jejak tumpukan).
ninjalj
4
Yang cukup menarik ketika saya mencoba ini di .Net (menggunakan Mono), program macet dengan kesalahan StackOverflow tanpa pernah menelepon akhirnya.
Kibbee
10
Ini tentang potongan kode terburuk yang pernah saya lihat :)
poitroae

Jawaban:

332

Itu tidak berjalan selamanya. Setiap stack overflow menyebabkan kode untuk pindah ke blok akhirnya. Masalahnya adalah itu akan memakan waktu yang sangat, sangat lama. Urutan waktu adalah O (2 ^ N) di mana N adalah kedalaman tumpukan maksimum.

Bayangkan kedalaman maksimum adalah 5

foo() calls
    foo() calls
       foo() calls
           foo() calls
              foo() which fails to call foo()
           finally calls
              foo() which fails to call foo()
       finally
           foo() calls
              foo() which fails to call foo()
           finally calls
              foo() which fails to call foo()
    finally calls
       foo() calls
           foo() calls
              foo() which fails to call foo()
           finally calls
              foo() which fails to call foo()
       finally
           foo() calls
              foo() which fails to call foo()
           finally calls
              foo() which fails to call foo()
finally calls
    foo() calls
       foo() calls
           foo() calls
              foo() which fails to call foo()
           finally calls
              foo() which fails to call foo()
       finally
           foo() calls
              foo() which fails to call foo()
           finally calls
              foo() which fails to call foo()
    finally calls
       foo() calls
           foo() calls
              foo() which fails to call foo()
           finally calls
              foo() which fails to call foo()
       finally
           foo() calls
              foo() which fails to call foo()
           finally calls
              foo() which fails to call foo()

Untuk mengerjakan setiap level ke dalam blok terakhir, perlu dua kali lebih panjang dari kedalaman tumpukan yang mencapai 10.000 atau lebih. Jika Anda dapat membuat 10.000.000 panggilan per detik, ini akan memakan waktu 10 ^ 3003 detik atau lebih lama dari usia alam semesta.

Peter Lawrey
sumber
4
Bagus, bahkan jika saya mencoba membuat tumpukan sekecil mungkin melalui -Xss, saya mendapatkan kedalaman [150 - 210], jadi 2 ^ n akhirnya menjadi angka digit [47-65]. Tidak akan menunggu selama itu, itu cukup dekat untuk saya.
ninjalj
64
@oldrinb Hanya untuk Anda, saya meningkatkan kedalaman ke 5.;)
Peter Lawrey
4
Jadi, pada akhirnya ketika fooakhirnya tidak berakhir, itu akan menghasilkan StackOverflowError?
arshajii
5
mengikuti matematika, ya. stack overflow terakhir dari akhirnya yang gagal stack overflow akan keluar dengan ... stack overflow = P. tidak bisa menolak.
WhozCraig
1
Jadi apakah ini benar-benar berarti bahkan yang mencoba menangkap kode juga harus berakhir dengan kesalahan stackoverflow ??
LPD
40

Ketika Anda mendapatkan pengecualian dari doa foo()di dalam try, Anda menelepon foo()dari finallydan mulai berulang lagi. Ketika itu menyebabkan pengecualian lain, Anda akan menelepon foo()dari bagian dalam yang lain finally(), dan seterusnya hampir tak terhingga .

ninjalj
sumber
5
Agaknya, StackOverflowError (SOE) dikirim ketika tidak ada lagi ruang di stack untuk memanggil metode baru. Bagaimana bisa foo()dipanggil dari akhirnya setelah BUMN?
assylias
4
@assylias: jika tidak ada cukup ruang, Anda akan kembali dari terbaru foo()doa, dan memohon foo()di finallyblok Anda saat foo()doa.
ninjalj
+1 ke ninjalj. Anda tidak akan menelepon foo dari mana saja setelah Anda tidak dapat menelepon foo karena kondisi overflow. ini termasuk dari blok akhirnya, yang mengapa ini akan, akhirnya (zaman semesta) berakhir.
WhozCraig
38

Coba jalankan kode berikut:

    try {
        throw new Exception("TEST!");
    } finally {
        System.out.println("Finally");
    }

Anda akan menemukan bahwa blok akhirnya dieksekusi sebelum melemparkan Exception ke tingkat di atasnya. (Keluaran:

Akhirnya

Pengecualian di utas "utama" java.lang.Exception: TEST! di test.main (test.java:6)

Ini masuk akal, karena akhirnya dipanggil tepat sebelum keluar dari metode. Ini berarti, bagaimanapun, bahwa setelah Anda mendapatkan yang pertama StackOverflowError, itu akan mencoba untuk membuangnya, tetapi akhirnya harus mengeksekusi terlebih dahulu, sehingga berjalan foo()lagi, yang mendapat tumpukan stack overflow, dan karena itu berjalan akhirnya lagi. Ini terus terjadi selamanya, sehingga pengecualian tidak pernah benar-benar dicetak.

Namun dalam metode bar Anda, segera setelah pengecualian terjadi, itu hanya dilemparkan langsung ke tingkat di atas, dan akan dicetak

Alex Coleman
sumber
2
Downvote. "Terus terjadi selamanya" salah. Lihat jawaban lain.
jcsahnwaldt mengatakan GoFundMonica
26

Dalam upaya memberikan bukti yang masuk akal bahwa AKAN ini pada akhirnya akan berakhir, saya menawarkan kode yang agak tidak berarti berikut ini. Catatan: Java BUKAN bahasa saya, oleh imajinasi yang paling jelas. Saya mengajukan hal ini hanya untuk mendukung jawaban Petrus, yang merupakan satu jawaban yang benar untuk pertanyaan itu.

Upaya ini untuk mensimulasikan kondisi dari apa yang terjadi ketika suatu pemanggilan TIDAK dapat terjadi karena itu akan menyebabkan stack overflow. Bagi saya, hal tersulit yang gagal dipahami orang adalah bahwa permohonan itu tidak terjadi ketika itu tidak dapat terjadi.

public class Main
{
    public static void main(String[] args)
    {
        try
        {   // invoke foo() with a simulated call depth
            Main.foo(1,5);
        }
        catch(Exception ex)
        {
            System.out.println(ex.toString());
        }
    }

    public static void foo(int n, int limit) throws Exception
    {
        try
        {   // simulate a depth limited call stack
            System.out.println(n + " - Try");
            if (n < limit)
                foo(n+1,limit);
            else
                throw new Exception("StackOverflow@try("+n+")");
        }
        finally
        {
            System.out.println(n + " - Finally");
            if (n < limit)
                foo(n+1,limit);
            else
                throw new Exception("StackOverflow@finally("+n+")");
        }
    }
}

Keluaran dari tumpukan kecil goo yang tidak berguna ini adalah sebagai berikut, dan pengecualian aktual yang ditangkap mungkin mengejutkan; Oh, dan 32 percobaan panggilan (2 ^ 5), yang sepenuhnya diharapkan:

1 - Try
2 - Try
3 - Try
4 - Try
5 - Try
5 - Finally
4 - Finally
5 - Try
5 - Finally
3 - Finally
4 - Try
5 - Try
5 - Finally
4 - Finally
5 - Try
5 - Finally
2 - Finally
3 - Try
4 - Try
5 - Try
5 - Finally
4 - Finally
5 - Try
5 - Finally
3 - Finally
4 - Try
5 - Try
5 - Finally
4 - Finally
5 - Try
5 - Finally
1 - Finally
2 - Try
3 - Try
4 - Try
5 - Try
5 - Finally
4 - Finally
5 - Try
5 - Finally
3 - Finally
4 - Try
5 - Try
5 - Finally
4 - Finally
5 - Try
5 - Finally
2 - Finally
3 - Try
4 - Try
5 - Try
5 - Finally
4 - Finally
5 - Try
5 - Finally
3 - Finally
4 - Try
5 - Try
5 - Finally
4 - Finally
5 - Try
5 - Finally
java.lang.Exception: StackOverflow@finally(5)
WhozCraig
sumber
23

Belajar melacak program Anda:

public static void foo(int x) {
    System.out.println("foo " + x);
    try {
        foo(x+1);
    } 
    finally {
        System.out.println("Finally " + x);
        foo(x+1);
    }
}

Ini adalah output yang saya lihat:

[...]
foo 3439
foo 3440
foo 3441
foo 3442
foo 3443
foo 3444
Finally 3443
foo 3444
Finally 3442
foo 3443
foo 3444
Finally 3443
foo 3444
Finally 3441
foo 3442
foo 3443
foo 3444
[...]

Seperti yang Anda lihat StackOverFlow dilemparkan pada beberapa layer di atas, jadi Anda bisa melakukan langkah rekursi tambahan hingga Anda menekan pengecualian lain, dan seterusnya. Ini adalah "loop" tanpa batas.

Karoly Horvath
sumber
11
itu sebenarnya bukan infinite loop, jika Anda cukup sabar akhirnya akan berakhir. Aku tidak akan menahan napas untuk itu.
Lie Ryan
4
Saya berpendapat bahwa itu tidak terbatas. Setiap kali mencapai kedalaman tumpukan maksimum, ia melempar pengecualian dan melepaskan tumpukan. Namun pada akhirnya ia memanggil Foo lagi menyebabkannya menggunakan kembali ruang stack yang baru saja pulih. Ini akan bolak-balik melempar pengecualian dan kemudian kembali Dow tumpukan sampai terjadi lagi. Selama-lamanya.
Kibbee
Selain itu, Anda ingin agar system.out.println yang pertama berada di pernyataan coba, jika tidak maka akan melepaskan loop lebih jauh dari yang seharusnya. mungkin menyebabkannya berhenti.
Kibbee
1
@ Kibbee Masalah dengan argumen Anda adalah bahwa ketika ia memanggil fookedua kalinya, di dalam finallyblok, ia tidak lagi dalam a try. Jadi sementara itu akan kembali ke stack dan membuat lebih banyak stack overflow sekali, yang kedua kali itu hanya akan menata kembali kesalahan yang dihasilkan oleh panggilan kedua foo, alih-alih memperdalam kembali.
amalloy
0

Program ini sepertinya berjalan selamanya; itu sebenarnya berakhir, tetapi secara eksponensial membutuhkan lebih banyak waktu lebih banyak ruang stack yang Anda miliki. Untuk membuktikan bahwa itu selesai, saya menulis sebuah program yang pertama menghabiskan sebagian besar ruang stack yang tersedia, dan kemudian memanggil foo, dan akhirnya menulis jejak apa yang terjadi:

foo 1
  foo 2
    foo 3
    Finally 3
  Finally 2
    foo 3
    Finally 3
Finally 1
  foo 2
    foo 3
    Finally 3
  Finally 2
    foo 3
    Finally 3
Exception in thread "main" java.lang.StackOverflowError
    at Main.foo(Main.java:39)
    at Main.foo(Main.java:45)
    at Main.foo(Main.java:45)
    at Main.foo(Main.java:45)
    at Main.consumeAlmostAllStack(Main.java:26)
    at Main.consumeAlmostAllStack(Main.java:21)
    at Main.consumeAlmostAllStack(Main.java:21)
    ...

Kode:

import java.util.Arrays;
import java.util.Collections;
public class Main {
  static int[] orderOfOperations = new int[2048];
  static int operationsCount = 0;
  static StackOverflowError fooKiller;
  static Error wontReachHere = new Error("Won't reach here");
  static RuntimeException done = new RuntimeException();
  public static void main(String[] args) {
    try {
      consumeAlmostAllStack();
    } catch (RuntimeException e) {
      if (e != done) throw wontReachHere;
      printResults();
      throw fooKiller;
    }
    throw wontReachHere;
  }
  public static int consumeAlmostAllStack() {
    try {
      int stackDepthRemaining = consumeAlmostAllStack();
      if (stackDepthRemaining < 9) {
        return stackDepthRemaining + 1;
      } else {
        try {
          foo(1);
          throw wontReachHere;
        } catch (StackOverflowError e) {
          fooKiller = e;
          throw done; //not enough stack space to construct a new exception
        }
      }
    } catch (StackOverflowError e) {
      return 0;
    }
  }
  public static void foo(int depth) {
    //System.out.println("foo " + depth); Not enough stack space to do this...
    orderOfOperations[operationsCount++] = depth;
    try {
      foo(depth + 1);
    } finally {
      //System.out.println("Finally " + depth);
      orderOfOperations[operationsCount++] = -depth;
      foo(depth + 1);
    }
    throw wontReachHere;
  }
  public static String indent(int depth) {
    return String.join("", Collections.nCopies(depth, "  "));
  }
  public static void printResults() {
    Arrays.stream(orderOfOperations, 0, operationsCount).forEach(depth -> {
      if (depth > 0) {
        System.out.println(indent(depth - 1) + "foo " + depth);
      } else {
        System.out.println(indent(-depth - 1) + "Finally " + -depth);
      }
    });
  }
}

Anda dapat mencobanya secara online! (Beberapa panggilan mungkin memanggil foolebih atau lebih sedikit daripada yang lain)

Vitruvius
sumber