什么是StackOverflowError?

527

什么是 StackOverflowError, 它是由什么引起的,我该如何处理它们?


Java中的堆栈大小较小。有时,例如在递归调用过多时,您会遇到此问题。您可以通过循环重新设计代码。您可以在此网址中找到通用的设计模式:http://www.jndanial.com/73/ - Danial Jalalnouri
一个不太显而易见的方法是:在某个静态上下文(例如 main 方法)中添加行 new Object() {{getClass().newInstance();}};。 从实例上下文中不起作用(仅抛出 InstantiationException)。 - John McClane
16个回答

461

参数和局部变量被分配在上(对于引用类型,对象存在于上,而栈中的变量引用堆上的对象)。栈通常位于地址空间的上部,随着使用它而逐渐向地址空间的底部移动(即朝零移动)。

您的进程还有一个,它位于进程的底部。当您分配内存时,此堆可以向地址空间的上部增长。如您所见,堆有可能与栈“碰撞”(有些像构造板块!)。

堆栈溢出的常见原因是错误的递归调用。通常,这是由于您的递归函数没有正确的终止条件,因此会一直调用自身。或者终止条件没问题,但在满足之前需要进行太多的递归调用,也可能导致其发生。

然而,在 GUI 编程中,可能会产生间接递归。例如,您的应用程序可能正在处理绘制消息,并在处理它们时调用了一个导致系统发送另一个绘制消息的函数。在这种情况下,您没有显式地调用自己,而是由操作系统/虚拟机为您完成。

要处理它们,您需要检查代码。如果您有调用自身的函数,则检查是否有终止条件。如果有,那么请检查在调用函数时是否至少修改了一个参数,否则递归调用的函数将没有可见的变化,并且终止条件将是无用的。还要注意,在达到有效的终止条件之前,您的堆栈空间可能会耗尽内存,因此确保您的方法能够处理需要更多递归调用的输入值。

如果没有明显的递归函数,请检查是否调用了任何库函数,这些库函数间接地会导致调用您的函数(如上述隐式情况)。


3
原帖发布者:嘿,这很棒。所以,递归总是导致堆栈溢出的原因吗?或者其他原因也可能导致堆栈溢出吗?不幸的是,我正在使用一个库...但并非我理解的那种。 - Ziggy
5
哈哈哈,这就是它:while (points < 100) {addMouseListeners(); moveball(); checkforcollision(); pause(speed);} 哇,我感到非常愚蠢,没有意识到我最终会得到一个堆满鼠标监听器的堆栈......谢谢大家! - Ziggy
9
不,堆栈溢出也可能是由于变量过大而无法在堆栈上分配引起的。如果您查阅 http://en.wikipedia.org/wiki/Stack_overflow 上的维基百科文章,就会了解到这一点。 - JB King
10
应该指出的是,几乎不可能“处理”堆栈溢出错误。在大多数环境中,要处理这个错误,需要在堆栈上运行代码,如果没有更多的堆栈空间,这将变得非常困难。 - Hot Licks
7
在Java中,只有原始类型和引用被保存在堆栈上,而所有的大型内容(如数组和对象)都在堆上。 - jcsahnwaldt Reinstate Monica
显示剩余4条评论

138

为了描述这个问题,首先让我们理解局部变量和对象的存储方式。

局部变量存储在堆栈上:

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 标志指定线程堆栈大小为 1 MB 的示例执行如下:
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. 最简单的解决方法是仔细检查堆栈跟踪并检测行号的重复模式。这些行号表示正在递归调用的代码。一旦检测到这些行,必须仔细检查代码并了解为什么递归永远不会终止。

  2. 如果已经验证递归的实现是正确的,可以增加堆栈的大小,以允许更多的调用。根据安装的Java虚拟机(JVM),默认的线程堆栈大小可能等于512 KB或1 MB。您可以使用-Xss标志来增加线程堆栈大小。可以通过项目的配置或命令行指定此标志。 -Xss参数的格式为:-Xss<size>[g|G|m|M|k|K]


在使用Windows时,某些Java版本似乎存在一个bug,即-Xss参数只对新线程生效。 - goerlibe

70

如果您有一个如下的函数:

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

然后foo()会不断地调用自己,越来越深入,当用于跟踪当前函数的空间被占满时,就会出现堆栈溢出错误。


14
错误。你的函数是尾递归的。大多数编译语言都有尾递归优化。这意味着递归会被转化为简单循环,因此在某些系统上,使用这段代码永远不会导致堆栈溢出。 - Cheery
哪些非功能性语言支持尾递归? - horseyguy
@banister 和一些 JavaScript 的实现 - Pacerier
@horseyguy Scala支持尾递归。 - AKs
1
这捕捉了可能导致堆栈溢出的本质。不错。 - user1300214

