发布评论
评论(14)
正确答案是已经给出的答案。 您可能 a) 代码中存在导致无限递归的错误,这通常很容易诊断和修复,或者 b) 代码可能导致非常深的递归,例如递归遍历不平衡的二叉树。 在后一种情况下,您需要更改代码以不在堆栈上分配信息(即不递归),而是在堆中分配信息。
例如,对于不平衡的树遍历,您可以将需要重新访问的节点存储在 Stack 数据结构中。 对于有序遍历,您将沿着左分支循环,在访问每个节点时推动每个节点,直到遇到要处理的叶子,然后从堆栈顶部弹出一个节点,处理它,然后使用以下命令重新启动循环右子节点(只需将循环变量设置为右节点。)这将通过将堆栈上的所有内容移动到堆栈数据结构中的堆来使用恒定数量的堆栈。 堆通常比堆栈丰富得多。
这通常是一个非常糟糕的主意,但在内存使用受到极大限制的情况下是必要的,您可以使用指针反转。 在这种技术中,您将堆栈编码到您正在遍历的结构中,并且通过重用您正在遍历的链接,您可以在不需要或显着减少额外内存的情况下完成此操作。 使用上面的例子,我们不需要在循环时推送节点,而是只需要记住我们的直接父节点,并且在每次迭代时,我们将遍历的链接设置为当前父节点,然后将当前父节点设置为我们要离开的节点。 当我们到达一片叶子时,我们对其进行处理,然后去找我们的父母,然后我们遇到了一个难题。 我们不知道是否要纠正左分支,处理该节点,然后继续处理右分支,或者纠正右分支并转到父分支。 因此,我们需要在迭代时分配额外的信息。 通常,对于该技术的低级实现,该位将存储在指针本身中,从而导致总体上不需要额外的内存和常量内存。 这在 Java 中不是一个选项,但可以将这一点存储在用于其他用途的字段中。 在最坏的情况下,所需内存量仍会减少至少 32 或 64 倍。 当然,这种算法非常容易出错,结果完全混乱,并且会对并发性造成严重破坏。 因此,除非分配内存无法维持,否则几乎不值得进行维护噩梦。 典型的例子是垃圾收集器,其中类似的算法很常见。
不过,我真正想讨论的是当您可能想要处理 StackOverflowError 时。 即在 JVM 上提供尾调用消除。 一种方法是使用蹦床样式,而不是执行尾部调用,您返回一个空过程对象,或者如果您只是返回一个值,则返回该值。 [注意:这需要某种方法来表示函数返回 A 或 B。在 Java 中,最简单的方法可能是正常返回一种类型并抛出另一种类型作为异常。]需要执行一个 while 循环来调用无效过程(其本身将返回无效过程或值),直到获得值。 无限循环将变成 while 循环,不断强制返回过程对象的过程对象。 蹦床风格的好处是,它只使用比正确消除所有尾部调用的实现多一个常数因子的堆栈,它使用普通的 Java 堆栈来进行非尾部调用,转换简单,并且只增加通过(繁琐的)常数因子编写代码。 缺点是您在每个方法调用上分配一个对象(这将立即变成垃圾),并且消耗这些对象涉及每个尾调用的几次间接调用。
理想的做法是从一开始就不分配那些无效过程或其他任何东西,这正是尾调用消除所要实现的目标。 不过,使用 Java 提供的功能,我们可以做的是正常运行代码,并且仅在堆栈耗尽时才执行这些无效过程。 现在我们仍然分配那些无用的帧,但我们在堆栈而不是堆上进行分配,并批量释放它们,而且,我们的调用是正常的直接 Java 调用。 描述这种转换的最简单方法是首先将所有多调用语句方法重写为具有两个调用语句的方法,即 fgh() { f(); G(); H(); } 变成 fgh() { f(); gh(); } 和 gh(){ g(); H(); }。 为简单起见,我假设所有方法都以尾部调用结束,可以通过将方法的其余部分打包到单独的方法中来安排尾部调用,但实际上,您希望直接处理这些方法。 在这些转换之后,我们有三种情况,要么一个方法有零次调用,在这种情况下没有什么可做的,要么它有一个(尾部)调用,在这种情况下,我们将它包装在一个 try-catch 块中,就像我们将在两次调用情况下的尾部调用。 最后,它可能有两个调用,一个非尾部调用和一个尾部调用,在这种情况下,我们应用示例中说明的以下转换(使用 C# 的 lambda 表示法,可以轻松地用具有一定增长的匿名内部类替换)
// top-level handler
Action tlh(Action act) {
return () => {
while(true) {
try { act(); break; } catch(Bounce e) { tlh(() => e.run())(); }
}
}
}
gh() {
try { g(); } catch(Bounce e) {
throw new Bounce(tlh(() => {
e.run();
try { h(); } catch(StackOverflowError e) {
throw new Bounce(tlh(() => h());
}
});
}
try { h(); } catch(StackOverflowError e) {
throw new Bounce(tlh(() => h()));
}
}
:这里的主要好处是,如果没有抛出异常,这与我们开始时安装的一些额外异常处理程序的代码相同。 由于尾部调用(h() 调用)不处理 Bounce 异常,因此该异常将飞过它们,从堆栈中展开那些(不必要的)帧。 非尾部调用捕获 Bounce 异常并重新抛出它们并添加剩余的代码。 这会将堆栈一直展开到顶层,消除尾部调用帧,但记住空过程中的非尾部调用帧。 当我们最终执行顶层的 Bounce 异常中的过程时,我们将重新创建所有非尾部调用帧。 此时,如果我们立即再次耗尽堆栈,那么,由于我们不重新安装 StackOverflowError 处理程序,它将不会按预期捕获,因为我们确实已经耗尽堆栈。 如果我们更进一步,将根据需要安装一个新的 StackOverflowError。 此外,如果我们确实取得了进展,但随后再次耗尽堆栈,则重新展开我们已经展开的帧没有任何好处,因此我们安装新的顶级处理程序,以便堆栈只会展开到它们。
这种方法的最大问题是,您可能想要调用普通的 Java 方法,并且调用时堆栈空间可能非常小,因此它们可能有足够的空间来启动但无法完成,并且您无法在中间。 对此至少有两种解决方案。 第一个是将所有此类工作发送到一个单独的线程,该线程将拥有自己的堆栈。 这是非常有效和简单的,并且不会引入任何并发性(除非您想要它)。另一种选择是在调用任何普通 Java 方法之前通过简单地在调用任何普通 Java 方法之前立即抛出 StackOverflowError 来故意展开堆栈。 如果当你恢复时它仍然耗尽堆栈空间,那么你一开始就完蛋了。
也可以做类似的事情来及时延续。 不幸的是,这种转换实际上无法在 Java 中手动完成,并且对于 C# 或 Scala 等语言来说可能是边界。 因此,像这样的转换往往是由针对 JVM 的语言完成的,而不是由人完成的。
我想你不能——或者它至少取决于你使用的jvm。 堆栈溢出意味着没有空间来存储局部变量和返回地址。 如果您的 jvm 执行某种形式的编译,那么 jvm 中也会出现 stackoverflow,这意味着您无法处理或捕获它。 jvm 必须终止。
可能有一种方法可以创建允许这种行为的 jvm,但速度会很慢。
我还没有测试过 jvm 的行为,但在 .net 中你无法处理 stackoverflow。 即使尝试捕获也无济于事。 由于 java 和 .net 依赖于相同的概念(带有 jit 的虚拟机),我怀疑 java 的行为会相同。 .NET 中存在 stackoverflow-Exception 表明,可能存在某些 vm 确实使程序能够捕获它,但正常情况则不会。
获得 StackOverflowError 的大多数机会是在递归函数中使用[长/无限]递归。
您可以通过更改应用程序设计以使用可堆叠数据对象来避免函数递归。 有一些编码模式可以将递归代码转换为迭代代码块。 看看下面的答案:
- way-to-go-from-recursion-to -迭代
- can-every-recursion-be-converted-into-迭代
- design-patterns-for-converting-recursive -algorithms-to-iterative-ones
因此,您可以通过使用自己的数据堆栈来避免 Java 对隐性函数调用进行内存堆栈。
在某些情况下,您无法捕获 StackOverflowError。
每当你尝试时,你都会遇到新的。 因为它是Java VM。 找到递归代码块是件好事,就像 Andrew Bullock 所说的。
/*
Using Throwable we can trap any know error in JAVA..
*/
public class TestRecur {
private int i = 0;
public static void main(String[] args) {
try {
new TestRecur().show();
} catch (Throwable err) {
System.err.println("Error...");
}
}
private void show() {
System.out.println("I = " + i++);
show();
}
}
不过,您可以查看以下链接:http://marxsoftware。 blogspot.in/2009/07/diagnosing-and-resolving.html 来理解代码片段,这可能会引发错误
我不确定你所说的“处理”是什么意思。
您当然可以捕获该错误:
public class Example {
public static void endless() {
endless();
}
public static void main(String args[]) {
try {
endless();
} catch(StackOverflowError t) {
// more general: catch(Error t)
// anything: catch(Throwable t)
System.out.println("Caught "+t);
t.printStackTrace();
}
System.out.println("After the error...");
}
}
但这很可能是一个坏主意,除非您确切地知道自己在做什么。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
查看 Raymond Chen 的帖子 调试堆栈溢出时,您需要关注重复的递归部分。 摘录:
Take a look at Raymond Chen's post When debugging a stack overflow, you want to focus on the repeating recursive part. An extract: