8

大量のメモリを消費している次のコード(.net 4)があります。

struct Data
{
    private readonly List<Dictionary<string,string>> _list;

    public Data(List<Dictionary<string,string>> List)
    {
        _list = List;
    }

    public void DoWork()
    {
        int num = 0;
        foreach (Dictionary<string, string> d in _list)
        {
            foreach (KeyValuePair<string, string> kvp in d)
                num += Convert.ToInt32(kvp.Value);
        }

        Console.Write(num);

        //_list = null;
    }
}

class Test1
{
    BlockingCollection<Data> collection = new BlockingCollection<Data>(10);
    Thread th;

    public Test1()
    {
        th = new Thread(Work);
        th.Start();
    }

    public void Read()
    {
        List<Dictionary<string, string>> l = new List<Dictionary<string, string>>();
        Random r = new Random();

        for (int i=0; i<100000; i++)
        {
            Dictionary<string, string> d = new Dictionary<string,string>();
            d["1"]  = r.Next().ToString();
            d["2"]  = r.Next().ToString();
            d["3"]  = r.Next().ToString();
            d["4"]  = r.Next().ToString();

            l.Add(d);
        }

        collection.Add(new Data(l));
    }

    private void Work()
    {
        while (true)
        {
            collection.Take().DoWork();
        }
    }
}

class Program
{
    Test1 t = new Test1();
    static void Main(string[] args)
    {
        Program p = new Program();
        for (int i = 0; i < 1000; i++)
        {
            p.t.Read();
        }
    }
}

ブロッキングコレクションのサイズは10です。私の知る限り、gcは、DoWorkメソッドが完了するとすぐに、「Data」構造体の参照を収集する必要があります。ただし、プログラムがクラッシュするか、プログラムが自動的にダウンするまで、メモリは急速に増加し続けます。これは、ローエンドマシンでより頻繁に発生します(一部のマシンでは、メモリが増加しません)。さらに、次の行を追加すると"_list = null;" DoWorkメソッドの最後で、「Data」を(構造体からの)クラスに変換すると、メモリは増加しません。

ここで何が起こっているのか。ここでいくつかの提案が必要です。

更新:この問題は、.net Framework 4がインストールされている(4.5がインストールされていない)マシンで発生しています。

4

2 に答える 2

6

私が自分のコンピューターで試した結果は次のとおりです。

  1. データをクラスとして使用_list = nullし、DoWorkの最後に使用しない場合->メモリが増加します
  2. データを構造体として使用_list = nullし、DoWorkの最後に使用しない場合->メモリが増加します
  3. クラスとしてのデータと_list = nullDoWorkの最後の場合->メモリは150MBで安定します
  4. データを構造体として使用_list = nullし、DoWorkの最後に->メモリが増加します

コメントされている場合、_list = nullこの結果を見ても驚くことではありません。_listへの参照がまだあるためです。DoWork二度と呼び出されなくても、GCはそれを知ることができません。

3番目のケースでは、ガベージコレクターは期待どおりの動作をします。

4番目のケースでは、BlockingCollectionは、のData引数として渡したときにを格納しますcollection.Add(new Data(l));が、その後はどうなりますか?

  1. 新しい構造体はequalstodataで作成されます(つまり、型はクラス(参照型)であるため、構造体ではのアドレスに等しい)。data._listlListdata._listDatal
  2. 次に、引数としてそれを渡し、1で作成されたcollection.Add(new Data(l));もののコピーをdata作成します。次に、のアドレスlがコピーされます。
  3. ブロッキングコレクションは、Data要素を配列に格納します。
  4. DoWork実行すると、現在の構造体でのみ_list = null問題のある参照が削除Listされ、に保存されているすべてのコピーされたバージョンでは削除されませんBlockingCollection
  5. 次に、をクリアしない限り、問題が発生しますBlockingCollection

問題を見つける方法は?

メモリリークの問題を見つけるには、SOS(http://msdn.microsoft.com/en-us/library/bb190764.aspx)を使用することをお勧めします。

ここでは、私がどのように問題を見つけたかを示します。これはヒープだけでなくスタックも意味する問題であるため、ヒープ分析(ここでのように)を使用することは、問題の原因を見つけるための最良の方法ではありません。

1ブレークポイントを設定します_list = null(この行は機能するはずなので!!!)

2プログラムを実行します

3ブレークポイントに達したら、SOSデバッグツールをロードします(イミディエイトウィンドウに「.loadsos」と書き込みます)

4問題は、private List> _listノートが正しく廃棄されていることに起因しているようです。そのため、このタイプのインスタンスを見つけようとします。イミディエイト!DumpHeap -stat -type Listウィンドウに入力します。結果:

total 0 objects
Statistics:
      MT    Count    TotalSize Class Name
0570ffdc        1           24 System.Collections.Generic.List1[[System.Threading.CancellationTokenRegistration, mscorlib]]
04f63e50        1           24 System.Collections.Generic.List1[[System.Security.Policy.StrongName, mscorlib]]
00202800        2           48 System.Collections.Generic.List1[[System.Collections.Generic.Dictionary2[[System.String, mscorlib],[System.String, mscorlib]], mscorlib]]
Total 4 objects

問題のあるタイプは最後のものList<Dictionary<...>>です。2つのインスタンスがあり、MethodTable(タイプの一種の参照)は00202800です。

5参照を取得するには、と入力し!DumpHeap -mt 00202800ます。結果:

 Address       MT     Size
02618a9c 00202800       24     
0733880c 00202800       24     
total 0 objects
Statistics:
      MT    Count    TotalSize Class Name
00202800        2           48 System.Collections.Generic.List1[[System.Collections.Generic.Dictionary2[[System.String, mscorlib],[System.String, mscorlib]], mscorlib]]
Total 2 objects

2つのインスタンスが、アドレスとともに表示されます02618a9c0733880c

6それらがどのように参照されているかを確認するには:(!GCRoot 02618a9c最初のインスタンスの場合)または!GCRoot 0733880c(2番目のインスタンスの場合)と入力します。結果(すべての結果をコピーしたわけではありませんが、重要な部分を保持しています):

ESP:3bef9c:Root:  0261874c(ConsoleApplication1.Test1)->
  0261875c(System.Collections.Concurrent.BlockingCollection1[[ConsoleApplication1.Data, ConsoleApplication1]])->
  02618784(System.Collections.Concurrent.ConcurrentQueue1[[ConsoleApplication1.Data, ConsoleApplication1]])->
  02618798(System.Collections.Concurrent.ConcurrentQueue1+Segment[[ConsoleApplication1.Data, ConsoleApplication1]])->
  026187bc(ConsoleApplication1.Data[])->
  02618a9c(System.Collections.Generic.List1[[System.Collections.Generic.Dictionary2[[System.String, mscorlib],[System.String, mscorlib]], mscorlib]])

最初のインスタンスの場合、および:

Scan Thread 5216 OSTHread 1460
ESP:3bf0b0:Root:  0733880c(System.Collections.Generic.List1[[System.Collections.Generic.Dictionary2[[System.String, mscorlib],[System.String, mscorlib]], mscorlib]])
Scan Thread 4960 OSTHread 1360
Scan Thread 6044 OSTHread 179c

2番目のオブジェクト(分析されたオブジェクトのルートが深くない場合は、スタックに参照があることを意味すると思います)。

私たちは最終的に私たちのタイプを見るので、見ることは何が起こるかを理解するための良い方法で026187bc(ConsoleApplication1.Data[])あるはずです。Data

7オブジェクトのコンテンツを表示するには、を使用します。!DumpObj 026187bcこの場合、配列であるため、を使用します!DumpArray -details 026187bc。結果(部分的):

Name:        ConsoleApplication1.Data[]
MethodTable: 00214f30
EEClass:     00214ea8
Size:        140(0x8c) bytes
Array:       Rank 1, Number of elements 32, Type VALUETYPE
Element Methodtable: 00214670
[0] 026187c4
    Name:        ConsoleApplication1.Data
    MethodTable: 00214670
    EEClass:     00211ac4
    Size:        12(0xc) bytes
    File:        D:\Development Projects\Centive Solutions\SVN\trunk\CentiveSolutions.Renderers\ConsoleApplication1\bin\Debug\ConsoleApplication1.exe
    Fields:
              MT    Field   Offset                 Type VT     Attr    Value Name
        00202800  4000001        0     ...lib]], mscorlib]]      0     instance     02618a9c     _list
[1] 026187c8
    Name:        ConsoleApplication1.Data
    MethodTable: 00214670
    EEClass:     00211ac4
    Size:        12(0xc) bytes
    File:        D:\Development Projects\Centive Solutions\SVN\trunk\CentiveSolutions.Renderers\ConsoleApplication1\bin\Debug\ConsoleApplication1.exe
    Fields:
              MT    Field   Offset                 Type VT     Attr    Value Name
        00202800  4000001        0     ...lib]], mscorlib]]      0     instance     6d50950800000000     _list
