4

次の Scala コード (2.9.2):

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、メモリ使用量は安定しています (Give or Take GC)。

ストリームが遅延評価されることは理解していますが、生成された要素は保持されます。しかし、上記のコードでは、(コードの最後の行によって生成された) 新しいストリームのそれぞれが、何らかの形で以前のストリームへの参照を保持しているようです。誰かが説明を手伝ってくれますか?

4

1 に答える 1

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、子ストリームへの参照を保持して、前の をラップするだけです。お願いしsizeたりした瞬間foreachから評価が始まります。

コードに戻ります。2 回目の繰り返しでは、3 番目のストリームを作成し、2 番目のストリームへの参照を保持します。2 番目のストリームは、最初に定義したストリームへの参照を保持します。基本的に、かなり大きなオブジェクトのスタックが成長しています。

しかし、これはメモリ リークが非常に速く発生する理由を説明していません。重要な部分は... println()a.size正確には。印刷しない (つまり全体を評価するStream)と、「未評価Stream」のままになります。未評価のストリームは値をキャッシュしないため、非常にスリムです。互いにストリームのチェーンが成長するため、メモリリークは依然として発生しますが、はるかに遅くなります。

これは疑問を投げかけます: なぜそれが動作するtoListのか 非常に単純です。List.map()新しい を熱心に作成しますList。限目。以前のものは参照されなくなり、GC の対象となります。

于 2013-02-15T15:10:14.627 に答える