为什么可以从 StackOverflow 错误恢复?

即使在 Java 中发生了 StackOverflowError之后,仍然可以继续执行,我对此感到惊讶。

我知道 StackOverflowError是 Error 类的子类。 类 Error 被转化为“ Throwable 的一个子类,它指示一个合理的应用程序不应该试图捕捉的严重问题。”

这听起来更像是一个建议而不是一个规则,意味着像 StackOverlowError 那样捕获 Error 实际上是允许的,而不这样做取决于程序员的合理性。看,我测试了这个代码,它正常终止。

public class Test
{
public static void main(String[] args)
{
try {
foo();
} catch (StackOverflowError e) {
bar();
}
System.out.println("normal termination");
}


private static void foo() {
System.out.println("foo");
foo();
}


private static void bar() {
System.out.println("bar");
}
}

怎么会这样?我认为在抛出 StackOverlowError 时,堆栈应该已经满了,没有空间调用另一个函数了。错误处理块是否运行在不同的堆栈中,或者这里发生了什么?

6254 次浏览

When the StackOverflowError is thrown, the stack is full. However, when it's caught, all those foo calls have been popped from the stack. bar can run normally because the stack is no longer overflowing with foos. (Note that I don't think the JLS guarantees you can recover from a stack overflow like this.)

When the stack overflows and StackOverflowError is thrown, the usual exception handling unwinds the stack. Unwinding the stack means:

  • abort the execution of the currently active function
  • delete its stack frame, proceed with the calling function
  • abort the execution of the caller
  • delete its stack frame, proceed with the calling function
  • and so on...

... until the exception is caught. This is normal (in fact, necessary) and independent of which exception is thrown and why. Since you catch the exception outside of the first call to foo(), the thousands of foo stack frames that filled the stack have all been unwound and most of the stack is free to be used again.

When the StackOverFlow occurs, the JVM will pop down to the catch, freeing the stack.

In you example, it get rids of all the stacked foo.

Because the stack doesn't actually overflow. A better name might be AttemptToOverflowStack. Basically what it means is that the last attempt to adjust the stack frame errs because there isn't enough free space left on the stack. The stack could actually have lots of space left, just not enough space. So, whatever operation would have depended upon the call succeeding (typically a method invocation), never gets exectued and all that is left is for the program to deal with that fact. Which means that it is really no different from any other exception. In fact, you could catch the exception in the function that is making the call.

As has already been answered, it is possible to execute code, and in particular to call functions, after catching a StackOverflowError because the normal exception handling procedure of the JVM unwinds the stack between the throw and the catch points, freeing stack-space for you to use. And your experiment confirms that is the case.

However, that is not quite the same as saying that it is, in general, possible to recover from a StackOverflowError.

A StackOverflowError IS-A VirtualMachineError, which IS-AN Error. As you point out, Java provides some vague advice for an Error:

indicates serious problems that a reasonable application should not try to catch

and you, reasonably, conclude that should sounds like catching an Error might be OK in some circumstances. Note that performing one experiment does not demonstrate that something is, in general, safe to do. Only the rules of the Java language and the specifications of the classes you use can do that. A VirtualMachineError is a special class of exception, because the Java Language Specification and the Java Virtual Machine Specification provide information about the semantics of this exception. In particular, the latter says:

A Java Virtual Machine implementation throws an object that is an instance of a subclass of the class VirtualMethodError when an internal error or resource limitation prevents it from implementing the semantics described in this chapter. This specification cannot predict where internal errors or resource limitations may be encountered and does not mandate precisely when they can be reported. Thus, any of the VirtualMethodError subclasses defined below may be thrown at any time during the operation of the Java Virtual Machine:

...

  • StackOverflowError: The Java Virtual Machine implementation has run out of stack space for a thread, typically because the thread is doing an unbounded number of recursive invocations as a result of a fault in the executing program.

The crucial problem is that you "cannot predict" where or when a StackOverflowError will be thrown. There are no guarantees about where it will not be thrown. You can not rely on it being thrown on entry to a method, for example. It could be thrown at a point within a method.

This unpredictability is potentially disastrous. As it can be thrown within a method, it could be thrown part way through a sequence of operations that the class considers to be one "atomic" operation, leaving the object in a partially modified, inconsistent, state. With the object in an inconsistent state, any attempt to use that object could result in erroneous behaviour. In all practical cases you can not know which object is in an inconsistent state, so you have to assume that no objects are trustworthy. Any recovery operation or attempt to continue after the exception is caught could therefore have erroneous behaviour. The only safe thing to do is therefore to not catch a StackOverflowError, but rather to allow the program to terminate. (In practice you might attempt to do some error logging to assist troubleshooting, but you can not rely on that logging operating correctly). That is, you can not reliably recover from a StackOverflowError.