การลองในที่สุดบล็อกจะป้องกัน StackOverflowError


331

ดูสองวิธีต่อไปนี้:

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

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

การรันbar()อย่างชัดเจนส่งผลให้ a StackOverflowErrorแต่การรันfoo()ไม่ได้ (โปรแกรมดูเหมือนว่าจะรันไปเรื่อย ๆ ) ทำไมถึงเป็นอย่างนั้น?


17
อย่างเป็นทางการในที่สุดโปรแกรมจะหยุดทำงานเนื่องจากข้อผิดพลาดเกิดขึ้นในระหว่างการประมวลผลส่วนfinallyคำสั่งจะเผยแพร่ไปยังระดับถัดไปขึ้นไป แต่อย่ากลั้นลมหายใจของคุณ จำนวนขั้นตอนที่ดำเนินการจะอยู่ที่ประมาณ 2 ถึง (ความลึกสูงสุดของสแต็ก) และการโยนข้อยกเว้นไม่ได้ราคาถูกเช่นกัน
Donal Fellows

3
มันจะเป็น "ถูกต้อง" สำหรับbar()แม้ว่า
dan04

6
@ dan04: Java ไม่ได้ทำ TCO, IIRC เพื่อให้แน่ใจว่ามีการติดตามสแต็กเต็มและสำหรับบางสิ่งที่เกี่ยวข้องกับการสะท้อนภาพ (อาจต้องเกี่ยวข้องกับการติดตามสแต็กด้วย)
ninjalj

4
น่าสนใจพอเมื่อฉันลองใช้. Net (โดยใช้ Mono) โปรแกรมขัดข้องด้วยข้อผิดพลาด StackOverflow โดยที่ไม่เคยโทรหาในที่สุด
Kibbee

10
นี้เป็นเรื่องเกี่ยวกับชิ้นส่วนที่เลวร้ายที่สุดของรหัสที่ฉันเคยเห็น :)
poitroae

คำตอบ:


332

มันไม่ทำงานตลอดไป การล้นสแต็กแต่ละครั้งทำให้รหัสนั้นย้ายไปยังบล็อกสุดท้าย ปัญหาคือว่ามันจะใช้เวลานานมากจริงๆ ลำดับเวลาคือ O (2 ^ N) โดยที่ N คือความลึกสูงสุดของสแต็ก

ลองนึกภาพความลึกสูงสุดคือ 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()

ในการทำงานแต่ละระดับในบล็อกสุดท้ายใช้เวลาสองครั้งตราบเท่าที่ความลึกของสแต็กอาจเท่ากับ 10,000 หรือมากกว่า หากคุณสามารถโทรได้ 10,000,000 ครั้งต่อวินาทีสิ่งนี้จะใช้เวลา 10 ^ 3003 วินาทีหรือนานกว่าอายุของจักรวาล


4
ดีแม้ว่าฉันจะพยายามทำให้สแต็กมีขนาดเล็กที่สุดเท่าที่จะเป็นไปได้-Xssแต่ฉันได้ความลึกของ [150 - 210] ดังนั้น 2 ^ n จึงกลายเป็นหมายเลข [47 - 65] ไม่ต้องรอนานขนาดนั้นนั่นก็เพียงพอแล้วสำหรับฉัน
ninjalj

64
@oldrinb เพียงสำหรับคุณฉันเพิ่มขึ้นความลึกถึง 5;)
ปีเตอร์ Lawrey

4
ดังนั้นในตอนท้ายของวันเมื่อfooสิ้นสุดในที่สุดก็จะส่งผลให้StackOverflowError?
arshajii

5
ติดตามคณิตศาสตร์ yup stack overflow สุดท้ายจากสุดท้ายที่ล้มเหลวในการ stack overflow สุดท้ายจะออกจาก ... Stack overflow = P ไม่สามารถต้านทาน
WhozCraig

1
ดังนั้นสิ่งนี้จริง ๆ แล้วหมายความว่าแม้ลองรหัสที่จับต้องควรสิ้นสุดในข้อผิดพลาด stackoverflow
LPD

40

เมื่อคุณได้รับข้อยกเว้นจากการเรียกใช้foo()ภายในtryคุณจะโทรfoo()จากfinallyและเริ่มการเรียกซ้ำอีกครั้ง เมื่อที่ทำให้เกิดข้อยกเว้นอื่นคุณจะเรียกfoo()จากด้านอื่นfinally()และอื่น ๆ เกือบจะไม่มีที่สิ้นสุด


5
สมมุติว่า StackOverflowError (SOE) ถูกส่งเมื่อไม่มีที่ว่างบนสแต็กเพื่อเรียกใช้เมธอดใหม่ วิธีการสามารถfoo()ถูกเรียกจากในที่สุดหลังจากรัฐวิสาหกิจหรือไม่?
assylias

4
@assylias: ถ้ามีพื้นที่ไม่เพียงพอคุณจะกลับมาจากล่าสุดfoo()ภาวนาและเรียกfoo()ในfinallyบล็อกปัจจุบันของคุณfoo()ภาวนา
ninjalj

+1 ถึง ninjalj คุณจะไม่เรียก foo จากที่ใด ๆเมื่อคุณไม่สามารถโทร foo ได้เนื่องจากสภาพล้น สิ่งนี้รวมถึงจากการบล็อกในที่สุดซึ่งเป็นเหตุผลว่าทำไมความประสงค์นี้ในที่สุด (อายุของจักรวาล) จึงสิ้นสุดลง
WhozCraig

38

ลองเรียกใช้รหัสต่อไปนี้:

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

