Try-finally bloğu StackOverflowError'ı önler


331

Aşağıdaki iki yönteme bir göz atın:

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

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

Açık bir bar()şekilde çalışmak bir sonuç verir StackOverflowError, ancak çalışma foo()yapmaz (program süresiz çalışıyor). Neden?


17
Resmi olarak, program sonunda duracaktır, çünkü finallymaddenin işlenmesi sırasında atılan hatalar bir sonraki seviyeye yükselecektir. Ama nefesini tutma; atılan adım sayısı yaklaşık 2 ila (maksimum yığın derinliği) olacaktır ve istisnaların atılması da tam olarak ucuz değildir.
Donal Fellows

3
Yine de "doğru" olurdu bar().
dan04

6
@ dan04: Java, tam yığın izlerine sahip olmak ve yansıma ile ilgili bir şey için (muhtemelen yığın izleriyle de ilgili) TCO, IIRC yapmaz.
ninjalj

4
İlginçtir ki .Net üzerinde (Mono kullanarak) bunu denediğimde, program nihayet aramadan bir StackOverflow hatasıyla çöktü.
Kibbee

10
Bu gördüğüm en kötü kod parçası hakkında :)
poitroae

Yanıtlar:


332

Sonsuza kadar koşmaz. Her yığın taşması, kodun nihayet bloğa taşınmasına neden olur. Sorun şu ki, gerçekten çok uzun zaman alacak. Zaman sırası O (2 ^ N) 'dir, burada N maksimum yığın derinliğidir.

Maksimum derinliğin 5 olduğunu hayal edin

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()

Her seviyeyi son blokta çalışmak için, yığın derinliğinin 10.000 veya daha fazla olabileceği iki kat daha uzun sürün. Saniyede 10.000.000 arama yapabiliyorsanız, bu, evrenin yaşından 10 ^ 3003 saniye veya daha uzun sürecektir.


4
Güzel, yığını mümkün olduğunca küçültmeye -Xssçalışsam bile, [150 - 210] derinliği elde ediyorum, bu yüzden 2 ^ n sonunda [47 - 65] basamaklı bir sayı olur. O kadar beklemeyeceğim, bu benim için sonsuzluğa yakın.
ninjalj

64
@oldrinb Sadece senin için derinliği 5'e
yükselttim

4
fooNihayet sona erdiği günün sonunda, bir StackOverflowError?
arshajii

5
matematiği takiben, evet. nihayet taşmayı başaramayan son nihayetten son yığın taşması ... yığın taşması = P ile çıkar. karşı koyamadım.
WhozCraig

1
Bu aslında bu bile catch kodu da stackoverflow hata sonunda gerekir anlamına gelir anlamına gelir mi?
LPD

40

Eğer çağırma gelen bir istisna olsun foo()içeride try, aramak foo()gelen finallyve yine recursing başlayın. Bu başka bir istisnaya neden olduğunda, başka foo()bir içten çağrı yaparsınız finally()ve böylece neredeyse reklam sonsuzluğu .


5
Muhtemelen, yığınta yeni yöntemler çağırmak için yer kalmadığında bir StackOverflowError (SOE) gönderilir. foo()Sonunda bir KİT'den sonra nasıl çağrılabilir ?
assylias

4
@assylias: yeterli alan yok ise, en son gelen dönecektir foo()çağırma ve çağırmak foo()içinde finallygeçerli blok foo()çağırma.
ninjalj

+ 1 ninjalj. Sen den foo çağrı olmaz her yerde nedeniyle taşma durumuna foo arayamazsın kez. bu nihayet bloktan içerir, bu yüzden bu sonunda (evrenin yaşı) sona erecektir.
WhozCraig

38

Aşağıdaki kodu çalıştırmayı deneyin:

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

