什么导致了堆栈溢出错误?

237

我到处查找,但无法找到一个确定的答案。根据文档,Java在以下情况下会抛出 java.lang.StackOverflowError 错误:

当应用递归过深时,堆栈溢出发生时抛出。

但这引发了两个问题:

  • 是否有其他方式可以导致堆栈溢出,而不仅仅是递归?
  • StackOverflowError 在 JVM 实际溢出堆栈之前或之后发生?

对于第二个问题的详细说明:

当 Java 抛出 StackOverflowError 时,您可以安全地假定堆栈未写入堆吗?如果您在抛出堆栈溢出异常的函数的 try/catch 中缩小堆栈或堆的大小,您是否可以继续工作?这是否有记录在案?

我不需要的答案:

  • 堆栈溢出是由于错误的递归造成的。
  • 堆栈溢出是当堆满足堆栈时发生的。

1
默认的堆栈大小相当大,据我所知在Linux上为8 MB。这使得不使用递归很难产生堆栈溢出。 - nosid
1
你可以产生一个巨大的方法调用链,这可能会导致栈溢出(例如方法a调用b,b调用c,c调用d,...),但这只是想象而已。 - Smutje
8
http://codegolf.stackexchange.com/questions/9359/shortest-program-that-throws-stackoverflow-error 包含一些会产生stackoverflow错误的程序,其中包括一些使用递归形式的Java程序。 - njzk2
1
堆栈运行到堆中?那是在内存管理单元普及于CPU之前的1980年代。;-) - Martin
1
“当堆栈相遇时,就会发生StackOverflow。”——嗯,你看,@Crackers,当堆和栈坠入爱河时…… - Jason C
显示剩余15条评论
10个回答

197

看来你认为stackoverflow 错误就像本地程序中的缓冲区溢出异常,存在写入未分配给缓冲区的内存的风险,从而破坏其他内存位置。 这根本不是这样。

对于每个线程的每个堆栈,JVM都有一个分配的内存,在尝试调用方法时,如果填充该内存,则 JVM 抛出错误。 就像在长度为 N 的数组的索引 N 处尝试写入一样。 不会发生任何内存损坏。 堆栈无法写入堆。

堆栈溢出错误(StackOverflowError)相当于堆空间溢出错误(OutOfMemoryError):它只是表示没有更多的可用内存。

虚拟机错误说明(§6.3)

StackOverflowError:Java 虚拟机实现已经为线程用完了堆栈空间,通常是因为由执行程序的故障导致线程进行无限递归调用。


26
只是一个约定,java.lang.StackOverflowError是一个错误,像OutOfMemory一样,不是异常。Error和Exception都扩展自Throwable。捕获Error/throwable而不是Exception是非常糟糕的实践。 - Ezequiel
1
将异常重命名为错误。谢谢。 - JB Nizet
需要回答第一个问题:“栈溢出不仅仅通过递归还有其他的方式吗?” - retrohacker
2
你在所有其他答案中都得到了答案。当堆栈已满时,无论以何种方式到达此情况,您都会收到错误提示。但是,鉴于堆栈的大尺寸,我从未见过没有递归发生这种情况。 - JB Nizet
2
HotSpot拥有可靠和安全的堆栈溢出检测,但我想知道这种行为是否是JVM规范所要求的? - ntoskrnl
4
@ntoskrnl:是的:http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-2.html#jvms-2.5.2 - JB Nizet

54

栈溢出不仅仅是通过递归发生的,还有其他方式吗?

当然。只需不断调用方法而不返回即可。但是需要许多方法,除非允许递归。实际上并没有区别:栈帧是一样的,无论它是递归方法的栈帧还是非递归方法的栈帧。

答案是:当JVM尝试为下一次调用分配栈帧时,如果发现不可能,则检测到了栈溢出。所以不会有任何被覆盖的情况发生。