คุณจะพบว่าในที่สุดบล็อกจะดำเนินการก่อนที่จะส่งข้อยกเว้นขึ้นไปถึงระดับที่สูงกว่า (ขาออก

ในที่สุด

ข้อยกเว้นในเธรด "main" java.lang.Exception: TEST! ที่ test.main (test.java:6)

สิ่งนี้สมเหตุสมผลในที่สุดเมื่อถูกเรียกใช้ก่อนที่จะออกจากวิธีการ ซึ่งหมายความว่าเมื่อคุณได้รับก่อนStackOverflowErrorมันจะพยายามโยน แต่ในที่สุดจะต้องดำเนินการครั้งแรกดังนั้นมันจะทำงานfoo()อีกครั้งซึ่งจะได้รับสแต็คล้นอื่น ๆ และในที่สุดก็วิ่งอีกครั้ง สิ่งนี้จะเกิดขึ้นตลอดไปดังนั้นข้อยกเว้นจะไม่ถูกพิมพ์จริง

ในวิธีการแถบของคุณอย่างไรก็ตามทันทีที่มีข้อยกเว้นเกิดขึ้นมันจะถูกส่งตรงไปยังระดับที่สูงกว่าและจะถูกพิมพ์


2
downvote "เกิดขึ้นตลอดไป" ผิด ดูคำตอบอื่น ๆ
jcsahnwaldt พูดว่า GoFundMonica

26

ในความพยายามที่จะให้หลักฐานที่สมเหตุสมผลว่าในที่สุด WILL นี้จะยุติฉันขอเสนอรหัสที่ไม่มีความหมายต่อไปนี้ หมายเหตุ: Java ไม่ใช่ภาษาของฉันด้วยจินตนาการที่สดใสที่สุด ผมเสนอนี้ขึ้นเพื่อสนับสนุนคำตอบของปีเตอร์ซึ่งเป็นตอบที่ถูกต้องกับคำถาม

ความพยายามที่จะจำลองเงื่อนไขของสิ่งที่เกิดขึ้นเมื่อการเรียกไม่สามารถเกิดขึ้นได้เพราะมันจะแนะนำกองล้น สำหรับฉันแล้วสิ่งที่ยากที่สุดที่ผู้คนไม่สามารถเข้าใจได้ในสิ่งที่การเรียกนั้นไม่เกิดขึ้นเมื่อมันไม่สามารถเกิดขึ้นได้

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

การส่งออกของกองหนาไม่มีจุดหมายของสารที่หนานี้มีดังต่อไปนี้และข้อยกเว้นที่เกิดขึ้นจริงอาจเป็นเรื่องแปลกใจ โอ้และลองโทร 32 ครั้ง (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

เรียนรู้วิธีติดตามโปรแกรมของคุณ:

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

นี่คือผลลัพธ์ที่ฉันเห็น:

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

ในขณะที่คุณเห็น StackOverFlow ถูกโยนในบางเลเยอร์ด้านบนดังนั้นคุณสามารถทำขั้นตอนการเรียกซ้ำเพิ่มเติมจนกว่าคุณจะพบข้อยกเว้นอื่นและอื่น ๆ นี่คือ "ลูป" ที่ไม่มีที่สิ้นสุด


11
มันไม่ได้วนรอบไม่สิ้นสุดจริง ๆ ถ้าคุณอดทนพอมันจะสิ้นสุดลงในที่สุด ฉันจะไม่กลั้นลมหายใจ
โกหก Ryan

4
ฉันจะวางตัวว่ามันไม่มีที่สิ้นสุด ทุกครั้งที่ถึงความลึกสูงสุดของสแต็กมันจะส่งข้อยกเว้นและคลายสแต็ก อย่างไรก็ตามในที่สุดมันก็เรียกฟูอีกครั้งทำให้มันกลับมาใช้พื้นที่สแต็กที่เพิ่งกู้คืนมาได้อีกครั้ง มันจะกลับไปโยนข้อยกเว้นแล้วกลับไป Dow กองซ้อนจนกว่ามันจะเกิดขึ้นอีกครั้ง ตลอดไป
Kibbee

นอกจากนี้คุณจะต้องการให้ระบบแรกนั้นพิมพ์ออกมาอยู่ในคำสั่งลองมิฉะนั้นจะเป็นการผ่อนคลายการวนซ้ำยิ่งกว่าที่ควร อาจทำให้มันหยุด
Kibbee

1
@Kibbee ปัญหากับข้อโต้แย้งของคุณคือว่าเมื่อมันเรียกfooเป็นครั้งที่สองในบล็อกมันไม่ได้อยู่ในfinally tryดังนั้นในขณะที่มันจะย้อนกลับไปที่สแต็กและสร้างสแต็คโอเวอร์โฟลว์มากขึ้นหนึ่งครั้งครั้งที่สองมันจะรื้อฟื้นข้อผิดพลาดที่เกิดขึ้นจากการเรียกครั้งที่สองfooแทนการเพิ่มความลึกอีกครั้ง
amalloy

0

ดูเหมือนว่าโปรแกรมจะทำงานตลอดไป จริง ๆ แล้วมันจะยุติลง แต่ใช้เวลามากขึ้นในการเพิ่มพื้นที่สแต็กที่คุณมี เพื่อพิสูจน์ว่ามันเสร็จสิ้นฉันได้เขียนโปรแกรมที่ทำให้พื้นที่สแต็กส่วนใหญ่ว่างก่อนแล้วจึงโทรfooและในที่สุดก็เขียนร่องรอยของสิ่งที่เกิดขึ้น:

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

รหัส:

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

คุณสามารถลองออนไลน์ได้! (บางวิ่งอาจเรียกfooเวลามากกว่าหรือน้อยกว่าอื่น ๆ )

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.