【问题标题】:BigInteger memory leak causes stack overflow in JavaBigInteger 内存泄漏导致 Java 中的堆栈溢出
【发布时间】:2015-06-18 23:15:39
【问题描述】:

我正在尝试编写一个优化的斐波那契作为能够计算 fib(300) 和 fib(8000) 的作业。这是我目前所拥有的(map 是一个 HashMap)。

public static BigInteger fibMemo(int n) {

    if (n <= 1){
        return BigInteger.valueOf(n);
    }

    if (!map.containsKey(n)){
        map.put(n, fibMemo(n-1).add(fibMemo(n-2)));
    }
    return map.get(n);        
}

当我打电话时

System.out.println("300: " + FibonacciMemo.fibMemo(300));

就其本身而言,它工作得很好。 还有,

System.out.println("8000: " + FibonacciMemo.fibMemo(8000));

如果我注释掉之前对 fib(300) 的调用,则可以正常工作。但是,如果我保留这两个调用,我会在递归 fibMemo 上得到堆栈溢出。这对我来说似乎很奇怪。有人可以澄清一下情况吗?提前致谢。

代码如下:

import java.util.HashMap; // Import Java's HashMap so we can use it
import java.math.BigInteger;

public class FibonacciMemo {
    private static HashMap<Integer, BigInteger> map = new HashMap<Integer, BigInteger>();
    /**
     * The classic recursive implementation with no memoization. Don't care
     * about graceful error catching, we're only interested in performance.
     * 
     * @param n
     * @return The nth fibonacci number
     */
    public static int fibNoMemo(int n) {
        if (n <= 1) {
            return n;
        }
        return fibNoMemo(n - 2) + fibNoMemo(n - 1);
    }
    /**
     * Your optimized recursive implementation with memoization. 
     * You may assume that n is non-negative.
     * 
     * @param n
     * @return The nth fibonacci number
     */
    public static BigInteger fibMemo(int n) {
        // YOUR CODE HERE
        if (n <= 1){
            return BigInteger.valueOf(n);
        }

        if (!map.containsKey(n)){
            map.put(n, fibMemo(n-1).add(fibMemo(n-2)));
        }
        return map.get(n);        
    }
public static void main(String[] args) {
        // Optional testing here        
        String m = "Fibonacci's real name was Leonardo Pisano Bigollo.";
        m += "\n" + "He was the son of a wealthy merchant.\n";
        System.out.println(m);
         System.out.println("300: " + FibonacciMemo.fibMemo(300));
        System.out.println("8000: " + FibonacciMemo.fibMemo(8000));
        // 46th Fibonacci = 1,836,311,903
        // 47th Fibonacci = 2,971,215,073
    }
}

【问题讨论】:

  • 地图在哪里声明以及如何声明?
  • 你的堆栈跟踪是什么?
  • private static HashMap&lt;Integer, BigInteger&gt; map = new HashMap&lt;Integer, BigInteger&gt;();同班
  • 我无法复制这个问题——即使只有fibMemo(8000) 而不是两者都有,我也会遇到堆栈溢出。您是否使用-Xss 增加了堆栈大小?
  • 同上,结果与 Neuronaut 相同。 8000 的 StackOverflow,无论是否调用 fib(300)。看来您只是在对抗 Java 的默认堆栈大小。

标签: java stack-overflow biginteger fibonacci


【解决方案1】:

您的代码有两个问题。显而易见的一个是堆栈消耗。 memoization 确实将时间复杂度从指数降低到了线性,但是该方法仍然具有线性堆栈消耗 - 对于输入值 8000,它分配了 8000 个堆栈帧。

docs 中所述,每个线程的默认堆栈大小为320kB,这足以满足大约 1000 - 2000 帧,这还不够。您可以使用-Xss JVM 开关增加堆栈大小,但这仍然不是防弹的。您应该改用迭代实现。

第二个问题是你的静态缓存永远不会被清除,这基本上会导致内存泄漏。您可以将递归方法包装在另一个方法中,该方法在递归终止后清除哈希图,但这会降低一些性能,因为一次调用的结果不能在以下调用中重用。

更高效的解决方案是使用适当的缓存实现,该实现不需要手动清理,但可以自行处理大小限制和垃圾收集。 Guava 提供了这样的实现。