1
@Cruncher 这不是一个固定的深度。方法的参数,例如,存储在堆栈上。更多的参数意味着需要更多的内存来存储它们在堆栈上,这意味着在分配给堆栈的内存耗尽之前,深度会变得更小。通过使用一个带有1个参数的方法进行测试,然后再使用5个参数进行测试。 - JB Nizet
3
请注意,Java虚拟机可能会选择使用固定深度的堆栈,也可能选择根据规范动态扩展堆栈空间。 Hotspot默认情况下不会这样做。详情请参阅http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-2.html#jvms-2.5.2。 - JB Nizet
1
@Ingo,字节码堆栈与运行时用于方法调用的堆栈有所不同。运行时将字节码编译为本机代码(或至少可以自由地这样做),并且在这样做时,至少其中一些堆栈使用被转换为机器寄存器使用。它可能会为临时存储保留一些堆栈空间,但这通常小于所需的最大字节码解释堆栈量。 - Jules
2
@Jsor,实际上,Java堆和栈不会相互干扰。即使在堆中有数千兆字节的空闲空间,您仍然可能遇到堆栈溢出。反之,您的堆栈大小并没有变小等情况下,也可能因为内存不足而死机。 - Ingo
3
一个Java应用程序可以有多个线程,这些线程共享堆,但每个线程都有独立的栈空间。因此,这不会那么容易实现。 - Paŭlo Ebermann
显示剩余6条评论

27
挑战接受 :) StackOverflowError 不仅仅是通过递归发生的,还有其他方式 (挑战失败,请参见评论):
public class Test
{
    final static int CALLS = 710;

    public static void main(String[] args)
    {
        final Functor[] functors = new Functor[CALLS];
        for (int i = 0; i < CALLS; i++)
        {
            final int finalInt = i;
            functors[i] = new Functor()
            {
                @Override
                public void fun()
                {
                    System.out.print(finalInt + " ");
                    if (finalInt != CALLS - 1)
                    {
                        functors[finalInt + 1].fun();
                    }
                }
            };
        }
        // Let's get ready to ruuuuuuumble!
        functors[0].fun(); // Sorry, couldn't resist to not comment in such moment. 
    }

    interface Functor
    {
        void fun();
    }
}

使用标准的javac Test.java编译,然后使用java -Xss104k Test 2> out运行。之后,more out将会告诉你:
Exception in thread "main" java.lang.StackOverflowError

第二次尝试。

现在这个想法更加简单了。Java中的基本数据类型可以存储在栈上。因此,让我们声明很多double变量,比如double a1,a2,a3...。这个脚本可以为我们编写、编译和运行代码

#!/bin/sh

VARIABLES=4000
NAME=Test
FILE=$NAME.java
SOURCE="public class $NAME{public static void main(String[] args){double "
for i in $(seq 1 $VARIABLES);
do
    SOURCE=$SOURCE"a$i,"
done
SOURCE=$SOURCE"b=0;System.out.println(b);}}"
echo $SOURCE > $FILE
javac $FILE
java -Xss104k $NAME

而且……我得到了一些意外的东西:

#
# A fatal error has been detected by the Java Runtime Environment:
#
#  SIGSEGV (0xb) at pc=0x00007f4822f9d501, pid=4988, tid=139947823249152
#
# JRE version: 6.0_27-b27
# Java VM: OpenJDK 64-Bit Server VM (20.0-b12 mixed mode linux-amd64 compressed oops)
# Derivative: IcedTea6 1.12.6
# Distribution: Ubuntu 10.04.1 LTS, package 6b27-1.12.6-1ubuntu0.10.04.2
# Problematic frame:
# V  [libjvm.so+0x4ce501]  JavaThread::last_frame()+0xa1
#
# An error report file with more information is saved as:
# /home/adam/Desktop/test/hs_err_pid4988.log
#
# If you would like to submit a bug report, please include
# instructions how to reproduce the bug and visit:
#   https://bugs.launchpad.net/ubuntu/+source/openjdk-6/
#
Aborted

这是与您的第二个问题相关的内容:它是100%重复的。

StackOverflowError发生在JVM实际溢出堆栈之前还是之后?

因此,在OpenJDK 20.0-b12的情况下,我们可以看到JVM首先爆炸了。但这似乎是一个错误,也许有人可以在评论中确认一下,因为我不确定。我应该报告这个问题吗?也许在某个更新的版本中已经修复了...根据JB Nizet在评论中提供的JVM specification link,JVM应该抛出StackOverflowError而不是死机:

如果线程中的计算需要比允许的Java虚拟机堆栈更大,则Java虚拟机会抛出StackOverflowError。


第三次尝试。

