什么是StackOverflowError?
首先让我们了解一下 局部 变量和对象是如何储存的。
局部变量储存在 栈 中:
如果你看了这张图片,你应该能够理解事情是如何工作的。
当 Java 应用程序调用一个函数调用时,在调用栈上分配了一个栈框架。栈框架包含被调用方法的参数、局部参数和方法的返回地址。返回地址指定程序执行从那里继续执行,这是在调用的方法返回后。如果没有新的栈框架空间,Java 虚拟机 (JVM) 将抛出 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
标志进行的示例执行,该标志指定线程堆栈的大小为 1 MB:
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
。这个例子是如何递归可能会导致问题的一个很好的例子,如果不小心实现。
如何处理 StackOverflowError
-
最简单的解决方案是仔细检查堆栈跟踪并检测行号的重复模式。这些行号表示被递归调用的代码。一旦检测到这些行,您必须仔细检查代码并了解为什么递归永远不会终止。
-
如果您已经验证了递归的正确实现,您可以增加堆栈的大小,以允许更多的调用。根据安装的 Java 虚拟机 (JVM) ,默认线程堆栈大小可能等于 512 KB 或 1 MB。您可以使用
-Xss
标志增加线程堆栈大小。可以通过项目的配置或通过命令行指定此标志。-Xss
参数的格式为:-Xss
[g|G|m|M|k|K]
参数和局部变量在栈上分配(对于引用类型,对象在堆上,栈中的变量引用堆上的对象)。栈通常位于地址空间的上端,随着使用它而向地址空间的底部移动(即朝向零)。\n\n进程还有一个堆,位于进程的底部。当你分配内存时,该堆可以向地址空间的上端扩展。正如你所看到的,堆和堆可能会“碰撞”(有点像构造板块!)。\n\n堆栈溢出的常见原因是错误的递归调用。通常,当你的递归函数没有正确的终止条件时,它会不停地调用自身。或者当终止条件正确时,可能会出现需要太多递归调用才能实现该条件。\n\n然而,在 GUI 编程中,可能会生成间接递归。例如,你的应用程序可能正在处理绘图消息,处理它们时,可能调用导致系统发送另一个绘图消息的函数。这里你没有显式地调用自己,但操作系统/虚拟机已经为你做了。\n\n为了处理它们,你需要检查你的代码。如果有调用自己的函数,请检查是否有终止条件。如果有,请检查在调用函数时是否至少修改了一个参数,否则递归调用函数将没有任何可见的更改,终止条件也就无用了。同时要注意,你的栈空间在达到有效的终止条件之前可能会耗尽内存,因此确保你的方法可以处理需要更多递归调用的输入值。\n\n如果没有明显的递归函数,请检查是否调用了任何会间接导致你的函数被调用的库函数(如上面的隐式情况)。