C# 如何捕获堆栈溢出异常

135

我有一个递归调用的方法,会抛出堆栈溢出异常。第一次调用被try catch块包围,但异常未被捕获。

堆栈溢出异常是否有特殊的行为?我能否正确地捕获/处理异常?

不确定是否相关,但需要注意以下信息:

  • 异常并没有在主线程中抛出

  • 代码抛出异常的对象是由Assembly.LoadFrom(...).CreateInstance(...)手动加载的


3
@RichardOD,当然我会修复这个bug,因为它是一个bug。不过这个问题可能会以不同的方式出现,我想要处理它。 - Toto
8
同意,堆栈溢出是一个严重的错误,因为它不应该被捕获。而应该修复有问题的代码。 - Ian Kemp
12
如果想设计一个递归下降解析器,不要限制深度超出主机所需的人为限制,应该怎么做?如果我可以选择,会有一个StackCritical异常,可以被显式地捕获,在还剩一点堆栈空间时就会触发;它会自行禁用,直到实际抛出异常,并且在安全量的堆栈空间仍然存在之前,不能再次被捕获。 - supercat
3
这个问题很有用--我希望在堆栈溢出异常发生时通过单元测试失败来检测--但NUnit只是将测试移动到“已忽略”类别,而不像其他异常一样使其失败--我需要捕获它并执行Assert.Fail。那么严肃地说--我们该如何解决这个问题? - BrainSlugs83
10个回答

119

从2.0版本开始,只有在以下情况下才能捕获堆栈溢出异常:

  1. CLR正在运行托管环境*,其中主机明确允许处理堆栈溢出异常。
  2. 堆栈溢出异常是由用户代码抛出的,并非由于实际的堆栈溢出情况导致的(参考)。

*“托管环境”指“我的代码托管CLR并配置CLR的选项”而不是“我的代码在共享主机上运行”


35
如果在任何相关的情景下都无法捕获它,为什么还要存在 StackoverflowException 对象? - Manu
11
@Manu 至少有两个原因。1)它可以在1.1中被捕获,因此具有目的性。2)如果您正在托管CLR,则仍然可以捕获它,因此它仍然是有效的异常类型。 - JaredPar
3
为什么 Windows 事件解释发生了什么事情时默认不包括完整的堆栈跟踪信息?如果无法捕获... - user645280
12
如何在托管环境中处理StackOverflowExceptions?我之所以问这个问题,是因为我运行的是托管环境,而且我遇到了这个确切的问题,它会破坏整个应用程序池。我更希望它中止线程,然后可以将其解开并返回到顶部,然后我可以记录错误并继续进行,而不必杀死所有应用程序池的线程。 - Brain2000
从2.0开始...我很好奇,是什么阻止了他们捕捉SO,而在1.1中又是如何实现的(你在评论中提到过)? - M.kazem Akhgary
@Brain2000 这里也有同样的需求。 - Weipeng

52

正确的方法是修复溢出,但是....

您可以为自己分配更大的堆栈:

using System.Threading;
Thread T = new Thread(threadDelegate, stackSizeInBytes);
T.Start();

您可以使用System.Diagnostics.StackTrace FrameCount属性来计算已使用的帧数,并在达到帧限制时抛出自己的异常。或者,您可以计算剩余堆栈的大小,并在其低于阈值时抛出自己的异常:
class Program
{
    static int n;
    static int topOfStack;
    const int stackSize = 1000000; // Default?

    // The func is 76 bytes, but we need space to unwind the exception.
    const int spaceRequired = 18*1024; 

    unsafe static void Main(string[] args)
    {
        int var;
        topOfStack = (int)&var;

        n=0;
        recurse();
    }

    unsafe static void recurse()
    {
        int remaining;
        remaining = stackSize - (topOfStack - (int)&remaining);
        if (remaining < spaceRequired)
            throw new Exception("Cheese");
        n++;
        recurse();
    }
}

只需要抓住奶酪。 ;)


55
“Cheese”这个词并不具体。我会选择“throw new CheeseException("Gouda");”。 - C.Evenhuis
17
@C.Evenhuis,虽然毫无疑问高达奶酪是一种非常出色的奶酪,但实际上它应该是一个RollingCheeseException("双倍格洛斯特")。详情请参见http://www.cheese-rolling.co.uk/。 - user159335
4
  1. 修复不可能,因为如果没有捕捉到,通常不知道它发生在哪里。
  2. 无限递归的情况下,增加堆栈大小是无用的。
  3. 在正确的位置检查堆栈就像第一个。
- Firo
6
但是我不耐乳糖。 - brianc

46

从关于StackOverflowException的MSDN页面中可以得知:

在之前版本的.NET Framework中,应用程序可以捕获StackOverflowException对象(例如,为了从无限递归中恢复)。然而,目前不鼓励这种做法,因为需要编写更多代码来可靠地捕获堆栈溢出异常并继续程序执行。