[2] 026187cc
    Name:        ConsoleApplication1.Data
    MethodTable: 00214670
    EEClass:     00211ac4
    Size:        12(0xc) bytes
    File:        D:\Development Projects\Centive Solutions\SVN\trunk\CentiveSolutions.Renderers\ConsoleApplication1\bin\Debug\ConsoleApplication1.exe
    Fields:
              MT    Field   Offset                 Type VT     Attr    Value Name
        00202800  4000001        0     ...lib]], mscorlib]]      0     instance     6d50950800000000     _list

ここに、配列の最初の3つの要素の属性の値があります_list02618a9c、、。「nullポインタ」だと思います。6d509508000000006d509508000000006d50950800000000

ここにあなたの質問に対する答えがあります:_listガベージコレクターにファイナライズさせたいアドレスを直接含む配列(ブロッキングコレクション(6を参照)によって参照されます)があります。

8行が実行されたときに変更されていないことを確認するために、行_line = nullを実行します。

ノート

すでに述べたように、DumpHeapの使用は、値型を意味する現在のタスクにはあまり適していません。なんで?値型はヒープではなくスタックにあるためです。これを確認するのは非常に簡単です!DumpHeap -stat -type ConsoleApplication1.Data。ブレークポイントを試してください。結果:

total 0 objects
Statistics:
      MT    Count    TotalSize Class Name
00214c00        1           20 System.Collections.Concurrent.ConcurrentQueue`1[[ConsoleApplication1.Data, ConsoleApplication1]]
00214e24        1           36 System.Collections.Concurrent.ConcurrentQueue`1+Segment[[ConsoleApplication1.Data, ConsoleApplication1]]
00214920        1           40 System.Collections.Concurrent.BlockingCollection`1[[ConsoleApplication1.Data, ConsoleApplication1]]
00214f30        1          140 ConsoleApplication1.Data[]
Total 4 objects

の配列はありますが、ありDataませんData。DumpHeapはヒープのみを分析するためです。次に!DumpArray -details 026187bc、ポインタは同じ値でここにあります。!GCRootそして、行を実行する前と後の前(と)で見つけた2つのインスタンスのルートを比較すると、行だけが削除されます。実際、リストへの参照は、値型の1つのコピーからのみ削除されていますData

于 2013-01-22T00:00:34.243 に答える
4

Stephen Toubの動作の説明を読むConcurrentQueueと、動作は理にかなっています。BlockingCollectionデフォルトで使用ConcurrentQueueします。これは、その要素を32要素セグメントのリンクリストに格納します。

同時アクセスの目的で、リンクリスト内の要素は上書きされないため、32のセグメント全体の最後が消費されるまで参照されなくなりません。10個の要素の制限された容量があるため、41個の要素を生成し、31個を消費したとします。つまり、31個の消費された要素と1個のキューに入れられた要素のセグメントと、残りの9個の要素を持つ別のセグメントがあります。この時点で、41個の要素すべてが参照されているため、各要素が25MBの場合、コレクションは1GBを占めることになります。次のアイテムが消費されると、ヘッドセグメントの32個の要素すべてが参照されなくなり、収集できるようになります。

キューには10個の要素があれば十分だと思うかもしれません。これは非並行キューの場合ですが、別のスレッドが生成または消費している間、1つのスレッドがキュー内の要素を列挙することはできません。要素。

.Net 4.5フレームワークがリークしない理由は、キューを列挙している人がいない限り、要素が生成されるとすぐに要素をヌルアウトするように動作を変更したためです。列挙を開始するcollectionと、.Net4.5フレームワークでもメモリリークが発生するはずです。

_list = nullを持っているときに設定が機能する理由classは、使用されているすべての場所でリストの参照を解除できる「ボックス」ラッパーを作成しているためです。ローカル変数に値を設定すると、キューが参照しているのと同じコピーが変更されます。

がある場合に設定_list = nullが機能しない理由structは、のコピーしか変更できないためstructです。そのキューセグメントにある「元の」バージョンは、ConcurrentQueue変更する方法を提供しないため、事実上不変です。つまり、キュー内のコピーを追跡するのではなく、ローカル変数内の値のコピーのみを変更します。

于 2013-01-22T01:35:38.030 に答える