什么是StackOverflowError?

什么是StackOverflowError,它的原因是什么,以及我应该如何处理它们?

798226 次浏览

如您所说,您需要展示一些代码。: -)

堆栈溢出错误通常发生在函数调用nest太深的时候。有关如何发生这种情况的一些示例,请参阅堆栈溢出代码高尔夫线程(尽管在该问题的情况下,答案会故意导致堆栈溢出)。

堆栈溢出通常是由于嵌套函数调用太深(在使用递归时尤其容易,即函数调用自身)或在堆栈上分配大量内存而使用堆更合适。

如果你有这样一个函数:

int foo()
{
// more stuff
foo();
}

然后foo()将继续调用自己,越调用越深,当用于跟踪所处函数的空间被填满时,就会得到堆栈溢出错误。

参数和局部变量分配在堆栈上(对于引用类型,对象位于上,堆栈中的一个变量在堆上引用该对象)。堆栈通常位于地址空间的端,当它被用完时,它会指向地址空间的(即趋于零)。

你的进程还有一个,它位于进程的端。当您分配内存时,这个堆可以向地址空间的顶端增长。正如你所看到的,堆有可能与堆栈一起“碰撞”(有点像构造板块!!)。

堆栈溢出的常见原因是糟糕的递归调用。通常,这是当递归函数没有正确的终止条件时引起的,因此它最终永远调用自己。或者当终止条件良好时,可能是由于在实现终止条件之前需要太多的递归调用而导致的。

然而,使用GUI编程,可以生成间接递归。例如,你的应用程序可能正在处理paint消息,在处理它们的同时,它可能会调用一个函数,导致系统发送另一个paint消息。这里您没有显式地调用自己,但是OS/VM已经为您完成了。

要处理这些问题,您需要检查您的代码。如果你有调用自己的函数,那么检查你是否有一个终止条件。如果有,那么在调用函数时检查至少修改了其中一个参数,否则递归调用的函数不会有明显的变化,终止条件也没有用。还要注意,您的堆栈空间可能在达到有效的终止条件之前耗尽内存,因此请确保您的方法可以处理需要更多递归调用的输入值。

如果没有明显的递归函数,则检查是否调用了任何间接将导致函数被调用的库函数(如上面的隐式情况)。

堆栈溢出最常见的原因是过度深度或无限递归。如果这是你的问题,关于Java递归的本教程可以帮助你理解问题。

堆栈溢出的意思就是:堆栈溢出。通常在程序中有一个堆栈,它包含局部作用域变量和例程执行结束时返回的地址。该堆栈往往是内存中的某个固定内存范围,因此它可以包含多少值是有限的。

如果堆栈是空的,你不能弹出,如果你这样做,你会得到堆栈溢出错误。

如果堆栈满了,你就不能推,如果你推了,你就会得到堆栈溢出错误。

当你在堆栈中分配了太多内存时,就会出现堆栈溢出。例如,在前面提到的递归中。

有些实现优化了某些形式的递归。特别是尾递归。尾递归例程是一种例程,其中递归调用作为例程所做的最后一件事出现。这样的例行调用被简化为跳转。

有些实现甚至实现了自己的递归堆栈,因此它们允许递归继续进行,直到系统内存耗尽。

你可以尝试的最简单的事情就是如果可以的话增加你的堆栈大小。如果不能这样做,那么第二种方法是查看是否有什么东西明显导致堆栈溢出。试着在调用前后打印一些东西到例程中。这可以帮助您找到失败的例程。

StackOverflowError之于栈,正如OutOfMemoryError之于堆。

无界递归调用会导致堆栈空间被用完。

下面的例子生成StackOverflowError:

class  StackOverflowDemo
{
public static void unboundedRecursiveCall() {
unboundedRecursiveCall();
}


public static void main(String[] args)
{
unboundedRecursiveCall();
}
}

StackOverflowError是可以避免的,如果递归调用是有界的,以防止内存中不完整调用的总和(以字节为单位)超过堆栈大小(以字节为单位)。

下面是一个递归算法的例子,用于反转单链表。在笔记本电脑(规格为4gb内存,Intel Core i5 2.3 GHz CPU 64位和Windows 7)上,对于大小接近10,000的链表,此函数将遇到StackOverflow错误。

我的观点是,我们应该明智地使用递归,始终考虑到系统的规模。

