Clojure惰性序列的頭保持問題

《Clojure編程》一書中有一個例子: java

(let[[t d](split-with #(< % 12) (range 1e8))]
  [(countd) (countt)])
;= OutOfMemoryError Java heap space  clojure.lang.ChunkBuffer.<init> (ChunkBuffer.java:20);
 
(let[[t d](split-with #(< % 12) (range 1e8))]
  [(countt) (countd)])
;=[12 99999988]

只是(count t) (count d)的順序不一樣,前一段代碼會拋OutOfMemoryError錯誤,後一個則徹底沒有問題,書裏面解釋的不是很詳細,這裏展開詳細說下其中的原因。這裏面涉及到Clojure裏面集合的數據結構共享機制。 程序員

Clojure數據結構共享

咱們知道Clojure裏面強調的是不可變數據結構,幾乎任何操做都不會改變現有值,而是會產生新值,好比咱們有這麼一個惰性序列: 編程

(let[h (range 1 6)])

它在內存中的表示大概是這樣的: 數據結構

頭指針h指向第一個元素(尚未被實例化的元素), 這些格子是虛線的,表示這些格子尚未實例化。如今咱們執行下列代碼: app

(let[h1 (next h))

(next h)並不會改變h自己,而是會產生一個新的h1序列,那麼Clojure會把h裏面全部的內存元素都拷貝一遍以產生這個新的h1麼?固然不會,Clojure沒那麼傻,內存結構會變成下面這樣: wordpress

一個元素都沒有複製,只是產生了一個新的h1頭指針,指向了h序列的第二個元素,原來的h頭指針仍是指向第一個元素,這樣雖然從程序員的角度來看有兩個獨立的序列h和h1, 可是內存裏面只有一份數據。這就是Clojure裏面的數據結構共享機制。 函數

count的執行過程

要解釋咱們前面提到的問題,咱們先看看count一個惰性序列是怎麼樣的一個過程,這個過程當中的內存佔用是怎麼樣的。 優化

(let[t (range 1 6)]
  (countt))

咱們來看看上面的代碼是怎麼執行的。(range 1 6)在內存裏面的結構在上面介紹數據結構共享機制的時候已經展現過了,會有一個頭指針指向這個數據結構的頭,咱們看看在count執行過程當中,內存結構會發生怎樣的變化。 spa

Clojure裏面的count函數最終是調用clojure.lang.RT.countFrom(Object obj)來實現的,下列代碼是當要count的集合是惰性序列的時候執行的邏輯: 指針

能夠看出對於惰性序列(以及其它持久性的數據結構),count是經過for循環遍歷集合裏面的每一個元素(從而實例化每一個元素)來計算出惰性序列的數量的。遍歷的時候調用的是s.next()方法,s.next()方法至關於調用(next s), 所以產生的也是一個新的持久性集合。遍歷完第一個元素以後的內存結構是這樣的:

上面的第一個元素(1)會被JVM回收掉的。也許有人會問了,上面(count t)在執行的時候,t還在有效做用域內,它下面的元素怎麼會被垃圾回收呢?不是應該等(count t)所有都執行完等程序控制流出了這個做用域才能回收t所佔用的內存嗎?在Java裏面是也許是這樣的,可是Clojure裏面對這方面作了優化,Clojure的編譯器發現t在當前做用域後面沒有再被用到了((count t)後面已經沒有再用到t了),所以能夠放心地把再也不被引用的元素1回收掉,這種技術叫作locals clearing[1]。

以此類推,無論要count的惰性序列所含數據量有多大,count所佔用的內存都是恆定的,所以下面的代碼是不會致使OutOfMemoryError的:

(count(range 1e8))

從這裏咱們能夠總結出來一個道理:咱們討論的這個頭保持問題不是count自己致使的

頭保持(head retention)

咱們再來看看下面代碼求頭尾兩個count的時候內存中的數據結構會怎麼樣:

(let[[t d](split-with #(< % 4) (range 1 6))]
  [(countd) (countt)])

在[(count d) (count t)]執行以前,整個序列是這樣的:

t的頭指向第一個元素,d的頭指向第四個元素。如今先執行(count d)(Clojure代碼是從左向右執行的), count過第一個元素以後整個序列是這樣的:

注意,這裏4這個元素是沒法被垃圾回收掉的,由於整個數據的頭還被t引用,所以整個數據結構上的任意節點都是不能被垃圾回收。想一想若是d後面有不少數據,那麼都得存在內存裏面不能被回收,最後的結果就是OutOfMemoryError。

若是咱們稍微調換下兩個count的順序呢:

(let[[t d](split-with #(< % 4) (range 1 6))]
  [(countt) (countd)])

那麼這樣Clojure會先執行(count t), count到第二個元素的時候內存結構是這樣的:

這裏已經實例化的元素1是能夠被垃圾回收的,由於兩個頭指針t,d都在元素1的後面,已經沒有人須要這個元素1了,所以它是能夠被垃圾回收的。所以無論d後面有多少數據,只要咱們先執行的是(count t), 整個序列的頭不被保持,那麼在咱們count過程當中內存會被不斷的回收,不會有全部元素保持在內存的問題,所以也就不會有OutOfMemoryError的問題了。

相關文章
相關標籤/搜索