2013-02-15 52 views
4

下面的Scala代碼(在2.9.2):意外的Scala集合記憶行爲

var a = (0 until 100000).toStream 
for (i <- 0 until 100000) 
{ 
    val memTot = Runtime.getRuntime().totalMemory().toDouble/(1024.0 * 1024.0) 
    println(i, a.size, memTot) 

    a = a.map(identity) 
} 

使用的環路的每一次迭代不斷增加的內存量。如果a被定義爲(0 until 100000).toList,那麼內存使用率是穩定的(給出或接受GC)。

據我所知,溪流懶惰地評估,但一旦它們被生成就保留元素。但似乎在上面的代碼中,每個新的流(由最後一行代碼生成)以某種方式保持對之前流的引用。有人可以幫忙解釋嗎?

+0

它仍然用'a = a.map(identity)'泄漏內存,你確定它適合你嗎? – 2013-02-15 15:25:17

+0

@TomaszNurkiewicz你說得對。它仍然與身份線一起泄漏。謝謝你的提問 - 我會更新這個問題。 – 2013-02-15 15:33:50

回答

6

這是發生了什麼事。總是懶惰地評估Stream,但已計算的元素稍後「緩存」。懶惰的評估是至關重要的。看看這段代碼:

a = a.flatMap(v => Some(v)) 

雖然看起來好像你正將其Stream到另一個丟棄舊人,這是不會發生什麼變化。新的Stream仍然保留舊的參考。這是因爲結果Stream不應該急於計算基礎流的所有元素,而是按需執行。以此爲例:

io.Source.fromFile("very-large.file").getLines().toStream. 
    map(_.trim). 
    filter(_.contains("X")). 
    map(_.substring(0, 10)). 
    map(_.toUpperCase) 

您可以根據需要鏈接儘可能多的操作,但文件幾乎不會觸及以讀取第一行。每個後續操作僅包含前一個Stream,持有對子流的引用。當您要求sizeforeach時,評估開始。

回到您的代碼。在第二次迭代中,您創建了第三個流,並保存對第二個流的引用,這又引用了您最初定義的引用。基本上你有一堆相當大的物體在增長。

但這並不能解釋爲什麼內存泄漏如此之快。關鍵部分是...... println()a.size準確。沒有打印(並因此評估整個StreamStream仍然是「未評估」。未評估的流不會緩存任何值,因此非常渺茫。記憶仍然會因爲彼此不斷增長的流鏈而泄漏,但速度要慢得多。

這引出一個問題:爲什麼它與toList一起工作這很簡單。 List.map()熱切地創造新List。期。前一個不再被引用並且符合GC的條件。

+0

確實是一個非常好的解釋。我有一種感覺,就是這樣的。有趣的是,由於內存消耗的原因,我一直很小心。但是我有(以下簡稱)代碼'(0到100).sliding(10).toSeq',當我(可能)期待時,意外地(對我來說)給了我一個Stream [Vector [Int]]'一個Vector [Vector [Int]]。然後這給了我最初的記憶問題。問題中的代碼就是我最小化的代碼。 – 2013-02-15 15:30:29