Sonunda bloğun, üstündeki seviyeye kadar bir İstisna atmadan önce yürütüldüğünü göreceksiniz. (Çıktı:

En sonunda

"Main" iş parçacığında özel durum java.lang.Exception: TEST! test.main adresinde (test.java:6)

Nihayet yöntemden çıkmadan hemen önce çağrıldığı için bu mantıklıdır. Bununla birlikte, bunu ilk aldığınızda StackOverflowError, atmaya çalışacağı, ancak nihayet önce çalıştırılması gerektiği anlamına gelir, bu yüzden foo()tekrar çalışır , bu da başka bir yığın taşması alır ve bu nedenle sonunda tekrar çalışır. Bu sonsuza kadar olmaya devam eder, bu nedenle istisna asla yazdırılmaz.

Ancak çubuk yönteminizde, istisna oluşur oluşmaz, doğrudan yukarıdaki seviyeye kadar atılır ve yazdırılır


2
Downvote. "sonsuza dek olmaya devam ediyor" yanlış. Diğer cevaplara bakın.
jcsahnwaldt diyor GoFundMonica

26

Sonunda bu FİDET'in sona ereceğine dair makul kanıt sağlamak için, aşağıdaki oldukça anlamsız kodu sunuyoruz. Not: Java, en canlı hayal gücünün herhangi bir uzantısıyla benim dilim değil. Ben ise Peter cevabı desteklemek için, sadece bu kadar buyruğu soruya doğru cevap.

Bu, bir istifleme gerçekleşemediğinde ne olacağının koşullarını simüle etmeye çalışır çünkü bir yığın taşması oluşturur. Zor şey insanlar zaman çağırmak olmaz ki kavramak başarısız gibi geliyor bana olamaz olur.

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+")");
        }
    }
}

Bu küçük anlamsız goo yığınının çıktısı şudur ve yakalanan gerçek istisna bir sürpriz olabilir; Oh ve tamamen beklenen 32 deneme çağrısı (2 ^ 5):

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)

23

Programınızı izlemeyi öğrenin:

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

Gördüğüm çıktı bu:

[...]
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
[...]

Gördüğünüz gibi StackOverFlow yukarıdaki bazı katmanlara atılır, böylece başka bir istisnayı vurana kadar ek özyineleme adımları yapabilirsiniz. Bu sonsuz bir "döngü" dir.


11
aslında sonsuz döngü değil, eğer yeterince sabırlıysanız sonunda sona erecek. Buna rağmen nefesimi tutmayacağım.
Yalan Ryan

4
Sonsuz olduğunu söyleyebilirim. Maksimum yığın derinliğine her ulaştığında, bir istisna atar ve yığını çözer. Ancak nihayetinde Foo'yu tekrar çağırır ve topladığı yığın alanını tekrar kullanmasına neden olur. İstisnalar atıp ileri geri gidecek ve daha sonra tekrar yığına kadar yığını Dow'a gidecektir. Sonsuza dek.
Kibbee

Ayrıca, ilk system.out.println dosyasının try deyiminde olmasını isteyeceksiniz, aksi takdirde döngüyü olması gerekenden daha fazla çözecektir. muhtemelen durmasına neden oluyor.
Kibbee

1
@Kibbee Argümanınızla ilgili sorun foo, ikinci kez çağırdığında , finallyblokta artık bir try. Böylece yığına geri döner ve bir kez daha fazla yığın taşması yaratırken, ikinci kez fooyeniden derinleştirme yerine ikinci çağrının ürettiği hatayı yeniden atar .
amalloy

0

Program sadece sonsuza dek sürecek gibi görünüyor; aslında sonlanır, ancak daha fazla yığın alanınız katlanarak daha fazla zaman alır. Bittiğini kanıtlamak için, önce kullanılabilir yığın alanının çoğunu tüketen bir program yazdım, sonra aradı foove son olarak ne olduğuna dair bir iz yazdı:

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)
    ...

Kod:

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);
      }
    });
  }
}

Şunları yapabilirsiniz çevrimiçi deneyin! (Bazı koşular foodiğerlerinden daha fazla veya daha az kez çağrılabilir)

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.