如何解决 StackOverflowError

applicationinsight
ai
stackoverflowerror
jvm

#1

在本文中我们讨论 Java 里的 StackOverflowError。抛出这个错误表明应用程序因为深递归导致栈被耗尽了。

StackOverflowErrorVirtualMachineError 的扩展类,VirtualMachineError 表明 JVM 中断或者已经耗尽资源,无法运行。而且,VirtualMachineError 类扩展自 Error 类,这个类用于指出那些应用程序不需捕获的严重问题。因为这些错误是在可能永远不会发生的异常情况下产生,所以方法中没有在它的 throw 语句中声明。

StackOverflowError 从 Java 1.0开始出现。

StackOverflowError 结构

构造函数
* StackOverflowError()
Creates an instance of the StackOverflowError class, setting null as its message.
* StackOverflowError(String s)
Creates an instance of the StackOverflowError class, using the specified string as message. The string argument indicates the name of the class that threw the error.


Java 里的 StackOverflowError

Java 应用程序唤起一个方法调用时就会在调用栈上分配一个栈帧, 这个栈帧包含引用方法的参数,本地参数,以及方法的返回地址。

这个返回地址是被引用的方法返回后程序能够继续执行的执行点。如果没有一个新的栈帧所需空间,Java 虚拟机就会抛出 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 标志来指定线程栈的大小等于 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 中 StackOverflowError

接下来的例子演示类之间有循环关系时的风险:

StackOverflowErrorToStringExample.java:

class A {
    private int aValue;
    private B bInstance = null;
     
    public A() {
        aValue = 0;
        bInstance = new B();
    }
     
    @Override
    public String toString() {
        return "";
    }
}
 
class B {
    private int bValue;
    private A aInstance = null;
     
    public B() {
        bValue = 10;
        aInstance = new A();
    }
     
    @Override
    public String toString() {
        return "";
    }
}
 
public class StackOverflowErrorToStringExample {
    public static void main(String[] args) {
        A obj = new A();
        System.out.println(obj.toString());
    }
}

在这个例子中我们定义了两个类,AB。类 A 包含一个 B 类的实例,同时,类 B 也包含 A 类的一个实例。这样,在两个类之间就有了一个循环依赖。而且,每一个 toString 方法从另一个类引用了相应的 toString 方法,等等,最终结果是抛出 StackOverflowError

执行结果如下所示:

Exception in thread "main" java.lang.StackOverflowError
    at main.java.B.(StackOverflowErrorToStringExample.java:24)
    at main.java.A.(StackOverflowErrorToStringExample.java:9)
    at main.java.B.(StackOverflowErrorToStringExample.java:24)
    at main.java.A.(StackOverflowErrorToStringExample.java:9)
    at main.java.B.(StackOverflowErrorToStringExample.java:24)
    at main.java.A.(StackOverflowErrorToStringExample.java:9)
    ...

如何处理 StackOverflowError

  • 最简单的解决方案是仔细检查输出信息中的栈路径,查明模式重复的代码行号。这些行号对应的代码被递归调用了。确认这些行后,你必须小心的检查你的代码,弄清楚为什么递归永远不结束。
  • 如果你确认递归实现是正确的,为了允许大量的调用,你可以增加栈的大小。依赖于安装的 Java 虚拟机,默认的线程栈大小可能是 512KB 或者 1MB。你可以使用 -Xss 标识来增加线程栈的大小。这个标识即可以通过项目的配置也可以通过命令行来指定。-Xss 参数的格式:

    -Xss<size>[g|G|m|M|k|K]

下载 Eclipse Project

这是一个关于 Java 中 StackOverflowError 的文章

Download
你可以在这里下载这个例子的全部代码:StackOverflowErrorExample.zip.

(编译自:https://examples.javacodegeeks.com/java-basics/exceptions/java-lang-stackoverflowerror-how-to-solve-stackoverflowerror/

OneAPM 为您提供端到端的 Java 应用性能解决方案,我们支持所有常见的 Java 框架及应用服务器,助您快速发现系统瓶颈,定位异常根本原因。分钟级部署,即刻体验,Java 监控从来没有如此简单。

本文未经允许不得转载