Math.Random中的StackOverflowError是一种随机递归方法

时间:2013-08-08 12:14:10

标签: java algorithm

这是我的计划的背景。

一个函数有50%的几率无所事事,50%有两次自我调用。 该计划完成的概率是多少?

我写了这段代码,显然效果很好。对每个人来说可能并不明显的答案是,该计划有100%的机会完成。但是当我运行这个程序时,会出现一个StackOverflowError(有多方便;)),在Math.Random()中出现。有人能指出我它来自哪里,并告诉我,如果我的代码可能错了吗?

static int bestDepth =0;
static int numberOfPrograms =0;
@Test
public void testProba(){
   for(int i = 0; i <1000; i++){
       long time = System.currentTimeMillis();
       bestDepth = 0;
       numberOfPrograms = 0;
       loop(0);
       LOGGER.info("Best depth:"+ bestDepth +" in "+(System.currentTimeMillis()-time)+"ms");
   }
}

public boolean loop(int depth){
    numberOfPrograms++;
    if(depth> bestDepth){
        bestDepth = depth;
    }
    if(proba()){
        return true;
    }
    else{
        return loop(depth + 1) && loop(depth + 1);
    }
}

public boolean proba(){
    return Math.random()>0.5;
}

java.lang.StackOverflowError
at java.util.Random.nextDouble(Random.java:394)
at java.lang.Math.random(Math.java:695)

。 我怀疑堆栈及其中的功能数量是有限的,但我真的没有看到这里的问题。

任何建议或线索显然都是受欢迎的。

法比安

编辑:感谢您的回答,我使用java -Xss4m运行它并且效果很好。

4 个答案:

答案 0 :(得分:14)

无论何时调用函数或创建非静态变量,堆栈都用于为其放置和保留空间。

现在,您似乎在递归调用loop函数。这将参数放在堆栈中,以及代码段和返回地址。这意味着很多信息都被放在堆栈上。

然而,堆栈是有限的。 CPU具有内置的机制,可以防止数据被压入堆栈的问题,并最终覆盖代码本身(随着堆栈的增长)。这称为General Protection Fault。发生一般保护故障时,操作系统会通知当前正在运行的任务。因此,发起Stackoverflow

这似乎发生在Math.random()

为了解决您的问题,我建议您使用Java的-Xss选项增加堆栈大小。

答案 1 :(得分:4)

正如你所说,loop函数以递归方式调用自身。现在,编译器可以将tail recursive calls重写为循环,而不占用任何堆栈空间(这称为尾调用优化,TCO)。不幸的是,java编译器不这样做。而且你的loop也不是尾递归的。您的选择是:

  1. 按照其他答案的建议增加堆栈大小。请注意,这只会在时间上进一步推迟问题:无论堆栈有多大,它的大小仍然是有限的。您只需要更长的递归调用链来突破空间限制。
  2. 以循环方式重写函数
  3. 使用language,它具有执行TCO的编译器
    1. 您仍然需要将函数重写为tail-recursive
    2. 或者用trampolines重写它(只需要进行微小的更改)。一篇好文章,解释蹦床并进一步概括它们被称为“Stackless Scala with Free Monads”。
  4. 为了说明3.2中的要点,这里是重写函数的样子:

    def loop(depth: Int): Trampoline[Boolean] = {
      numberOfPrograms = numberOfPrograms + 1
      if(depth > bestDepth) {
        bestDepth = depth
      }
      if(proba()) done(true)
      else for {
        r1 <- loop(depth + 1)
        r2 <- loop(depth + 1)
      } yield r1 && r2
    }
    

    初始通话为loop(0).run

答案 2 :(得分:2)

增加堆栈大小是一个很好的临时修复。但是,正如this post所证明的那样,尽管loop()函数保证最终返回 ,但loop()所需的平均堆栈深度为无穷大< / strong>即可。因此,无论您增加多少堆栈,您的程序最终都会耗尽内存并崩溃。

我们无法阻止这一点;我们总是需要在内存中编码堆栈以某种方式,我们永远不会有无限的内存。但是,有一种方法可以将您使用的内存量减少大约2个数量级。这应该会让你的程序显着更高的返回机会,而不是崩溃。

我们可以注意到,在堆栈的每一层,我们只需要运行您的程序所需的一条信息:告诉我们是否需要再次调用loop()的信息回来后。因此,我们可以使用一堆位来模拟递归。每个模拟的堆栈帧只需要一个的内存(现在需要64-96倍,具体取决于您是以32位还是64位运行)

代码看起来像这样(虽然我现在没有Java编译器所以我无法测试它)

static int bestDepth = 0;
static int numLoopCalls = 0;

public void emulateLoop() {
    //Our fake stack.  We'll push a 1 when this point on the stack needs a second call to loop() made yet, a 0 if it doesn't
    BitSet fakeStack = new BitSet();
    long currentDepth = 0;
    numLoopCalls = 0;

    while(currentDepth >= 0)
    {
        numLoopCalls++;

        if(proba()) {
            //"return" from the current function, going up the callstack until we hit a point that we need to "call loop()"" a second time
            fakeStack.clear(currentDepth);
            while(!fakeStack.get(currentDepth))
            {
                currentDepth--;
                if(currentDepth < 0)
                {
                    return;
                }
            }

            //At this point, we've hit a point where loop() needs to be called a second time.
            //Mark it as called, and call it
            fakeStack.clear(currentDepth);
            currentDepth++;
        }
        else {
            //Need to call loop() twice, so we push a 1 and continue the while-loop
            fakeStack.set(currentDepth);
            currentDepth++;
            if(currentDepth > bestDepth)
            {
                bestDepth = currentDepth;
            }
        }
    }
}

这可能会稍微慢一点,但它会使用大约1/100的内存。请注意BitSet存储在堆上,因此不再需要增加堆栈大小来运行它。如果有的话,你会想要increase the heap-size

答案 3 :(得分:0)

递归的缺点是它开始填满你的堆栈,如果你的递归太深,最终会导致堆栈溢出。如果要确保测试结束,可以使用以下Stackoverflow线程中给出的答案来增加堆栈大小:

How to increase to Java stack size?