public class Test {
    Test test = new Test();

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

我们希望创建一个新的Test对象。因此,它的(隐式)构造函数将被调用。但是,在此之前,Test的所有成员都会被初始化。因此,Test test = new Test()首先被执行...
我们希望创建一个新的Test对象...
更新:不幸的是,这是递归,我在这里提出了相关问题。

11
这不是递归吗?fun() 调用了 fun()。虽然是在不同的对象上调用,但还是递归的。 - JB Nizet
3
@JBNizet 但是 fun() 调用不同的 fun(),使用不同的“封闭”对象finalInt,因此这不是递归。请注意,此处仅为翻译,没有解释或附加内容。 - Adam Stelmaszczyk
17
不。调用的是同一个类的不同实例上的单个 fun() 方法。这就是递归。 - JB Nizet
5
请问需要翻译的是:@Adam Write a native JNI function that allocates a gigantic object on the stack and then call a Java method. - Voo
4
哦,是的,你肯定发现了一个 bug。基本上只要你不写有问题的本地代码并通过 JNI 调用它(至少是间接地),你就不应该看到那些错误消息。必须检查一下那个 bug 在最新版本是否还有效。 - Voo
显示剩余12条评论

3

StackOverFlowError最常见的原因是过深或无限递归。

例如:

public int yourMethod(){
       yourMethod();//infinite recursion
}

在Java中:
内存有两个区域,分别是堆和栈。栈内存用于存储局部变量和函数调用,而堆内存用于存储Java对象。
如果栈内存没有足够的空间来存储函数调用或局部变量,JVM会抛出java.lang.StackOverFlowError错误。
而如果没有更多的堆空间来创建对象,JVM会抛出java.lang.OutOfMemoryError错误。

3

没有“StackOverFlowException”。你可能指的是“StackOverFlowError”。

如果你捕获到这个错误,可以继续工作,因为在这种情况下堆栈被清除,但这将是一个不好看且不优雅的选项。

什么时候会抛出这个错误?- 当你调用一个方法时,JVM会验证是否有足够的内存来执行它。当然,如果不可能,就会抛出该错误。

