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


当前回答

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

局部变量存储在堆栈上:

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

当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

The simplest solution is to carefully inspect the stack trace and detect the repeating pattern of line numbers. These line numbers indicate the code being recursively called. Once you detect these lines, you must carefully inspect your code and understand why the recursion never terminates. If you have verified that the recursion is implemented correctly, you can increase the stack’s size, in order to allow a larger number of invocations. Depending on the Java Virtual Machine (JVM) installed, the default thread stack size may equal to either 512 KB, or 1 MB. You can increase the thread stack size using the -Xss flag. This flag can be specified either via the project’s configuration, or via the command line. The format of the -Xss argument is: -Xss<size>[g|G|m|M|k|K]

其他回答

一个简单的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();
    }
}

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

下面是一个递归算法的例子,用于反转单链表。在笔记本电脑(规格为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);
}

这里有一个例子

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

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

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

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

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

To deal with them, you'll need to examine your code. If you've got functions that call themselves then check that you've got a terminating condition. If you have, then check that when calling the function you have at least modified one of the arguments, otherwise there'll be no visible change for the recursively called function and the terminating condition is useless. Also mind that your stack space can run out of memory before reaching a valid terminating condition, thus make sure your method can handle input values requiring more recursive calls.

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