อะไรคือStackOverflowError
สาเหตุอะไรและฉันควรจัดการกับพวกเขาอย่างไร
new Object() {{getClass().newInstance();}};
ในบริบทแบบสแตติก (เช่นmain
วิธีการ) ไม่ทำงานจากบริบทอินสแตนซ์ (พ่นเท่านั้นInstantiationException
)
อะไรคือStackOverflowError
สาเหตุอะไรและฉันควรจัดการกับพวกเขาอย่างไร
new Object() {{getClass().newInstance();}};
ในบริบทแบบสแตติก (เช่นmain
วิธีการ) ไม่ทำงานจากบริบทอินสแตนซ์ (พ่นเท่านั้นInstantiationException
)
คำตอบ:
พารามิเตอร์และตัวแปรโลคัลถูกจัดสรรไว้บนสแต็ก (ที่มีชนิดการอ้างอิงอ็อบเจ็กต์จะอยู่บนฮีปและตัวแปรในการอ้างอิงสแต็กที่อ็อบเจ็กต์บนฮีป) โดยทั่วไปสแต็กจะอยู่ที่ปลายด้านบนของพื้นที่ที่อยู่ของคุณและเมื่อมันถูกใช้งานมันจะมุ่งหน้าไปยังด้านล่างของพื้นที่ที่อยู่ (เช่นไปยังศูนย์)
กระบวนการของคุณยังมีฮีปซึ่งอยู่ที่ส่วนล่างสุดของกระบวนการของคุณ ในขณะที่คุณจัดสรรหน่วยความจำฮีปนี้สามารถขยายไปยังส่วนท้ายของพื้นที่ที่อยู่ของคุณ อย่างที่คุณเห็นมีความเป็นไปได้ที่ฮีปจะ"ชนกัน"กับกองซ้อน (เหมือนแผ่นเปลือกโลก !!!)
สาเหตุที่พบบ่อยสำหรับกองล้นเป็นโทร recursive ที่ไม่ดี โดยทั่วไปสิ่งนี้เกิดขึ้นเมื่อฟังก์ชั่นการเรียกซ้ำของคุณไม่มีเงื่อนไขการเลิกจ้างที่ถูกต้องดังนั้นจึงเป็นการเรียกตัวเองตลอดไป หรือเมื่อเงื่อนไขการยกเลิกเป็นไปอย่างปกติอาจเกิดจากการเรียกซ้ำแบบเรียกซ้ำมากเกินไปก่อนที่จะทำให้สำเร็จ
อย่างไรก็ตามด้วยการเขียนโปรแกรม GUI คุณสามารถสร้างการสอบถามซ้ำทางอ้อมการเรียกซ้ำทางอ้อมตัวอย่างเช่นแอปของคุณอาจจัดการกับข้อความสีและในขณะประมวลผลพวกเขาอาจเรียกใช้ฟังก์ชั่นที่ทำให้ระบบส่งข้อความสีอื่น ที่นี่คุณไม่ได้เรียกตัวเองอย่างชัดเจน แต่ OS / VM ได้ทำเพื่อคุณแล้ว
ในการจัดการกับพวกเขาคุณจะต้องตรวจสอบรหัสของคุณ หากคุณมีฟังก์ชั่นที่เรียกตนเองว่าตรวจสอบว่าคุณมีเงื่อนไขที่ยกเลิก หากคุณมีให้ตรวจสอบว่าเมื่อเรียกใช้ฟังก์ชันที่คุณมีการแก้ไขอย่างน้อยหนึ่งข้อโต้แย้งมิฉะนั้นจะไม่มีการเปลี่ยนแปลงที่มองเห็นได้สำหรับฟังก์ชั่นที่เรียกซ้ำและเงื่อนไขการยกเลิกนั้นไม่มีประโยชน์ นอกจากนี้โปรดทราบว่าพื้นที่สแต็กของคุณสามารถมีหน่วยความจำไม่เพียงพอก่อนที่จะถึงเงื่อนไขการยกเลิกที่ถูกต้องดังนั้นโปรดตรวจสอบให้แน่ใจว่าวิธีการของคุณสามารถจัดการกับค่าอินพุตที่ต้องการการโทรซ้ำ
หากคุณไม่มีฟังก์ชั่นวนซ้ำอย่างเห็นได้ชัดให้ตรวจสอบเพื่อดูว่าคุณกำลังเรียกฟังก์ชั่นห้องสมุดใด ๆ ที่ทางอ้อมจะทำให้เกิดการเรียกใช้ฟังก์ชันของคุณ (เช่นกรณีโดยนัยด้านบน)
เพื่ออธิบายสิ่งนี้ก่อนอื่นให้เราเข้าใจว่าตัวแปรโลคัลและอ็อบเจ็กต์ถูกเก็บไว้อย่างไร
ตัวแปรโลคัลถูกเก็บในสแต็ก :
หากคุณดูรูปคุณควรเข้าใจว่าสิ่งต่าง ๆ ทำงานอย่างไร
เมื่อเรียกใช้ฟังก์ชันโดยแอ็พพลิเคชัน Java เฟรมสแต็กจะถูกจัดสรรบนสแตกการเรียก กรอบสแต็กมีพารามิเตอร์ของวิธีการเรียกใช้พารามิเตอร์ท้องถิ่นและที่อยู่ผู้ส่งของวิธีการ ที่อยู่ผู้ส่งคืนแสดงถึงจุดดำเนินการที่การดำเนินการของโปรแกรมจะดำเนินต่อไปหลังจากวิธีที่เรียกใช้คืนค่ากลับมา หากไม่มีที่ว่างสำหรับสแต็กเฟรมใหม่StackOverflowError
จะถูกโยนโดย Java Virtual Machine (JVM)
กรณีที่พบบ่อยที่สุดที่อาจทำให้สแต็กของแอ็พพลิเคชัน Java หมดลงคือการเรียกซ้ำ ในการเรียกซ้ำเมธอดจะเรียกใช้ตัวเองระหว่างการเรียกใช้งาน recursion ถือเป็นที่มีประสิทธิภาพวัตถุประสงค์ทั่วไปเทคนิคการเขียนโปรแกรม StackOverflowError
แต่ต้องใช้ด้วยความระมัดระวังเพื่อหลีกเลี่ยง
ตัวอย่างของการขว้าง StackOverflowError
แสดงอยู่ด้านล่าง:
StackOverflowErrorExample.java:
public class StackOverflowErrorExample {
public static void recursivePrint(int num) {
System.out.println("Number: " + num);
if (num == 0)
return;
else
recursivePrint(++num);
}
public static void main(String[] args) {
StackOverflowErrorExample.recursivePrint(1);
}
}
ในตัวอย่างนี้เรากำหนดวิธีการเรียกซ้ำที่เรียกว่า recursivePrint
ว่าพิมพ์จำนวนเต็มแล้วเรียกตัวเองด้วยจำนวนเต็มต่อเนื่องเป็นอาร์กิวเมนต์ การสอบถามซ้ำจะสิ้นสุดลงจนกว่าเราจะผ่าน0
เป็นพารามิเตอร์ อย่างไรก็ตามในตัวอย่างของเราเราส่งผ่านพารามิเตอร์จาก 1 และผู้ติดตามเพิ่มขึ้นดังนั้นการสอบถามซ้ำจะไม่สิ้นสุด
ตัวอย่างการดำเนินการโดยใช้ -Xss1M
แฟล็กที่ระบุขนาดของสแตกเธรดเท่ากับ 1MB จะแสดงด้านล่าง:
Number: 1
Number: 2
Number: 3
...
Number: 6262
Number: 6263
Number: 6264
Number: 6265
Number: 6266
Exception in thread "main" java.lang.StackOverflowError
at java.io.PrintStream.write(PrintStream.java:480)
at sun.nio.cs.StreamEncoder.writeBytes(StreamEncoder.java:221)
at sun.nio.cs.StreamEncoder.implFlushBuffer(StreamEncoder.java:291)
at sun.nio.cs.StreamEncoder.flushBuffer(StreamEncoder.java:104)
at java.io.OutputStreamWriter.flushBuffer(OutputStreamWriter.java:185)
at java.io.PrintStream.write(PrintStream.java:527)
at java.io.PrintStream.print(PrintStream.java:669)
at java.io.PrintStream.println(PrintStream.java:806)
at StackOverflowErrorExample.recursivePrint(StackOverflowErrorExample.java:4)
at StackOverflowErrorExample.recursivePrint(StackOverflowErrorExample.java:9)
at StackOverflowErrorExample.recursivePrint(StackOverflowErrorExample.java:9)
at StackOverflowErrorExample.recursivePrint(StackOverflowErrorExample.java:9)
...
ขึ้นอยู่กับการกำหนดค่าเริ่มต้นของ JVM ผลลัพธ์อาจแตกต่างกัน แต่ในที่สุด StackOverflowError
จะถูกส่งออกไป ตัวอย่างนี้เป็นตัวอย่างที่ดีมากว่าการเรียกซ้ำสามารถทำให้เกิดปัญหาได้อย่างไรหากไม่ดำเนินการด้วยความระมัดระวัง
วิธีจัดการกับ StackOverflowError
ทางออกที่ง่ายที่สุดคือการตรวจสอบการติดตามสแต็กอย่างระมัดระวังและตรวจสอบรูปแบบการทำซ้ำของหมายเลขบรรทัด หมายเลขบรรทัดเหล่านี้ระบุรหัสที่ถูกเรียกซ้ำ เมื่อคุณตรวจพบบรรทัดเหล่านี้คุณต้องตรวจสอบโค้ดของคุณอย่างรอบคอบและเข้าใจว่าเหตุใดการเรียกซ้ำจึงไม่สิ้นสุด
หากคุณตรวจสอบว่ามีการใช้การสอบถามซ้ำอย่างถูกต้องคุณสามารถเพิ่มขนาดของสแต็กเพื่อให้มีการเรียกใช้จำนวนมากขึ้น ทั้งนี้ขึ้นอยู่กับโปรแกรม Java Virtual Machine (JVM) ติดตั้งขนาดด้ายสแต็คเริ่มต้นอาจเท่ากับทั้ง512KB หรือ 1MB คุณสามารถเพิ่มขนาดเธรดสแต็กได้โดยใช้-Xss
แฟล็ก แฟล็กนี้สามารถระบุได้ผ่านการกำหนดค่าของโครงการหรือผ่านบรรทัดคำสั่ง รูปแบบของการ
-Xss
โต้แย้งคือ:
-Xss<size>[g|G|m|M|k|K]
หากคุณมีฟังก์ชั่นเช่น:
int foo()
{
// more stuff
foo();
}
จากนั้น foo () จะเรียกมันต่อไปเรื่อย ๆ ลึกขึ้นเรื่อย ๆ และเมื่อพื้นที่ที่ใช้ในการติดตามฟังก์ชั่นที่คุณเติมเต็มอยู่
สแต็คโอเวอร์โฟลว์หมายความว่าสแต็คโอเวอร์โฟลว์ โดยปกติจะมีสแต็กหนึ่งรายการในโปรแกรมที่มีตัวแปรขอบเขตภายในและที่อยู่ที่จะส่งคืนเมื่อการดำเนินการตามปกติสิ้นสุดลง สแต็กนั้นมีแนวโน้มที่จะเป็นช่วงหน่วยความจำคงที่ที่ใดที่หนึ่งในหน่วยความจำดังนั้นมันจึงถูก จำกัด ว่ามันสามารถมีค่าเท่าใด
หากสแต็กว่างเปล่าคุณไม่สามารถป๊อปอัปได้หากคุณทำเช่นนั้นคุณจะได้รับข้อผิดพลาดสแต็กอันเดอร์โฟล์ว
หากสแต็คเต็มคุณไม่สามารถผลักดันได้ถ้าคุณทำเช่นนั้นคุณจะได้รับข้อผิดพลาดสแต็คล้น
สแต็คโอเวอร์โฟลว์ปรากฏขึ้นเมื่อคุณจัดสรรสแต็กมากเกินไป ตัวอย่างเช่นในการสอบถามซ้ำดังกล่าว
การใช้งานบางอย่างปรับให้เหมาะสมในการเรียกซ้ำบางอย่าง หางเรียกซ้ำโดยเฉพาะ Tail recursive routines เป็นรูปแบบของชุดคำสั่งที่ recursive call ปรากฏขึ้นเป็นสิ่งสุดท้ายที่สิ่งที่ทำเป็นประจำ การโทรตามปกตินั้นลดลงอย่างรวดเร็ว
การใช้งานบางอย่างไปจนถึงการใช้สแต็คของตัวเองสำหรับการเรียกซ้ำดังนั้นพวกเขาจึงอนุญาตให้เรียกซ้ำเพื่อดำเนินการต่อจนกว่าระบบจะหมดหน่วยความจำ
สิ่งที่ง่ายที่สุดที่คุณสามารถลองได้ก็คือการเพิ่มขนาดสแต็คของคุณถ้าทำได้ หากคุณไม่สามารถทำเช่นนั้นสิ่งที่ดีที่สุดที่สองคือการดูว่ามีบางสิ่งที่ทำให้เกิดการล้นสแต็คอย่างชัดเจนหรือไม่ ลองโดยพิมพ์บางอย่างก่อนและหลังการโทรเข้าสู่รูทีน สิ่งนี้จะช่วยคุณค้นหารูทีนที่ล้มเหลว
กองซ้อนล้นมักจะเรียกว่าฟังก์ชั่นการทำรังเรียกลึกเกินไป (โดยเฉพาะอย่างยิ่งเมื่อใช้การเรียกซ้ำเช่นฟังก์ชั่นที่เรียกตัวเอง) หรือการจัดสรรหน่วยความจำจำนวนมากบนสแต็กที่ใช้กองจะเหมาะสมกว่า
อย่างที่คุณพูดคุณต้องแสดงรหัส :-)
ข้อผิดพลาดสแตกล้นมักเกิดขึ้นเมื่อฟังก์ชันของคุณเรียกซ้อนอย่างลึกเกินไป ดูสแต็กเธรดโอเวอร์โฟลว์โค้ดกอล์ฟสำหรับตัวอย่างของการเกิดเหตุการณ์นี้ (แม้ว่าในกรณีของคำถามนั้น
สาเหตุที่พบบ่อยที่สุดของล้นสแต็คเป็นมากเกินไปหรือลึก recursion หากนี่เป็นปัญหาของคุณการสอนเกี่ยวกับการเรียกซ้ำ Javaนี้อาจช่วยให้เข้าใจปัญหาได้
StackOverflowError
คือสแต็กตามที่OutOfMemoryError
เป็นไปยังกอง
การโทรซ้ำแบบไม่ จำกัด ส่งผลให้พื้นที่สแต็กมีการใช้หมด
ตัวอย่างต่อไปนี้สร้างStackOverflowError
:
class StackOverflowDemo
{
public static void unboundedRecursiveCall() {
unboundedRecursiveCall();
}
public static void main(String[] args)
{
unboundedRecursiveCall();
}
}
StackOverflowError
หลีกเลี่ยงได้หากการเรียกซ้ำซ้ำถูก จำกัด ขอบเขตเพื่อป้องกันผลรวมการโทรในหน่วยความจำที่ไม่สมบูรณ์ (เป็นไบต์) เกินขนาดสแต็ก (เป็นไบต์)
นี่คือตัวอย่างของอัลกอริทึมแบบเรียกซ้ำสำหรับการย้อนกลับรายการที่เชื่อมโยงโดยลำพัง บนแล็ปท็อปที่มีสเปคต่อไปนี้ (หน่วยความจำ 4G, Intel Core i5 2.3GHz CPU, 64 บิต Windows 7) ฟังก์ชั่นนี้จะทำงานเป็นข้อผิดพลาด StackOverflow สำหรับรายการที่เชื่อมโยงขนาดใกล้ 10,000
ประเด็นของฉันคือเราควรใช้การเรียกซ้ำอย่างรอบคอบโดยคำนึงถึงระดับของระบบเสมอ บ่อยครั้งที่การเรียกซ้ำสามารถเปลี่ยนเป็นโปรแกรมวนซ้ำได้ (หนึ่งรุ่นซ้ำของอัลกอริทึมเดียวกันได้รับที่ด้านล่างของหน้ามันกลับรายการเชื่อมโยงเดี่ยวขนาด 1 ล้านใน 9 มิลลิวินาที)
private static LinkedListNode doReverseRecursively(LinkedListNode x, LinkedListNode first){
LinkedListNode second = first.next;
first.next = x;
if(second != null){
return doReverseRecursively(first, second);
}else{
return first;
}
}
public static LinkedListNode reverseRecursively(LinkedListNode head){
return doReverseRecursively(null, head);
}
เวอร์ชันซ้ำของอัลกอริทึมเดียวกัน:
public static LinkedListNode reverseIteratively(LinkedListNode head){
return doReverseIteratively(null, head);
}
private static LinkedListNode doReverseIteratively(LinkedListNode x, LinkedListNode first) {
while (first != null) {
LinkedListNode second = first.next;
first.next = x;
x = first;
if (second == null) {
break;
} else {
first = second;
}
}
return first;
}
public static LinkedListNode reverseIteratively(LinkedListNode head){
return doReverseIteratively(null, head);
}
A StackOverflowError
เป็นข้อผิดพลาดรันไทม์ใน java
มันจะถูกโยนเมื่อเกินจำนวนหน่วยความจำสแต็กการโทรที่จัดสรรโดย JVM เกิน
กรณีทั่วไปของการStackOverflowError
ถูกโยนคือเมื่อ call stack เกินกว่าเนื่องจากการเรียกซ้ำที่ลึกหรือไม่มีที่สิ้นสุดมากเกินไป
ตัวอย่าง:
public class Factorial {
public static int factorial(int n){
if(n == 1){
return 1;
}
else{
return n * factorial(n-1);
}
}
public static void main(String[] args){
System.out.println("Main method started");
int result = Factorial.factorial(-1);
System.out.println("Factorial ==>"+result);
System.out.println("Main method ended");
}
}
การติดตามสแต็ก:
Main method started
Exception in thread "main" java.lang.StackOverflowError
at com.program.stackoverflow.Factorial.factorial(Factorial.java:9)
at com.program.stackoverflow.Factorial.factorial(Factorial.java:9)
at com.program.stackoverflow.Factorial.factorial(Factorial.java:9)
ในกรณีข้างต้นสามารถหลีกเลี่ยงได้โดยทำการเปลี่ยนแปลงทางโปรแกรม แต่ถ้าลอจิกของโปรแกรมนั้นถูกต้องและยังคงเกิดขึ้นคุณจะต้องเพิ่มขนาดสแต็ก
นี้เป็นกรณีทั่วไปของjava.lang.StackOverflowError
... วิธีการที่จะเรียกตัวเองซ้ำที่ไม่มีทางออกในdoubleValue()
, floatValue()
ฯลฯ
public class Rational extends Number implements Comparable<Rational> {
private int num;
private int denom;
public Rational(int num, int denom) {
this.num = num;
this.denom = denom;
}
public int compareTo(Rational r) {
if ((num / denom) - (r.num / r.denom) > 0) {
return +1;
} else if ((num / denom) - (r.num / r.denom) < 0) {
return -1;
}
return 0;
}
public Rational add(Rational r) {
return new Rational(num + r.num, denom + r.denom);
}
public Rational sub(Rational r) {
return new Rational(num - r.num, denom - r.denom);
}
public Rational mul(Rational r) {
return new Rational(num * r.num, denom * r.denom);
}
public Rational div(Rational r) {
return new Rational(num * r.denom, denom * r.num);
}
public int gcd(Rational r) {
int i = 1;
while (i != 0) {
i = denom % r.denom;
denom = r.denom;
r.denom = i;
}
return denom;
}
public String toString() {
String a = num + "/" + denom;
return a;
}
public double doubleValue() {
return (double) doubleValue();
}
public float floatValue() {
return (float) floatValue();
}
public int intValue() {
return (int) intValue();
}
public long longValue() {
return (long) longValue();
}
}
public class Main {
public static void main(String[] args) {
Rational a = new Rational(2, 4);
Rational b = new Rational(2, 6);
System.out.println(a + " + " + b + " = " + a.add(b));
System.out.println(a + " - " + b + " = " + a.sub(b));
System.out.println(a + " * " + b + " = " + a.mul(b));
System.out.println(a + " / " + b + " = " + a.div(b));
Rational[] arr = {new Rational(7, 1), new Rational(6, 1),
new Rational(5, 1), new Rational(4, 1),
new Rational(3, 1), new Rational(2, 1),
new Rational(1, 1), new Rational(1, 2),
new Rational(1, 3), new Rational(1, 4),
new Rational(1, 5), new Rational(1, 6),
new Rational(1, 7), new Rational(1, 8),
new Rational(1, 9), new Rational(0, 1)};
selectSort(arr);
for (int i = 0; i < arr.length - 1; ++i) {
if (arr[i].compareTo(arr[i + 1]) > 0) {
System.exit(1);
}
}
Number n = new Rational(3, 2);
System.out.println(n.doubleValue());
System.out.println(n.floatValue());
System.out.println(n.intValue());
System.out.println(n.longValue());
}
public static <T extends Comparable<? super T>> void selectSort(T[] array) {
T temp;
int mini;
for (int i = 0; i < array.length - 1; ++i) {
mini = i;
for (int j = i + 1; j < array.length; ++j) {
if (array[j].compareTo(array[mini]) < 0) {
mini = j;
}
}
if (i != mini) {
temp = array[i];
array[i] = array[mini];
array[mini] = temp;
}
}
}
}
2/4 + 2/6 = 4/10
Exception in thread "main" java.lang.StackOverflowError
2/4 - 2/6 = 0/-2
at com.xetrasu.Rational.doubleValue(Rational.java:64)
2/4 * 2/6 = 4/24
at com.xetrasu.Rational.doubleValue(Rational.java:64)
2/4 / 2/6 = 12/8
at com.xetrasu.Rational.doubleValue(Rational.java:64)
at com.xetrasu.Rational.doubleValue(Rational.java:64)
at com.xetrasu.Rational.doubleValue(Rational.java:64)
at com.xetrasu.Rational.doubleValue(Rational.java:64)
at com.xetrasu.Rational.doubleValue(Rational.java:64)
ใน crunch สถานการณ์ด้านล่างจะทำให้เกิดข้อผิดพลาดมากเกินไปของ Stack
public class Example3 {
public static void main(String[] args) {
main(new String[1]);
}
}
นี่คือตัวอย่าง
public static void main(String[] args) {
System.out.println(add5(1));
}
public static int add5(int a) {
return add5(a) + 5;
}
โดยทั่วไปแล้ว StackOverflowError คือเมื่อคุณพยายามทำบางสิ่งบางอย่างที่มีแนวโน้มว่าจะเรียกตัวเองและไปหาอินฟินิตี้ (หรือจนกว่าจะให้ StackOverflowError)
add5(a)
จะเรียกตัวเองแล้วเรียกตัวเองอีกครั้งและอื่น ๆ
คำว่า "stack overrun (ล้น)" มักจะถูกใช้ แต่เรียกชื่อผิด; การโจมตีไม่ล้นกอง แต่บัฟเฟอร์ในกอง
- จากสไลด์บรรยายของศ. ดร. Dieter Gollmann