  • 不,那是你唯一可能遇到这个错误的方式:让你的堆栈充满。但不仅仅是通过递归,还包括无限调用其他方法的方法。这是一个非常具体的错误,所以不会。
  • 它会在堆栈满之前抛出,确切地说是在验证时。如果没有可用空间,你会放置数据在哪里?覆盖其他数据?不行。

1
如果我能自己回答这个问题,就不会问和做研究了。我知道这通常是递归函数的结果,这就是为什么我在问题本身中提到了这一点,并明确说明这不是我要找的答案。是否存在由于内存限制可能导致堆栈溢出的情况,但这并不一定是致命问题?最后两个要点是什么?我不明白它们在说什么。 - retrohacker
在捕获StackOverflowError后,没有任何现实情况可以让您假设自己是“安全的”。问题不在于JVM的健康状况,而在于您自己的数据和操作的健康状况,并且这个问题很复杂,因为您无法预测和计划错误发生的时间和位置,因此也就无法确定哪些代码已经执行或未执行。 - jackr
3
有很多情况下,例如使用递归算法只修改了一小部分数据。在这种情况下,你可以放弃这些结果,并按照自己的想法处理失败。 - Tim B

3
Java有两个主要的存储位置。第一个是堆,用于动态分配对象。new

此外,每个运行中的线程都有自己的栈,并且为该栈分配了一定数量的内存。

当您调用方法时,数据被推入栈中以记录方法调用、传递的参数和分配的任何局部变量。具有五个局部变量和三个参数的方法将比没有局部变量的void doStuff()方法使用更多的栈空间。

栈的主要优点是不存在内存碎片,一个方法调用的所有内容都分配在栈的顶部,并且从方法返回很容易。要从方法返回,只需将栈展开回到上一个方法,设置所需的任何返回值即可完成。

由于栈对于每个线程都是固定大小的(请注意,Java规范并不要求固定大小,但是大多数JVM实现在撰写本文时使用固定大小),并且因为每次进行方法调用时都需要空间,希望现在清楚为什么可能会用尽栈以及是什么原因导致它用尽了。没有固定的方法调用次数,递归也没有任何特定的东西,当您尝试调用方法并且没有足够的内存时,您将得到异常。

当然,栈的大小设置得足够高,以至于在常规代码中很难发生这种情况。但在递归代码中,可以很容易地递归到非常深的层次,此时您开始遇到此错误。


很好的总结,但请注意堆栈大小不需要固定,参见JB Nizet的这条评论 - siegi
@ siegi 谢谢,我已经添加了一条注释来澄清。 - Tim B

2

StackOverflowError是由应用程序递归太深引起的(这不是你期望得到的答案)。

现在,其他导致StackOverflowError发生的事情是从方法中不断调用方法,直到出现StackOverflowError,但没有人可以编写程序来获取StackOverflowError,即使那些程序员这样做,他们也没有遵循编码标准圈复杂度,每个程序员在编程时都必须理解。这种“StackOverflowError”的原因需要花费很多时间才能纠正。

但是,无意中编写一行或两行代码会导致StackOverflowError,这是可以理解的,JVM会抛出异常,我们可以立即纠正它。在这里是我对其他问题的答案及图片。


0
在C#中,您可以通过错误地定义对象属性以不同的方式实现堆栈溢出。 例如:
private double hours;

public double Hours
        {
            get { return Hours; }
            set { Hours = value; }
        }

正如您所看到的,它将永远返回大写的“H”小时,这本身将返回小时,依此类推。

堆栈溢出通常也会发生在内存耗尽或使用托管语言时,因为您的语言管理器(CLR、JRE)将检测到您的代码被卡在无限循环中。


0

当函数调用被执行且栈已满时,就会发生StackOverflow。

就像ArrayOutOfBoundException一样。它不会破坏任何东西,事实上很可能捕获它并从中恢复。

通常情况下,这是由于无法控制的递归导致的,但也可能是由于函数调用堆栈非常深而引起的。


1
这是错误的。StackOverflowError与ArrayOutOfBoundException完全不同。它甚至不是一个异常!它绝对可以破坏事情。如果您调用某个递归函数,并且它抛出StackOverflowError,则不能对其触及的任何对象的状态的正确性做出任何假设。它可能只完成了操作的一部分。通常,除非您有非常好的理由并知道自己在做什么,否则尝试从中恢复是一个坏主意。 - Cruncher
考虑一个快速排序的堆栈溢出问题。当堆栈溢出时,数组可能没有真正的逻辑顺序。数组已经被破坏。 - Cruncher
5
这是一种比较。SO 并不是 AOOBE,但可以将其与 AOOBE 进行比较,因为基本上您正在尝试向已满的堆栈添加元素。至于事物的损坏,执行会在非常精确的时刻停止,就像其他任何错误一样。它本身不会写入堆中的内容,这也是问题的意思(“您可以安全地假设堆栈没有写入堆吗?”)。 - njzk2
5
@Cruncher: QuickSort引发的StackOverFlowError是否比其他异常更容易导致数组中出现重复和缺失项?例如,包含项的对象根据项的平均值进行比较,其中一个对象没有项并抛出"DivisionByZeroException"异常。无论如何,排序算法要么保持不变量,即在调用比较函数时始终保持数组是一个排列,要么就不保持。 - supercat
1
@Cruncher:你再怎么强调,我仍然不同意。从SO的意图来看,这并不是腐败行为。我再次引用:“你能否安全地假设堆栈没有写入堆?”=>“是的” - njzk2
显示剩余12条评论

-1
但是这引发了两个问题:
1.除了递归,难道没有其他方式导致堆栈溢出吗?
2.StackOverflowError是在JVM实际溢出堆栈之前还是之后发生的?
1.当我们分配大于堆栈限制的大小时(例如int x [10000000];),也可能会发生堆栈溢出。
2.第二个问题的答案是
每个线程都有自己的堆栈,其中包含该线程上执行的每个方法的帧。因此,当前正在执行的方法位于堆栈顶部。对于每个方法调用,都会创建并添加(推送)到堆栈顶部一个新帧。当方法正常返回或在方法调用期间抛出未捕获的异常时,将删除(弹出)该帧。堆栈不直接操作,除了推送和弹出帧对象外,因此帧对象可以分配在堆中,内存不需要连续。
因此,通过考虑线程中的堆栈,我们可以得出结论。

栈可以是动态或固定大小的。如果线程需要比允许的更大的堆栈,则会抛出StackOverflowError。如果线程需要一个新的帧,但没有足够的内存来分配它,则会抛出OutOfMemoryError

您可以在此处获取JVM的描述。


4
不是用Java语言。int x[10000000]被分配在堆上。 - Jeremy Stein
@JeremyStein 我在这里阅读了它这里,没错吧? - eatSleepCode
@JeremyStein int是原始类型,它在堆栈上获得内存。您可以在此处检查http://programmers.stackexchange.com/questions/65281/stack-and-heap-memory-in-java - eatSleepCode
我相信当堆已满时会抛出 OutOfMemoryError。然而,当线程需要额外的帧但没有空间时,会抛出 StackOverflowError。(这在本页面的评论和答案中得到了确认)。 - 11684
@eatSleepCode,roseindia页面有误。那根本就不是合法的Java代码。你需要编写 int[] x = new int[10000000]; - Jeremy Stein

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