什么是堆栈溢出错误?

whhtz7ly  于 2021-07-08  发布在  Java
关注(0)|答案(11)|浏览(525)

什么是 StackOverflowError ,是什么原因造成的,我该如何处理?

yshpjwxd

yshpjwxd1#

为了描述这一点,首先让我们了解局部变量和对象是如何存储的。
局部变量存储在堆栈中:

如果你看了图片,你应该能够理解事情是如何运作的。
当java应用程序调用函数调用时,会在调用堆栈上分配一个堆栈帧。堆栈框架包含被调用方法的参数、其本地参数和方法的返回地址。返回地址表示执行点,在调用的方法返回后,程序将从该点继续执行。如果没有新堆栈帧的空间,则 StackOverflowError 由java虚拟机(jvm)抛出。
可能耗尽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 将被抛出。这个例子是一个非常好的例子,说明了如果不谨慎地实现递归,它会导致问题。
如何处理堆栈溢出错误
最简单的解决方案是仔细检查堆栈跟踪并检测行号的重复模式。这些行号表示递归调用的代码。一旦检测到这些行,就必须仔细检查代码并理解为什么递归永远不会终止。
如果您已经验证了递归的实现是正确的,那么您可以增加堆栈的大小,以便允许更多的调用。根据安装的java虚拟机(jvm),默认线程堆栈大小可能等于512kb或1mb。可以使用 -Xss 旗帜。可以通过项目的配置或命令行指定此标志。文件的格式 -Xss 参数是: -Xss<size>[g|G|m|M|k|K]

irlmq6kh

irlmq6kh2#

堆栈溢出的最常见原因是太深或无限递归。如果这是您的问题,那么本关于java递归的教程可以帮助您理解这个问题。

u1ehiz5o

u1ehiz5o3#

堆栈溢出通常是通过嵌套太深的函数调用来调用的(特别是在使用递归时,即调用自身的函数)或在堆栈上分配大量内存(使用堆更合适)。

e5njpo68

e5njpo684#

如果你有这样一个函数:

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

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

zengzsys

zengzsys5#

就像你说的,你需要展示一些代码。:-)
当函数调用嵌套太深时,通常会发生堆栈溢出错误。请参阅stack overflow code golf thread以获取有关如何发生这种情况的一些示例(尽管在该问题中,答案故意导致堆栈溢出)。

smdnsysy

smdnsysy6#

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)

在上述情况下,可以通过进行编程更改来避免这种情况。但是如果程序逻辑是正确的并且仍然发生,那么堆栈大小需要增加。

kg7wmglp

kg7wmglp7#

下面是一个用于反转单链表的递归算法的示例。在具有以下规格(4g内存、intel core i5 2.3ghz cpu、64位windows 7)的笔记本电脑上,对于大小接近10000的链表,此函数将遇到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);
}
w51jfk4q

w51jfk4q8#

参数和局部变量在堆栈上分配(对于引用类型,对象位于堆上,堆栈中的变量引用堆上的对象)。堆栈通常位于地址空间的上端,当它用完时,它会朝向地址空间的底部(即朝向零)。
您的进程还有一个堆,它位于进程的底部。当您分配内存时,这个堆可以向地址空间的上端增长。如您所见,堆有可能与堆“碰撞”(有点像板块构造!!!)。
堆栈溢出的常见原因是错误的递归调用。通常,这是因为递归函数没有正确的终止条件,所以它最终会永远调用自己。或者当终止条件很好时,可能是因为在实现之前需要太多的递归调用。
但是,通过gui编程,可以生成间接递归。例如,您的应用程序可能正在处理绘制消息,并且在处理这些消息时,它可能会调用导致系统发送另一条绘制消息的函数。这里您没有显式地调用自己,但是os/vm已经为您完成了。
要处理它们,需要检查代码。如果你有自己调用的函数,那么检查你是否有终止条件。如果有,那么检查调用函数时是否至少修改了一个参数,否则递归调用的函数将没有可见的更改,终止条件也没有用。还要注意,在达到有效的终止条件之前,堆栈空间可能会耗尽内存,因此请确保方法能够处理需要更多递归调用的输入值。
如果没有明显的递归函数,那么检查是否调用了任何间接导致函数被调用的库函数(如上面的隐式情况)。

o4tp2gmn

o4tp2gmn9#

堆栈的空间限制取决于操作系统,正常大小是8mb(在ubuntu中,您可以使用 $ ulimit -u 也可以在其他操作系统中进行检查(类似地)。任何程序都会在运行时使用堆栈,但要完全知道何时使用堆栈,需要检查汇编语言。例如,在x86\u 64中,堆栈用于:
进行过程调用时保存返回地址
保存局部变量
保存特殊寄存器以便以后恢复
向过程调用传递参数(多于6个)
其他:随机未使用的堆栈基、加那利值、填充。。。等
如果您不知道x86_64(正常情况),您只需要知道您使用的特定高级别编程语言何时编译这些操作。例如在c中:
(1) → 函数调用
(2) → 函数调用中的局部变量(包括main)
(3) → 函数调用中的局部变量(非main)
(4) → 函数调用
(5) → 通常是函数调用,它通常与堆栈溢出无关。
因此,在c中,只有局部变量和函数调用使用堆栈。使堆栈溢出的两种(独特的?)方法是:
在main或其调用的任何函数中声明过大的局部变量( int array[10000][10000]; )
非常深或无限的递归(同时调用太多函数)。
为了避免 StackOverflowError 你可以:
检查局部变量是否太大(1 mb的顺序)→ 使用堆(malloc/calloc调用)或全局变量。
检查inf

gpfsuwkq

gpfsuwkq10#

堆栈溢出的意思就是:堆栈溢出。通常在程序中有一个包含局部作用域变量和地址的堆栈,当例程执行结束时,这些变量和地址将返回。堆栈往往是内存中某个位置的固定内存范围,因此它可以包含多少值是有限的。
如果堆栈是空的,你不能弹出,如果你这样做,你会得到堆栈下溢错误。
如果堆栈已满,则无法推送,否则将出现堆栈溢出错误。
因此,当您在堆栈中分配太多时,就会出现堆栈溢出。例如,在前面提到的递归中。
有些实现优化了某些形式的递归。尤其是尾部递归。尾部递归例程是例程的一种形式,其中递归调用作为例程所做的最后一件事出现。这样的例行呼叫被简化为跳转。
有些实现甚至实现自己的递归堆栈,因此它们允许递归继续,直到系统内存耗尽。
如果可以的话,最简单的方法就是增加堆栈大小。如果您不能做到这一点,那么第二个最好的方法就是查看是否有明显的原因导致堆栈溢出。尝试在调用之前和之后打印一些东西到例程中。这有助于你找出失败的例行公事。

lx0bsm1f

lx0bsm1f11#

StackOverflowError 在堆栈中作为 OutOfMemoryError 是一堆。
无限递归调用会导致堆栈空间被耗尽。
下面的示例生成 StackOverflowError :

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

    public static void main(String[] args) 
    {
        unboundedRecursiveCall();
    }
}
``` `StackOverflowError` 如果递归调用是有界的,以防止内存中不完整调用(以字节为单位)的聚合总数超过堆栈大小(以字节为单位),则可以避免。

相关问题