【讨论】:

  • 感谢纳蒂克斯。这离我的误解越来越近了。你能解释一下第二段吗?我不清楚的原因是因为我认为为 fib(300) 完成的计算将用于 fib(8000)。为什么这里会发生内存泄漏?
  • 好吧,在您的程序中,您只需计算 300 的值,然后计算 8000 的值,然后终止,所以在这种情况下,这并不重要。但是假设你在一个长时间运行的应用程序(例如一个 web 服务器)中使用了这个类:一旦你调用了fibMemo(8000),静态映射将被 8000 个条目填满,这些条目将保留在那里并占据堆直到应用程序终止,甚至如果没有人再次调用该方法。
  • 这完全有道理,但是不能看看硬币的另一面吗?比如说,你清除了缓存,然后有人需要调用 fib(5000)。现在您必须重新计算所有这些值。
  • 是的,没错。这就是为什么我提到使用比普通哈希图更复杂的缓存实现的原因。诸如 Guava 中的缓存会保留先前计算的值,但可以有一个大小限制,以确保它不会不受控制地增长或可以在指定的超时后被清除。
【解决方案2】:

看来您的递归算法对于 Java 的默认堆栈大小来说实在是太大了。堆栈内存的优化与硬件中的堆不同,无论如何您都不应该使用具有这么多递归的算法。一些语言可以优化尾递归。至少在这种情况下,Java 似乎不会总是优化您的代码。

因此,最好的解决方案 imo 就是改写代码以使用循环。

   private final static List<BigInteger> fibs = new ArrayList<>();
   static{ fibs.add( BigInteger.ZERO ); fibs.add( BigInteger.ONE ); }

   public static BigInteger lFib( int n ) {
      if( n < 0 ) throw new IllegalArgumentException();
      if( n >= fibs.size() ) {
         for( int i = fibs.size(); i <= n; i++ )
            fibs.add( fibs.get(i-2).add( fibs.get(i-1) ) );
      }
      return fibs.get(n);
   }

非常轻微的测试。

【讨论】:

    【解决方案3】:

    问题是线程堆栈的大小,在大量递归调用时可能会耗尽。解决方案是提供足够的堆栈大小。您可以尝试使用 vm arg -Xss 运行该应用程序。我试过-Xss2m,效果很好。

    【讨论】:

      【解决方案4】:

      更改您的代码

              map.put(n, fibMemo(n-1).add(fibMemo(n-2)));
      

              map.put(n, fibMemo(n-2).add(fibMemo(n-1)));
      

      效果很好。

      前者的调用顺序是

      fibMemo(10) nested level = 0
      fibMemo(9) nested level = 1
      fibMemo(8) nested level = 2
      fibMemo(7) nested level = 3
      fibMemo(6) nested level = 4
      fibMemo(5) nested level = 5
      fibMemo(4) nested level = 6
      fibMemo(3) nested level = 7
      fibMemo(2) nested level = 8
      fibMemo(1) nested level = 9
      fibMemo(0) nested level = 9
      fibMemo(1) nested level = 8
      fibMemo(2) nested level = 7
      fibMemo(3) nested level = 6
      fibMemo(4) nested level = 5
      fibMemo(5) nested level = 4
      fibMemo(6) nested level = 3
      fibMemo(7) nested level = 2
      fibMemo(8) nested level = 1
      

      后者的调用序列是

      fibMemo(10) nested level = 0
      fibMemo(8) nested level = 1
      fibMemo(6) nested level = 2
      fibMemo(4) nested level = 3
      fibMemo(2) nested level = 4
      fibMemo(0) nested level = 5
      fibMemo(1) nested level = 5
      fibMemo(3) nested level = 4
      fibMemo(1) nested level = 5
      fibMemo(2) nested level = 5
      fibMemo(5) nested level = 3
      fibMemo(3) nested level = 4
      fibMemo(4) nested level = 4
      fibMemo(7) nested level = 2
      fibMemo(5) nested level = 3
      fibMemo(6) nested level = 3
      fibMemo(9) nested level = 1
      fibMemo(7) nested level = 2
      fibMemo(8) nested level = 2
      

      后者对堆栈的消耗较少。

      【讨论】:

        猜你喜欢
        • 2012-02-05
        • 1970-01-01
        • 2015-05-21
        • 2014-02-14
        • 1970-01-01
        • 1970-01-01
        • 2020-02-18
        • 1970-01-01
        • 2014-02-19
        相关资源
        最近更新 更多