首页 > 解决方案 > 为什么Java递归调用不释放局部变量内存

问题描述

我有一个像下面这样的递归方法

private void foo(int i){
    byte[] a = new byte[1 * 1024 * 1024];
    System.out.println(i+" "+a.length);
    foo(i+1);
}

a我发现无法释放局部变量,如果我将最大堆大小设置为 50M (-Xmx50M),它将在第 44 次调用时遇到 OOM

44 1048576
java.lang.OutOfMemoryError: Java heap space

但是改成for循环就没有这个问题了

private void bar(){
    int index = 1;
    while (true) {
        byte[] a = new byte[1 * 1024 * 1024];
        System.out.println(index+" "+a.length);
        index += 1;
    }
}

那么为什么在递归调用它不释放局部变量的内存呢?

标签: javarecursion

解决方案


在 for 循环变量 a 绑定到 for 循环的范围,因此对数组的引用在每次迭代后被释放,垃圾收集器可以释放该内存。

当您执行每个数组的递归引用时,每个数组的引用都存储在堆栈中,因此垃圾收集器无法销毁这些数组,因为有人(在本例中为堆栈)正在引用这些数组。想象一下这种情况:

private void foo(int i){
    byte[] a = new byte[1 * 1024 * 1024];
    System.out.println(i+" "+a.length);
    foo(i+1);
    a[1] += a[0] + 1;
}

这个函数表明,即使在递归调用数组 a 之后仍然可以使用。

奖励:您提出的递归类型称为尾递归,并且有一些算法可以自动将它们转换为循环,因此在 Kotlin 等语言中,您可以这样做:

tailrec fun foo(i: Int): Boolean {
    var a = Array<Byte>(1 * 1024 * 1024, { 0 });
    System.out.println("$i ${a.size}");
    foo(i + 1);
}

这会起作用,因为 Kotlin 编译器会将其转换为 for 或 while 循环。


推荐阅读