26

堆栈溢出(Stack Overflow)就是指:一个堆栈超出了它的容量。通常程序中有一个堆栈,其中包含局部作用域变量以及执行完一段例程后返回的地址。这个堆栈往往是内存中的一个固定范围,因此它所能包含的数值是有限的。

如果堆栈为空,则无法弹出元素,否则会出现堆栈下溢错误。

如果堆栈已满,则无法推入元素,否则将出现堆栈溢出错误。

因此,在向堆栈分配过多空间时,就会出现堆栈溢出。例如,在所提到的递归中。

一些实现可以优化掉某些形式的递归,特别是尾递归。尾递归是一种递归形式,其中递归调用作为例程最后执行的事情。这种例程调用只需被简化成跳转。

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

你可以尝试的最简单的方法就是增加堆栈大小。然而,如果你无法这样做,第二个最好的方法就是查找是否有明显引起堆栈溢出的问题。通过在调用例程之前和之后打印一些内容,可以帮助你找到失败的例程。


5
栈会出现“下溢”这样的情况吗? - Pacerier
6
在汇编语言中可能会出现堆栈下溢(弹出比压入的数量还多),但在编译语言中几乎不可能发生。我不确定,您可能可以找到C语言中alloca()的实现,它“支持”负大小。 - Score_Under
2
栈溢出的意思就是栈溢出了。通常程序中有一个栈,其中包含局部作用域变量 -> 不是的,每个线程都有自己的栈,其中包含每个方法调用的栈帧,其中包含局部变量。 - Koray Tugay

11

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


1
抱歉,没有看到 Java 标签。 - Greg
@ChrisJester-Young 如果我在一个方法中有100个本地变量,难道所有的变量都会被放在堆栈上吗?没有例外吗? - Pacerier
@Pacerier:本地_变量_都在堆栈上,是正确的。 但是,如果变量是引用类型,则它们指向的对象都分配在堆上(除非逃逸分析优化的情况)。 (注:在Java中,数组属于引用类型,即使是基元素类型的数组。) - C. K. Young
顺便问一下@ChrisJester-Young,堆栈上的变量有任何机会被其他线程看到吗? - Pacerier
@ChrisJester-Young 抱歉,我觉得我表达不清楚。我实际上是指“堆栈有没有可能是非线程本地堆栈”? - Pacerier
显示剩余6条评论

7

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)

在上述情况下,可以通过进行程序的变动来避免此错误。但如果程序逻辑正确仍然出现此错误,则需要增加堆栈大小。

7

就像你所说的那样,你需要展示一些代码。 :-)

堆栈溢出错误通常发生在函数调用嵌套过深的情况下。可以查看Stack Overflow Code Golf线程,了解一些导致这种情况发生的示例(尽管在该问题的情况下,答案故意导致堆栈溢出)。


1
我完全想添加代码,但是因为我不知道什么会导致堆栈溢出,所以我不确定要添加什么代码。添加所有代码也太平庸了,对吧? - Ziggy
你的项目是开源的吗?如果是的话,只需要创建一个Sourceforge或Github账户,然后将所有代码上传到那里即可。 :-) - C. K. Young
这听起来是个好主意,但我太菜了,甚至不知道我需要上传什么。比如,我导入的库、扩展的类等都是未知的。哦,天啊:糟糕的时光。 - Ziggy

6

StackOverflowError(堆栈溢出错误)相当于堆(heap)中的OutOfMemoryError(内存溢出错误)。

不受限制的递归调用会导致使用完整个堆栈空间。

下面的示例会产生StackOverflowError

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

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

StackOverflowError可以避免,只要限制递归调用的深度,防止未完成的内存调用(以字节为单位)总和超过堆栈大小(以字节为单位)即可。


5
堆栈溢出的最普遍原因是过度深入或无限递归。如果这是您的问题,这个 Java 递归教程 可能有助于理解问题。

5

这是一个递归算法反转单向链表的例子。在一台配备4 GB内存、Intel Core i5 2.3 GHz 64位CPU和Windows 7操作系统的笔记本电脑上,当单向链表的大小接近10,000时,该函数会遇到栈溢出错误。

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

通常情况下,递归可以转换为迭代程序,从而更好地扩展。(同样算法的一种迭代版本在页面底部给出。它可以在9毫秒内反转大小为1百万的单向链表。)

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

我认为使用JVM时,你的笔记本电脑的规格实际上并不重要。 - kevin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接