`
1028826685
  • 浏览: 938620 次
  • 性别: Icon_minigender_1
  • 来自: 重庆
社区版块
存档分类

在Java中谈尾递归--尾递归和垃圾回收的比较

    博客分类:
  • J2EE
 
阅读更多
转载请注明:博客园-阁刚广志,地址:http://www.cnblogs.com/bellkosmos/p/5280619.html 
 
一、首先我们讲讲递归
  1. 递归的本质是,某个方法中调用了自身。本质还是调用一个方法,只是这个方法正好是自身而已
  2. 递归因为是在自身中调用自身,所以会带来以下三个显著特点:
    1. 调用的是同一个方法
    2. 因为1,所以只需要写一个方法,就可以让你轻松调用无数次(不用一个个写,你定个n就能有n个方法),所以调用的方法数可能非常巨大
    3. 在自身中调用自身,是嵌套调用(栈帧无法回收,开销巨大)
  3. 因为上面2和3两个特点,所以递归调用最大的诟病就是开销巨大,栈帧和堆一起爆掉,俗称内存泄露
    1. 一个误区,不是因为调用自身而开销巨大,而是嵌套加上轻易就能无数次调用,使得递归可以很容易开销巨大
 
既然会导致内存泄露如此,那肯定要想办法了,方法很简单,那就是尾递归优化
二、尾递归优化
  1. 尾递归优化是利用上面的第一个特点“调用同一个方法”来进行优化的
  2. 尾递归优化其实包括两个东西:1)尾递归的形式;2)编译器对尾递归的优化
    1. 尾递归的形式
      1. 尾递归其实只是一种对递归的特殊写法,这种写法原本并不会带来跟递归不一样的影响,它只是写法不一样而已,写成这样不会有任何优化效果,该爆的栈和帧都还会爆
      2. 具体不一样在哪里
        1. 前面说了,递归的本质是某个方法调用了自身,尾递归这种形式就要求:某个方法调用自身这件事,一定是该方法做的最后一件事(所以当有需要返回值的时候会是return f(n),没有返回的话就直接是f(n)了)
      3. 要求很简单,就一条,但是有一些常见的误区
        1. 这个f(n)外不能加其他东西,因为这就不是最后一件事了,值返回来后还要再干点其他的活,变量空间还需要保留
          1. 比如如果有返回值的,你不能:乘个常数 return 3f(n);乘个n return n*f(n);甚至是 f(n)+f(n-1)
      4. 另外,使用return的尾递归还跟函数式编程有一点关系
    2. 编译器对尾递归的优化
      1. 上面说了,你光手动写成尾递归的形式,并没有什么卵用,要实现优化,还需要编译器中加入了对尾递归优化的机制
      2. 有了这个机制,编译的时候,就会自动利用上面的特点一来进行优化
      3. 具体是怎么优化的:
        1. 简单说就是重复利用同一个栈帧,不仅不用释放上一个,连下一个新的都不用开,效率非常高(有人做实验,这个比递推比迭代都要效率高)
  3. 为什么写成尾递归的形式,编译器就能优化了?或者说【编译器对尾递归的优化】的一些深层思想
    1. 说是深层思想,其实也是因为正好编译器其实在这里没做什么复杂的事,所以很简单
    2. 由于这两方面的原因,尾递归优化得以实现,而且效果很好
      1. 因为在递归调用自身的时候,这一层函数已经没有要做的事情了,虽然被递归调用的函数是在当前的函数里,但是他们之间的关系已经在传参的时候了断了,也就是这一层函数的所有变量什么的都不会再被用到了,所以当前函数虽然没有执行完,不能弹出栈,但它确实已经可以出栈了,这是一方面
      2. 另一方面,正因为调用的是自身,所以需要的存储空间是一毛一样的,那干脆重新刷新这些空间给下一层利用就好了,不用销毁再另开空间
    3. 有人对写成尾递归形式的说法是【为了告诉编译器这块要尾递归】,这种说法可能会导致误解,因为不是只告诉编译器就行,而是你需要做优化的前半部分,之后编译器做后半部分
  4. 所以总结:为了解决递归的开销大问题,使用尾递归优化,具体分两步:1)你把递归调用的形式写成尾递归的形式;2)编译器碰到尾递归,自动按照某种特定的方式进行优化编译
举例:
(没有使用尾递归的形式)
def recsum(x):
  if x == 1:
    return x
  else:
    return x + recsum(x - 1)

(使用尾递归的形式)

def tailrecsum(x, running_total=0):
  if x == 0:
    return running_total
  else:
    return tailrecsum(x - 1, running_total + x)

 

但不是所有语言的编译器都做了尾递归优化。比如C实现了,JAVA没有去实现
说到这里你很容易联想到JAVA中的自动垃圾回收机制,同是处理内存问题的机制,尾递归优化跟垃圾回收是不是有什么关系,这是不是就是JAVA不实现尾递归优化的原因?
 
三、所以下面要讲一下垃圾回收(GC)
  1. 首先我们需要谈一下内存机制,这里我们需要了解内存机制的两个部分:栈和堆。下面虽然是在说JAVA,但是C也是差不多的
    1. 在Java中, JVM中的栈记录了线程的方法调用。每个线程拥有一个栈。在某个线程的运行过程中, 如果有新的方法调用,那么该线程对应的栈就会增加一个存储单元,即栈帧 (frame)。在frame 中,保存有该方法调用的参数、局部变量和返回地址
    2. Java的参数和局部变量只能是 基本类型 的变量(比如 int),或者对象的引用(reference) 。因此,在栈中,只保存有基本类型的变量和对象引用。而引用所指向的对象保存在堆中。
  2. 然后由栈和堆的空间管理方式的不同,引出垃圾回收的概念
    1. 当被调用方法运行结束时,该方法对应的帧将被删除,参数和局部变量所占据的空间也随之释放。线程回到原方法,继续执行。当所有的栈都清空时,程序也随之运行结束。
    2. 如上所述,栈 (stack)可以自己照顾自己。但堆必须要小心对待。堆是 JVM中一块可自由分配给对象的区域。当我们谈论垃圾回收 (garbage collection) 时,我们主要回收堆(heap)的空间
    3. Java的普通对象存活在堆中。与栈不同,堆的空间不会随着方法调用结束而清空(即使它在栈上的引用已经被清空了)(也不知道为什么不直接同步清空)。因此,在某个方法中创建的对象,可以在方法调用结束之后,继续存在于堆中。这带来的一个问题是,如果我们不断的创建新的对象,内存空间将最终消耗殆尽。
    4. 如果没有垃圾回收机制的话,你就需要手动地显式分配及释放内存,如果你忘了去释放内存,那么这块内存就无法重用了(不管是什么局部变量还是其他的什么)。这块内存被占有了却没被使用,这种场景被称之为内存泄露
  3. 所以不管是C还是JAVA,最原始的情况,都是需要手动释放堆中的对象,C到现在也是这样,所以你经常需要考虑对象的生存周期,但是JAVA则引入了一个自动垃圾回收的机制,它能智能地释放那些被判定已经没有用的对象
 
四、现在我们就可以比较一下尾递归优化和垃圾回收了
  1. 他们最本质的区别是,尾递归优化解决的是内存溢出的问题,而垃圾回收解决的是内存泄露的问题
    1. 内存泄露:指程序中动态分配内存给一些临时对象,但是对象不会被GC所回收,它始终占用内存。即被分配的对象可达但已无用。
    2. 内存溢出:指程序运行过程中无法申请到足够的内存而导致的一种错误。内存溢出通常发生于OLD段或Perm段垃圾回收后,仍然无内存空间容纳新的Java对象的情况。
    3. 从定义上可以看出内存泄露是内存溢出的一种诱因,不是唯一因素。
  2. 自动垃圾回收机制的特点是:
    1. 解决了所有情况下的内存泄露的问题,但还可以由于其他原因内存溢出
    2. 针对内存中的堆空间
    3. 正在运行的方法中的堆中的对象是不会被管理的,因为还有引用(栈帧没有被清空)
      1. 一般简单的自动垃圾回收机制是采用 引用计数 (reference counting)的机制。每个对象包含一个计数器。当有新的指向该对象的引用时,计数器加 1。当引用移除时,计数器减 1,当计数器为0时,认为该对象可以进行垃圾回收
  3. 与之相对,尾递归优化的特点是:
    1. 优化了递归调用时的内存溢出问题
    2. 针对内存中的堆空间和栈空间
    3. 只在递归调用的时候使用,而且只能对于写成尾递归形式的递归进行优化
    4. 正在运行的方法的堆和栈空间正是优化的目标
 
最后可以解答一下前头提出的问题
  1. 通过比较可以发现尾递归和GC是完全不一样的,JAVA不会是因为有GC所以不需要尾递归优化。那为什么呢,我看到有的说法是:JAVA编写组不实现尾递归优化是觉得麻烦又没有太大的必要,就懒得实现了(原话是:在日程表上,但是非常靠后),官方的建议是不使用递归,而是使用while循环,迭代,递推

 

参考资料:

http://it.deepinmind.com/jvm/2014/04/16/tail-call-optimization-and-java.html

http://book.51cto.com/art/201212/370096.htm

分享到:
评论

相关推荐

    java面试题及答案-非常全面(包括基础、网络、数据结构、算法及IT大厂面经)

    - **集合框架**:Java中的集合框架主要分为两种类型:`List` 和 `Set`。 - **List**:有序集合,可以包含重复元素。主要实现有`ArrayList`(基于数组)、`LinkedList`(基于双向链表)。 - **Set**:不允许重复...

    java面试必会200题.docx

    - **finalize()方法**:在对象被垃圾回收之前由Java虚拟机调用,给对象最后一次机会清理资源。 2. **final** - **final关键字**:用于声明不可变的变量或类。 - **final类**:声明为final的类不能被继承。 - **...

    Java经典面试题+答案(带书签)pdf

    - 在Java中,多态主要通过继承和接口实现。 **2. 关于继承与实现的问题** - **继承**:一个类可以从另一个类继承属性和行为。 - **实现**:一个类可以实现一个接口,从而获得接口中声明的方法。 **3. 抽象关键字...

    百度Java面试题 前200页精选(上)

    标题《百度Java面试题 前200页精选(上)》所指向的内容是关于Java开发和面试的知识点,包含了Java的基本概念、原理、数据结构和一些开发中的高级特性。文档中列举的问题覆盖面广泛,既包括了基础知识,也包括了一些...

    Java解释器源码-symmetric-train:用Racket编写的原始Java解释器。interpreter.rkt中的源代码

    7. **内存管理**:虽然Racket可能有自己的垃圾回收机制,但Java解释器需要模拟Java的内存模型,包括对象的创建、引用计数和垃圾回收。 8. **线程与同步**:如果解释器支持多线程,那么它需要实现线程的创建、调度和...

    Java经典问题答案(带书签).pdf

    - Unicode问题通常涉及如何正确地处理非ASCII字符,在Java中需要注意字符集编码和转换问题。 **Eclipse简便设置** - Eclipse是一款流行的IDE,用于Java开发。简便设置包括配置工作空间、添加外部库、设置编译器...

    HighPerformanceScript.zip

    3. **使用尾递归**:在支持尾递归优化的语言中,如Scheme或JavaScript(某些实现),使用尾递归可以避免栈溢出。 综上所述,"HighPerformanceScript.zip"文件可能包含了以上提到的多种高性能脚本编程策略和实践,...

    百度2019年最新面试题库

    - **JVM(Java Virtual Machine)**: Java虚拟机,负责解释执行Java字节码,提供内存管理、垃圾回收等功能。 - **JIT(Just-In-Time Compiler)**: 即时编译器,将热点代码编译成本地机器码,提高运行效率。 #### ...

    java程序员面试大纲错过了金三银四你还要错过2018吗.docx

    5. **HashMap 1.7与1.8的区别**:Java 8中`HashMap`进行了多项优化,如链表转红黑树、使用尾递归等,提高了性能和稳定性。 6. **final、finally、finalize**:`final`关键字用于声明不可变的变量或方法;`finally`...

    Effective Scala中文版

    - **递归**: 推荐使用尾递归优化。 - **返回**: 使用Return表达式来处理提前返回。 - **for循环和for推导**: 利用Scala强大的for表达式。 - **require和断言**: 如何使用require和assert来确保代码的正确性。 #### ...

    JAVA 内存溢出案例汇总

    为了避免栈深度溢出,可以使用迭代替代递归调用,或者使用尾递归优化等技术。 永久代内存溢出 永久代内存溢出是指 Java 虚拟机的永久代内存空间溢出,导致内存溢出。这种情况通常发生在使用大量的 ClassLoader ...

    安卓面试题

    - **优化**:尾递归优化、三数取中法。 #### 48. synchronized关键字 - **作用**:实现线程同步。 - **用法**:修饰方法或代码块。 #### 49. 线程状态 - **新建状态**:Thread对象创建后。 - **就绪状态**:线程...

    effective scala

    在编写递归函数时要特别注意尾递归的优化。for循环和for推导则是Scala中简洁表达集合操作的强大工具。 函数式编程是Scala的核心特性之一。Scala支持通过case类模拟代数数据类型,提供Option类型来处理可能不存在的...

    如何优化Java程序设计和编码提高性能

    - Java程序中过度创建对象可能导致频繁的垃圾回收,影响性能。例如,使用`String`类时,连续的`+`操作会不断创建新的`String`对象,而使用`StringBuffer`或`StringBuilder`则能避免这个问题。在需要拼接字符串的...

    FastLinkStack:一个下班后的项目,源于与同事的“我想知道”的对话。 这是为 Java 应用程序创建快速 ITS 链接适配器(链接堆栈)的探索性工作

    3. **性能优化**:通过优化算法和数据结构来提升性能,比如使用尾递归优化、减少不必要的对象创建等。 4. **线程安全**:如果设计为线程安全,可能使用了锁或其他并发控制机制,如ReentrantLock或Atomic变量。 在...

    亚马逊 面经

    - **垃圾回收**: Java 有自动垃圾回收机制,而 C++ 没有。 **12. Trie Tree (前缀树)** - **定义**: 前缀树是一种特殊的树形结构,用于存储字符串集合。它的每个节点代表一个字符串的前缀。 - **用途**: 适用于...

    JVM-Compiler

    - **尾递归优化**:Scala编译器支持尾递归优化,可以避免无限递归导致的栈溢出问题。 - **模式匹配**:Scala的模式匹配语法在编译时转化为高效的字节码,提升了代码的可读性和性能。 6. **JVM工具与调试**: - *...

    Leetcode_Solutions:个人 Leetcode 解决方案

    3. 尾递归优化:虽然Java标准不支持尾递归优化,但通过合理设计,可以减少递归的深度,降低栈溢出风险。 4. 定义静态内部类:对于只使用一次的辅助类,可以使用静态内部类,避免每次实例化时创建新的类对象。 四、...

Global site tag (gtag.js) - Google Analytics