从.NET Framework 2.0开始,无法使用try-catch块捕获StackOverflowException对象,默认情况下对应的进程会被终止。因此,建议用户编写代码以检测和防止堆栈溢出。例如,如果应用程序依赖于递归,请使用计数器或状态条件来终止递归循环。注意,托管公共语言运行时(CLR)的应用程序可以指定CLR卸载发生堆栈溢出异常的应用程序域,并让相应进程继续运行。有关更多信息,请参阅ICLRPolicyManager接口和托管公共语言运行时。


31

正如其他用户所说,你无法捕获异常。 但是,如果你正在努力找出异常发生的位置,可以配置Visual Studio在异常抛出时中断。

要做到这一点,需要从“调试”菜单中打开“异常设置”。 在较旧版本的Visual Studio中,这位于“调试” - “异常”;在较新版本中,它位于“调试” - “Windows” - “异常设置”。

打开设置后,展开“公共语言运行时异常”,展开“系统”,向下滚动并选中“System.StackOverflowException”。然后您可以查看调用堆栈并查找重复调用模式。 这应该让您知道在哪里修复导致堆栈溢出的代码。


1
在VS 2015中,调试-异常在哪里? - FrenkyB
2
调试 - 窗口 - 异常设置 - Simon

17

多次提到过,由于进程状态损坏导致的 StackOverflowException 是无法被 System 捕获的。但有一种方法可以将其作为事件来检测:

  http://msdn.microsoft.com/en-us/library/system.appdomain.unhandledexception.aspx    

从 .NET Framework 4 开始,仅当事件处理程序具有 HandleProcessCorruptedStateExceptionsAttribute 属性并且是安全关键的时,才会对破坏进程状态的异常(如堆栈溢出或访问冲突)引发此事件。

然而,在事件函数结束后,你的应用程序将终止(非常低劣的解决方法是在此事件中重新启动应用程序,哈哈,从未尝试过且永远不会尝试)。但它足够好用于记录日志!

 

在 .NET Framework 版本 1.0 和 1.1 中,除了主应用程序线程外的其他线程中发生的未处理异常会被运行时捕获,因此不会导致应用程序终止。因此,可能会引发 UnhandledException 事件而不会导致应用程序终止。从 .NET Framework 版本 2.0 开始,删除了子线程中未处理异常的后备,因为这种静默失败的累积效应包括性能降低、数据损坏和锁定,所有这些都很难调试。有关更多信息,包括一些运行时不会终止的情况,请参阅托管线程中的异常。


7

从CLR 2.0开始,默认情况下StackOverflowException会终止进程。 - Brian Rasmussen
不。你可以捕获OOM,在某些情况下这样做可能是有意义的。我不知道你所说的线程消失是什么意思。如果一个线程有一个未处理的异常,CLR将终止进程。如果你的线程完成了它的方法,它会被清理掉。 - Brian Rasmussen

6

正如大部分帖子所解释的那样,你不能这样做,我再补充一个方面:

在许多网站上,您会发现有人说避免这种情况的方法是使用不同的AppDomain,这样如果发生这种情况,该域将被卸载。但这是绝对错误的(除非您托管 CLR),因为CLR的默认行为会引发KillProcess事件,关闭您的默认AppDomain。


6

你不行,CLR 不会让你这么做。栈溢出是致命错误,无法从中恢复。


那么,如果这个异常无法被捕获,而是导致单元测试运行器崩溃,你该如何使单元测试失败呢? - BrainSlugs83
1
@BrainSlugs83。这是一个愚蠢的想法,你不需要这样做。为什么要测试代码是否会因为StackOverflowException而失败呢?如果CLR更改后可以处理更深的堆栈,那会发生什么?如果您在已经有深度嵌套堆栈的地方调用单元测试函数会发生什么?这似乎是无法测试的东西。如果您想手动抛出异常,请选择更适合此任务的异常。 - Matthew Scharley

4

这是不可能的,也有很好的原因(例如,请考虑所有那些catch(Exception){})。

如果您想在堆栈溢出后继续执行代码,请在不同的AppDomain中运行危险代码。CLR策略可以设置为在溢出时终止当前AppDomain而不影响原始域。


2
“catch”语句不会真正成为问题,因为当catch语句执行时,系统会回滚尝试使用过多堆栈空间的任何操作的影响。 捕获堆栈溢出异常并不一定是危险的。 它们无法被捕获的原因是,安全地允许它们被捕获需要向使用堆栈的所有代码添加一些额外开销,即使它没有溢出。 - supercat
4
有些时候这个声明并不是经过深思熟虑的。如果你无法捕捉到Stackoverflow,那么你可能永远不知道它在生产环境中发生了什么地方。 - Offler

1
你可以使用 RuntimeHelpers.EnsureSufficientExecutionStack 方法 msdn 在执行方法之前检查堆栈。
using System.Runtime.CompilerServices;

class Program
{
   static void Main()
   {
      try
      {
          Recursion();
      }
      catch (InsufficientExecutionStackException e)
      {
      }
   }

   static void Recursion()
   {
       RuntimeHelpers.EnsureSufficientExecutionStack();
       Recursion();
   }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接