通常递归可以转换为迭代程序,迭代程序的伸缩性更好。(本页底部给出了同一算法的一个迭代版本。它在9毫秒内反转大小为100万的单链表。)

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

为了描述这一点,首先让我们了解当地的变量和对象是如何存储的。

局部变量存储在堆栈中:

Enter image description here

如果你看了图片,你应该能够理解事情是如何工作的。

当Java应用程序调用函数时,将在调用堆栈上分配堆栈帧。堆栈帧包含被调用方法的参数、局部参数和方法的返回地址。返回地址表示执行点,在调用的方法返回后,程序将从该执行点继续执行。如果没有空间用于新的堆栈帧,则由Java虚拟机(JVM)抛出StackOverflowError

可能耗尽Java应用程序堆栈的最常见情况是递归。在递归中,方法在执行过程中调用自身。递归被认为是一种强大的通用编程技术,但必须谨慎使用,以避免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 .

    最简单的解决方案是仔细检查堆栈跟踪和 检测行号的重复模式。这些行号 指示递归调用的代码。一旦你发现了这些 行,您必须仔细检查您的代码,并理解为什么

  1. 如果你已经验证了递归 如果实现正确,可以增加堆栈的大小,在 以允许更大数量的调用。取决于Java 虚拟机(JVM)安装后,默认线程堆栈大小可能 等于512kb,即1mb。您可以增加线程堆栈 size使用-Xss标志。方法指定此标志 项目的配置,或者通过命令行。的格式 -Xss参数是: -Xss<size>[g|G|m|M|k|K] < / p >

这里有一个例子

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)将调用自身,然后再次调用自身,以此类推。

StackOverflowError是Java中的运行时错误。

当超过JVM分配的调用堆栈内存量时抛出该异常。

抛出StackOverflowError的一种常见情况是当调用堆栈由于过度深度或无限递归而超出时。

例子:

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()等中没有出口。

文件Rational.java

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

文件Main.java

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)

下面是OpenJDK 7中StackOverflowError的源代码

在紧急情况下,以下情况将导致堆栈溢出错误。

public class Example3 {


public static void main(String[] args) {


main(new String[1]);
}


}

堆栈有一个空间限制,这取决于操作系统。正常的大小是8 MB(在Ubuntu (Linux)中,你可以用$ ulimit -u检查这个限制,在其他操作系统中也可以类似地检查)。任何程序都在运行时使用堆栈,但要完全了解何时使用堆栈,您需要检查汇编语言。例如,在x86_64中,堆栈用于:

  1. 在进行过程调用时保存返回地址
  2. 保存本地变量
  3. 保存特殊寄存器以便稍后恢复它们
  4. 向过程调用传递参数(大于6)
  5. 其他:随机未使用的堆栈基础,金丝雀值,填充,…等。

如果您不知道x86_64(一般情况下),您只需要知道您所使用的特定高级编程语言何时编译这些操作。例如在C语言中:

  • →函数调用
  • (2)→函数调用中的局部变量(包括main)
  • (3)→函数调用中的局部变量(不是main)
  • →函数调用
  • (5)→通常是一个函数调用,通常与堆栈溢出无关。

因此,在C语言中,只有局部变量和函数调用使用堆栈。造成堆栈溢出的两种(唯一的?)方法是:

  • 在main或任何函数中声明过大的局部变量(int array[10000][10000];)
  • 深度递归或无限递归(同时调用太多函数)。

为了避免StackOverflowError,你可以:

  • 检查局部变量是否太大(1mb数量级)→使用堆(malloc/calloc调用)或全局变量。

  • 检查无限递归→你知道该怎么做…正确的!

  • 检查正常的太深递归→最简单的方法是将实现更改为迭代。

还要注意全局变量、包含库等等……不要使用堆栈。

只有当上述方法不起作用时,才可以在特定的操作系统上将堆栈大小更改为最大值。例如Ubuntu: ulimit -s 32768 (32 MB)。(这从来都不是我的任何堆栈溢出错误的解决方案,但我也没有太多经验。)

我省略了C语言中的特殊和/或非标准情况(例如alloc()和类似的用法),因为如果你正在使用它们,你应该已经确切地知道你在做什么。

一个简单的Java示例,由于错误的递归调用导致Java .lang. stackoverflowerror:

class Human {
Human(){
new Animal();
}
}


class Animal extends Human {
Animal(){
super();
}
}


public class Test01 {
public static void main(String[] args) {
new Animal();
}
}