什么是 StackOverflowError
, 它是由什么引起的,我该如何处理它们?
什么是 StackOverflowError
, 它是由什么引起的,我该如何处理它们?
参数和局部变量被分配在栈上(对于引用类型,对象存在于堆上,而栈中的变量引用堆上的对象)。栈通常位于地址空间的上部,随着使用它而逐渐向地址空间的底部移动(即朝零移动)。
您的进程还有一个堆,它位于进程的底部。当您分配内存时,此堆可以向地址空间的上部增长。如您所见,堆有可能与栈“碰撞”(有些像构造板块!)。
堆栈溢出的常见原因是错误的递归调用。通常,这是由于您的递归函数没有正确的终止条件,因此会一直调用自身。或者终止条件没问题,但在满足之前需要进行太多的递归调用,也可能导致其发生。
然而,在 GUI 编程中,可能会产生间接递归。例如,您的应用程序可能正在处理绘制消息,并在处理它们时调用了一个导致系统发送另一个绘制消息的函数。在这种情况下,您没有显式地调用自己,而是由操作系统/虚拟机为您完成。
要处理它们,您需要检查代码。如果您有调用自身的函数,则检查是否有终止条件。如果有,那么请检查在调用函数时是否至少修改了一个参数,否则递归调用的函数将没有可见的变化,并且终止条件将是无用的。还要注意,在达到有效的终止条件之前,您的堆栈空间可能会耗尽内存,因此确保您的方法能够处理需要更多递归调用的输入值。
如果没有明显的递归函数,请检查是否调用了任何库函数,这些库函数间接地会导致调用您的函数(如上述隐式情况)。
为了描述这个问题,首先让我们理解局部变量和对象的存储方式。
局部变量存储在堆栈上:
如果您查看了该图像,则应该能够理解其中的运作方式。
当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<size>[g|G|m|M|k|K]
如果您有一个如下的函数:
int foo()
{
// more stuff
foo();
}
然后foo()会不断地调用自己,越来越深入,当用于跟踪当前函数的空间被占满时,就会出现堆栈溢出错误。
堆栈溢出(Stack Overflow)就是指:一个堆栈超出了它的容量。通常程序中有一个堆栈,其中包含局部作用域变量以及执行完一段例程后返回的地址。这个堆栈往往是内存中的一个固定范围,因此它所能包含的数值是有限的。
如果堆栈为空,则无法弹出元素,否则会出现堆栈下溢错误。
如果堆栈已满,则无法推入元素,否则将出现堆栈溢出错误。
因此,在向堆栈分配过多空间时,就会出现堆栈溢出。例如,在所提到的递归中。
一些实现可以优化掉某些形式的递归,特别是尾递归。尾递归是一种递归形式,其中递归调用作为例程最后执行的事情。这种例程调用只需被简化成跳转。
一些实现甚至实现了自己的递归堆栈,因此它们允许递归继续执行直到系统耗尽内存。
你可以尝试的最简单的方法就是增加堆栈大小。然而,如果你无法这样做,第二个最好的方法就是查找是否有明显引起堆栈溢出的问题。通过在调用例程之前和之后打印一些内容,可以帮助你找到失败的例程。
堆栈溢出通常由于嵌套函数调用过深(在使用递归时尤其容易,即一个函数调用自己)或在堆栈上分配大量内存而应该使用堆更合适。
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)
就像你所说的那样,你需要展示一些代码。 :-)
堆栈溢出错误通常发生在函数调用嵌套过深的情况下。可以查看Stack Overflow Code Golf线程,了解一些导致这种情况发生的示例(尽管在该问题的情况下,答案故意导致堆栈溢出)。
StackOverflowError
(堆栈溢出错误)相当于堆(heap)中的OutOfMemoryError
(内存溢出错误)。
不受限制的递归调用会导致使用完整个堆栈空间。
下面的示例会产生StackOverflowError
:
class StackOverflowDemo
{
public static void unboundedRecursiveCall() {
unboundedRecursiveCall();
}
public static void main(String[] args)
{
unboundedRecursiveCall();
}
}
StackOverflowError
可以避免,只要限制递归调用的深度,防止未完成的内存调用(以字节为单位)总和超过堆栈大小(以字节为单位)即可。
这是一个递归算法反转单向链表的例子。在一台配备4 GB内存、Intel Core i5 2.3 GHz 64位CPU和Windows 7操作系统的笔记本电脑上,当单向链表的大小接近10,000时,该函数会遇到栈溢出错误。
我的观点是我们应该明智地使用递归,始终考虑到系统规模。
通常情况下,递归可以转换为迭代程序,从而更好地扩展。(同样算法的一种迭代版本在页面底部给出。它可以在9毫秒内反转大小为1百万的单向链表。)
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);
}
main
方法)中添加行new Object() {{getClass().newInstance();}};
。 从实例上下文中不起作用(仅抛出InstantiationException
)。 - John McClane