关于java递归调用内存泄露

本文通过两个示例对比分析了Java中递归调用可能导致的内存溢出问题,探讨了尾递归优化的重要性及如何避免内存泄漏。
看两段代码:
import java.util.ArrayList;   
import java.util.List;   
  
public class TailRecursionTest {   
    public static void main(String[] args) {   
        TailRecursionTest t = new TailRecursionTest();   
        for (int i = 0; i < 10000; i++)   
            t.a(0);   
    }
   
  
    public void a(int j) {   
        j++;   
        List list = new ArrayList<Integer>(100000);   
        // 对list进行处理   
    }
   
}

没啥特殊的,仅仅是为了测试,我们将a方法调用10000次,a方法创建一个有100000个元素的list的局部变量。
第二个程序:
import java.util.ArrayList;   
import java.util.List;   
  
public class TailRecursionTest2 {   
    public static void main(String[] args) {   
        TailRecursionTest2 t = new TailRecursionTest2();   
        t.a(0);   
    }
   
  
    public void a(int j) {   
        System.out.println(j);   
        j++;   
        if (j == 10000)   
            return;   
        List list = new ArrayList<Integer>(100000);   
        // 对list进行处理   
        a(j);   
    }
   
}
  

也没啥特殊的,就是将循环换成了递归,a方法做的事情没变。两个都跑一下,程序1顺利结束,程序2出问题了,啥问题?如下:
161  
162  
163  
164  
165  
Exception in thread 
"main" java.lang.OutOfMemoryError: Java heap space   
    at java.util.ArrayList.
<init>(Unknown Source)   
    at TailRecursionTest2.a(TailRecursionTest2.java:
17)   
    at TailRecursionTest2.a(TailRecursionTest2.java:
20)   
    at TailRecursionTest2.a(TailRecursionTest2.java:
20)   
    at TailRecursionTest2.a(TailRecursionTest2.java:
20)   
    at TailRecursionTest2.a(TailRecursionTest2.java:
20

我倒,才运行166次了,heap就满了。问题在哪呢?oh,yep,你肯定想到了,是不是重复创建list这个大集合引起的呢?它不是局部变量吗?怎么也会溢出?是的,list是局部变量,在a的方法栈里引用着,指向heap上的大对象,更关键的问题在于,java是没有尾递归优化的,递归方法是不会使用同一个栈帧,每一次递归调用,都将压入新的栈帧,并且这个栈帧上又new了一个list变量,引用着heap上新的一个大集合。随着栈深度的增加, jvm里维持着一条长长的方法调用轨迹以便你能回来,在方法没有返回之前,这些list变量一直被各自的栈帧引用着,不能被GC,你说,能不OOM吗?

    也许,你想到了个补救方法来挽救程序2,就是每次在处理完list后,我把它设置为null,不让栈帧继续引用着它,咱编写对gc友好的代码,这不就行了,试试:


import java.util.ArrayList;   
import java.util.List;   
  
public class TailRecursionTest2 {   
    public static void main(String[] args) {   
        TailRecursionTest2 t = new TailRecursionTest2();   
        t.a(0);   
    }
   
  
    public void a(int j) {   
        System.out.println(j);   
        j++;   
        if (j == 10000)   
            return;   
        List list = new ArrayList<Integer>(100000);   
        // 对list进行处理   
        list = null;  //gc友好   
        a(j);   
    }
   
}
 

得意洋洋,我跑一下看看,这次跑到4000多次,但是:
   
4289  
4290  
4291  
4292  
java.lang.StackOverflowError   
    at sun.nio.cs.ext.DoubleByteEncoder.encodeArrayLoop(Unknown Source)   
    at sun.nio.cs.ext.DoubleByteEncoder.encodeLoop(Unknown Source)   
    at java.nio.charset.CharsetEncoder.encode(Unknown Source) 

总结:在java里,递归最好咱还是别用,老老实实地while、for;就算递归了,最好递归方法不要new太大的对象,除非你能确定递归的深度不是那么大,否则OOM和堆栈溢出的阴影将笼罩着你。
### Java递归调用的概述 在 Java 编程语言中,递归是指函数在其定义或实现过程中直接或间接地调用了自己。这种技术对于处理能够被自然划分为相似子问题的任务特别有效[^3]。 #### 递归的工作原理 每当一个递归方法被执行时,在 JVM 上下文中会创建一个新的栈帧来保存该次调用的状态信息,比如传递给它的参数以及内部使用的局部变量等。如果递归层次太深,则可能会超出虚拟机所能提供的最大栈深度限制,从而引发 `StackOverflowError` 错误[^1]。 ```java public class Factorial { public static long factorial(long number) { if (number <= 1) { // base case return 1; } else { // recursive step return number * factorial(number - 1); } } public static void main(String[] args) { try { System.out.println("Factorial of 5 is " + factorial(5)); } catch (Exception e) { System.err.println(e.getMessage()); } } } ``` 上述代码展示了如何通过递归来计算阶乘值。需要注意的是,这里设置了一个终止条件(即基本情形),这是为了避免无限循环并最终导致堆栈溢出的关键所在。 #### 防止堆栈溢出的方法 为了防止由于过度深入而引起的堆栈溢出: - **优化算法逻辑**:确保存在清晰明确的基础情况,并且每一次递归都能朝着基础情况进行简化。 - **采用尾递归转换**:虽然标准版 JDK 不支持自动将常规递归转化为更高效的尾递归形式,但在编写代码时可以手动调整为尾递归模式以减少中间状态的数量。 - **迭代替代方案**:有时可以通过改写程序使用显式的数据结构如队列或者栈来进行广度优先搜索(BFS)/深度优先搜索(DFS),以此代替隐含于递归之中的系统调用栈操作[^4]。 #### 自引用与类级别的递归调用注意事项 当涉及到对象之间的相互关联或者是自我复制的数据结构时,应当小心谨慎地管理这些关系以免造成意外的行为。例如,在声明数组或其他容器类作为实例字段之前要充分考虑其生命周期及其潜在影响;另外也要注意初始化顺序可能带来的副作用。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值