Java 如何预测递归方法的最大调用深度?
为了估计递归方法在给定内存量下可能达到的最大调用深度,在可能发生堆栈溢出错误之前,计算所用内存的(近似)公式是什么 编辑: 许多人的回答是“视情况而定”,这是合理的,因此让我们使用一个简单但具体的例子来删除一些变量:Java 如何预测递归方法的最大调用深度?,java,memory,recursion,jvm,stack-overflow,Java,Memory,Recursion,Jvm,Stack Overflow,为了估计递归方法在给定内存量下可能达到的最大调用深度,在可能发生堆栈溢出错误之前,计算所用内存的(近似)公式是什么 编辑: 许多人的回答是“视情况而定”,这是合理的,因此让我们使用一个简单但具体的例子来删除一些变量: public static int sumOneToN(int n) { return n < 2 ? 1 : n + sumOneToN(n - 1); } 公共静态int-sumOneToN(int-n){ 返回n
public static int sumOneToN(int n) {
return n < 2 ? 1 : n + sumOneToN(n - 1);
}
公共静态int-sumOneToN(int-n){
返回n<2?1:n+sumOneToN(n-1);
}
很容易看出,在我的EclipseIDE中运行它会使n
爆炸性地接近1000(对我来说低得出奇)。
在不执行的情况下,是否可以估计此调用深度限制
编辑:我忍不住认为Eclipse有一个固定的最大调用深度1000,因为我得到了
998
,但是有一个用于main,还有一个用于方法的初始调用,总共使1000
。这是一个“太圆”的数字,不可能是巧合。我会进一步调查的。我只需要Dux开销-Xss vm参数;这是最大堆栈大小,因此Eclipse runner必须在某个地方设置-Xss1000
只有部分答案:从,堆栈帧可以在堆上分配,并且堆栈大小可能是动态的。我不能确定,但似乎应该可以让堆栈大小仅受堆大小的限制:
由于Java虚拟机堆栈除了用于推送和弹出帧之外从不直接操作,因此帧可能是堆分配的
及
该规范允许Java虚拟机堆栈
一个固定的大小或大小,可以根据
计算。如果Java虚拟机堆栈的大小固定,
可以选择每个Java虚拟机堆栈的大小
在创建堆栈时独立运行
Java虚拟机实现可以为程序员或
用户控制Java虚拟机堆栈的初始大小,
在动态扩展或收缩Java的情况下
虚拟机堆栈,控制最大和最小大小
因此,这将取决于JVM的实现。答案是,这取决于具体情况 首先,可以更改Java堆栈的大小
其次,给定方法的堆栈帧大小可以根据不同的变量而变化。您可以查看的调用堆栈帧大小部分了解更多详细信息。取决于您的系统体系结构(32或64位地址)、本地变量的数量和方法的参数。 如果没有开销,如果编译器将其优化为循环
你看,没有简单的答案。这显然是JVM,也可能是特定于架构的 我测量了以下各项:
static int i = 0;
public static void rec0() {
i++;
rec0();
}
public static void main(String[] args) {
...
try {
i = 0; rec0();
} catch (StackOverflowError e) {
System.out.println(i);
}
...
}
使用
Java(TM) SE Runtime Environment (build 1.7.0_09-b05)
Java HotSpot(TM) 64-Bit Server VM (build 23.5-b02, mixed mode)
在x86上运行
对于20MB Java堆栈(-Xss20m
),摊销成本在每次调用16-17字节左右波动。我见过的最低值是16.15字节/帧。因此,我得出结论,成本是16字节,其余是其他(固定)开销
采用单个int
的函数的成本基本相同,为16字节/帧
有趣的是,一个需要10个整数的函数需要32字节/帧。我不知道为什么成本这么低
以上结果在代码经过JIT编译后适用。在编译之前,每帧的成本要高得多。我还没有找到可靠的估计方法但是,这意味着您不可能可靠地预测最大递归深度,除非您能够可靠地预测递归函数是否已JIT编译。
所有这些都是使用128K和8MB的
ulimit
堆栈大小进行测试的。两种情况下的结果都是一样的。你可以走经验道路,用你的代码和-Xss
设置进行实验。有关更多信息,请参见此处:添加到NPE答案:
最大堆栈深度似乎是灵活的。以下测试程序打印了大量不同的数字:
public class StackDepthTest {
static int i = 0;
public static void main(String[] args) throws Throwable {
for(int i=0; i<10; ++i){
testInstance();
}
}
public static void testInstance() {
StackDepthTest sdt = new StackDepthTest();
try {
i=0;
sdt.instanceCall();
} catch(StackOverflowError e){}
System.out.println(i);
}
public void instanceCall(){
++i;
instanceCall();
}
}
我使用了此JRE的默认设置:
java version "1.7.0_09"
OpenJDK Runtime Environment (IcedTea7 2.3.3) (7u9-2.3.3-0ubuntu1~12.04.1)
OpenJDK 64-Bit Server VM (build 23.2-b09, mixed mode)
所以结论是:如果你有足够的(即超过两次),你会有第二次机会;-) (这似乎取决于JVM,可能还取决于架构。)我猜这只是局部变量+帧指针+返回地址。我还认为,如果不测量递归,就不可能进行估计。递归与此无关(除非它是生成真正深层调用堆栈的最常用方法)。为了确定调用深度限制,递归调用与任何其他类型的调用没有区别。(当然,编译器可能会消除尾部递归,在这种情况下,递归会让你的问题分心。)这被标记为“java”,因此我们假设我们在这里讨论的是JVM。@RyanStewart有32位和64位JVM,但JVM中的内存工作原理是一样的。JVM中也没有尾部递归,我也不知道有哪种java编译器会进行这样的优化。因此,在我看来,你一定在谈论别的东西。@Ryan Stewart:我刚刚在google上搜索了一下,很震惊地发现java还没有尾部递归优化。但可能是在将来。堆栈帧可能是堆分配的,因此帧大小可能无关,取决于。你是如何测量递归深度的?他/她可以使用静态成员字段轻松测量递归深度,该字段在每次调用时递增。在64位系统上,每个堆栈帧大约16字节看起来像一个返回地址+一个额外地址(帧指针?
这是一个
指针(从技术上讲,它是null
,并非不存在)?)@Bohemian:我已经扩展了答案(昨晚没有时间这么做)。@JanDvorak:我想根本就不会有这个。我只能猜第二个8字节是用来做什么的。很可能是某种指针,真的吗
java version "1.7.0_09"
OpenJDK Runtime Environment (IcedTea7 2.3.3) (7u9-2.3.3-0ubuntu1~12.04.1)
OpenJDK 64-Bit Server VM (build 23.2-b09, mixed mode)