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的问题了。

相关文章
相关标签/搜索