为什么Java递归调用不释放局部变量内存

时间:2018-04-26 13:54:15

标签: java recursion

我有一个类似下面的递归方法

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;
    }
}

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

4 个答案:

答案 0 :(得分:2)

释放将在方法返回后发生,这是在执行第一个foo之后,但在您的情况下,它会调用另一个foo,它还会创建一个新的字节数组,并调用第三个foo,因此它累积所有字节数组,垃圾收集器不会尝试清除它们,直到它们全部返回,或者你的获取OOM。

答案 1 :(得分:1)

在方法返回之前,Java不会从堆栈中释放局部变量。因此,即使永远不会使用局部变量,它仍然被引用,因此在方法返回之前,不能对数组进行垃圾收集。

Java也没有实现尾调用优化,因此不会提前返回。

答案 2 :(得分:1)

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

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

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

此函数演示即使在递归调用数组之后仍然可以使用。

BONUS:你提出的递归类型称为尾递归,有些算法可以自动将它们转换为循环,所以在像Kotlin这样的语言中你可以这样做:

pg_event_trigger_ddl_commands()

这可行,因为Kotlin编译器会将其转换为for或while循环。

答案 3 :(得分:0)

递归在这种情况下不起作用,因为java必须在内存中包含方法的每个实例,所以当你递归调用它时,它会用完ram,但是当你使用循环时,它可以使用优化它不能用于递归。