在Java中談尾遞歸--尾遞歸和垃圾回收的比較


我不是故意在JAVA中談尾遞歸的,因為在JAVA中談尾遞歸真的是要繞好幾個彎,只是我確實只有JAVA學得比較好,雖然確實C是在學校學過還考了90+,真學得沒自學的JAVA好
不過也是因為要繞幾個彎,所以才會有有意思的東西可寫,另外還有我發現把尾遞歸如果跟JAVA中的GC比對一下,也頗有一些妙處(發現還沒有人特地比較過)
(不過后來邊寫邊整理思路,寫出來又是另一個樣子了)
 
轉載請注明:博客園-閣剛廣志,地址: 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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM