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

我到处找都找不到确切的答案。根据文档,Java在以下情况下抛出java.lang.StackOverflowError错误:

当由于应用程序递归太深而发生堆栈溢出时引发。

但这引发了两个问题:

  • 除了通过递归,没有其他方法可以导致堆栈溢出吗?
  • StackOverflowError发生在JVM溢出堆栈之前还是之后?

关于第二个问题:

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

我不想要的答案:

  • 发生StackOverflow是由于糟糕的递归。
  • 当堆遇到堆栈时发生StackOverflow。
92957 次浏览

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

JVM为每个线程的每个堆栈分配了一个给定的内存,如果试图调用一个方法恰好填满了这个内存,JVM就会抛出一个错误。就像你想写一个长度为N的数组的下标N一样,不会发生内存损坏。堆栈不能写入堆。

StackOverflowError之于堆栈,就像OutOfMemoryError之于堆:它只是表明没有更多可用内存。

虚拟机错误描述(§6.3

StackOverflowError: Java虚拟机实现已经耗尽了线程的堆栈空间,通常是因为由于执行程序中的错误,线程正在执行无限数量的递归调用。

StackOverflow发生在函数调用时,堆栈已满。

就像一个ArrayOutOfBoundException。它不会腐蚀任何东西,事实上很有可能抓住它并从中恢复。

它通常是由于不受控制的递归导致的,但也可能是由于调用了非常深的函数堆栈而导致的。

除了通过递归,没有其他方法可以导致堆栈溢出吗?

当然。只需要继续调用方法,而不返回。不过,您将需要很多方法,除非您允许递归。实际上,这并没有什么区别:一个堆栈框架就是一个堆栈框架,不管它是不是递归方法之一都是一样的。

第二个问题的答案是:当JVM试图为下一个调用分配堆栈帧时,发现不可能分配堆栈帧,就会检测到stackoverflow。所以,没有东西会被覆盖。

没有“StackOverFlowException”。你的意思是“StackOverFlowError”。

是的,如果你抓住它,你可以继续工作,因为当你这样做的时候,堆栈被清除了,但这将是一个糟糕和丑陋的选择。

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

  • 不,这是得到错误的唯一方法:使堆栈满。但不仅通过递归,还调用无限调用其他方法的方法。这是一个非常特殊的错误,所以不是。
  • 它在堆栈满之前抛出,恰好在您验证它的时候抛出。如果没有可用空间,您将把数据放在哪里?凌驾他人之上?没有。

是否有其他方式导致堆栈溢出 通过递归?< / p >

接受挑战:)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 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%的重复。这和你第二个问题有关。

是否在JVM溢出之前发生StackOverflowError 叠完还是之后?< / p >

因此,在OpenJDK 20.0-b12的情况下,我们可以看到JVM首先爆发了。但这似乎是一个bug,也许有人可以在评论中确认,因为我不确定。我应该报告吗?也许它已经在一些更新的版本中修复了……根据JVM规范link(在注释中由JB Nizet给出),JVM应该抛出StackOverflowError,而不是die:

如果线程中的计算需要一个更大的Java虚拟机 Java虚拟机抛出一个 StackOverflowError . < / p >


第三次尝试。

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


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

我们想要创建一个新的Test对象。因此,它的(隐式)构造函数将被调用。但是,在此之前,Test的所有成员都被初始化了。因此,Test test = new Test()首先执行…

我们想要创建一个新的Test对象…

更新:运气不好,这是递归,我问了关于在这里的问题。

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

此外,每个运行的线程都有自己的堆栈,并获得分配给该堆栈的一定数量的内存。

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

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

因为每个线程的堆栈大小是固定的(注意Java规范不要求固定的大小,但在编写时大多数JVM实现使用固定的大小),而且因为每当进行方法调用时都需要堆栈上的空间,所以希望现在应该清楚为什么它会耗尽以及什么会导致它耗尽。方法调用没有固定的数量,递归没有任何特定的东西,当你试图调用一个方法时,你会得到异常,但没有足够的内存。

当然,堆栈的大小设置得足够高,以至于在常规代码中不太可能发生这种情况。在递归代码中,递归很容易深入到很深的地方,这时你就会遇到这个错误。

但这引发了两个问题:

  1. 除了通过递归,没有其他方法可以导致堆栈溢出吗?
  2. StackOverflowError发生在JVM溢出堆栈之前还是之后?
  1. 当我们分配的大小大于栈的限制时(例如。int x[10000000];)。

  2. 第二个答案是

每个线程都有自己的堆栈,为在该线程上执行的每个方法保存一个框架。当前正在执行的方法在堆栈的顶部。对于每次方法调用,都会创建一个新帧并将其添加(压入)到堆栈的顶部。当方法正常返回或在方法调用期间抛出未捕获的异常时,帧将被删除(弹出)。栈不直接被操作,除了推和弹出帧对象,因此帧对象可以在堆中分配,内存不需要连续。

因此,通过考虑线程中的堆栈,我们可以得出结论。

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

你可以得到JVM 在这里的描述

StackOverflowError发生是因为应用程序递归太深(这不是你期望的答案)。

现在,StackOverflowError还会发生另一件事,就是从方法中不断调用方法,直到你得到StackOverflowError,但没有人能够编程得到StackOverflowError,即使那些程序员这样做了,他们也没有遵循每个程序员在编程时都必须理解的圈complixity编码标准。这样的原因'StackOverflowError'将需要很多时间来纠正它。

但不知不觉地编写一行或两行导致StackOverflowError的代码是可以理解的,JVM抛出了它,我们可以立即纠正它。在这里是我的回答与其他一些问题的图片。

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

例如:

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

在Java中:

在堆和堆栈的内存中有two区域。stack memory用于存储局部变量和函数调用,而heap memory用于存储Java中的对象。

如果堆栈中没有剩余内存用于存储函数调用或局部变量,JVM将抛出java.lang.StackOverFlowError

而如果没有更多的堆空间来创建对象,JVM将抛出java.lang.OutOfMemoryError

在c#中,你可以通过错误地定义对象属性来实现堆栈溢出。 例如:

private double hours;


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

如你所见,它会一直返回带有大写H的Hours, H本身也会返回Hours,等等。

堆栈溢出也经常发生,因为内存不足,或者当使用托管语言时,因为您的语言管理器(CLR, JRE)将检测到您的代码陷入无限循环。