什么是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);
}

其他回答

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

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

如果您不知道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()和类似的用法),因为如果你正在使用它们,你应该已经确切地知道你在做什么。

StackOverflowError之于堆栈,就像OutOfMemoryError之于堆。

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

产生StackOverflowError的示例如下:

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

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

如果对递归调用进行了限制,以防止内存中不完整调用的总和(以字节为单位)超过堆栈大小(以字节为单位),则可以避免StackOverflowError。

这个问题的很多答案都是好的。但是,我想采取稍微不同的方法,并对内存的工作原理和(简化的)可视化更好地理解StackOverflow错误给出一些更深入的见解。这种理解不仅适用于Java,而且适用于所有进程。

在现代系统中,所有新进程都有自己的虚拟地址空间(VAS)。从本质上讲,VAS是操作系统在物理内存之上提供的抽象层,目的是确保进程之间不相互干扰内存。内核的任务是将虚拟地址映射到实际的物理地址。


VAS可以分为几个部分:

为了让CPU知道它应该做什么,机器指令必须加载到内存中。这通常被称为代码或文本段,具有静态大小。

在此之上,可以找到数据段和堆。数据段大小固定,包含全局变量或静态变量。 当程序遇到特殊情况时,它可能需要额外分配数据,这就是堆发挥作用的地方,因此它能够动态地增长大小。

堆栈位于虚拟地址空间的另一侧,并使用后进先出(LIFO)数据结构跟踪所有函数调用。与堆类似,程序在运行时可能需要额外的空间来跟踪正在调用的新函数调用。由于堆栈位于VAS的另一侧,它正向相反的方向增长,即朝着堆的方向增长。

博士TL;

这就是StackOverflow错误发挥作用的地方。

由于堆栈向下增长(朝向堆),可能会发生在某个时间点它不能继续增长,因为它会与堆地址空间重叠。一旦发生这种情况,就会发生StackOverflow错误。

发生这种情况最常见的原因是由于程序中的一个错误导致递归调用不能正确终止。


请注意,在某些系统上,VAS的行为可能略有不同,甚至可以分为更多的部分,但是,这种一般理解适用于所有UNIX系统。

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