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 方法完成后立即收集“数据”结构中的引用。但是,内存一直在快速增加,直到程序崩溃或自行关闭,这在低端机器上更常见(在某些机器上内存不会增加)。此外,当我添加以下行时“_list = null;” 在 DoWork 方法结束并将“数据”转换为类(来自结构)时,内存不会增加。

这里可能发生什么。我在这里需要一些建议。

更新:问题出现在安装了 .net framework 4(安装 4.5)的机器上

4

2 回答 2

6

我在我的电脑上试过了,结果如下:

  1. 使用 Data 作为类并且_list = null在 DoWork 结束时没有 -> 内存增加
  2. 使用 Data 作为结构并且_list = null在 DoWork 结束时没有 -> 内存增加
  3. 使用 Data 作为类并_list = null在 DoWork 结束时使用 -> 内存稳定在 150MB
  4. 使用 Data 作为结构并_list = null在 DoWork 结束时使用 -> 内存增加

在被评论的情况下,_list = null看到这个结果并不奇怪。因为还有对_list的引用。即使DoWork不再调用,GC 也不知道。

在第三种情况下,垃圾收集器具有我们期望的行为。

对于第四种情况,Data当您将 BlockingCollection 作为 in 的参数传递时,BlockingCollection 会存储它collection.Add(new Data(l));,但是接下来会做什么呢?

  1. 一个新的结构data是用data._list等于创建的l(即类型List是一个类(引用类型),data._list在结构中等于Data的地址l)。
  2. 然后你将它作为参数传递,collection.Add(new Data(l));然后它会创建一个data在 1 中创建的副本。然后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调试工具(在即时窗口中写入“.load sos”)

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

显示了两个实例及其地址:02618a9c0733880c

6要查找它们是如何引用的:键入!GCRoot 02618a9c(对于第一个实例)或!GCRoot 0733880c(对于第二个实例)。结果(我没有复制所有结果,但保留了重要部分):

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

对于第二个(当分析的对象没有更深的根时,我认为这意味着它在堆栈中有引用)。

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

这里我们有_list数组的前 3 个元素的属性值:02618a9c6d509508000000006d50950800000000。我怀疑6d50950800000000是“空指针”。

在这里,我们有您问题的答案:有一个数组(由阻塞收集引用(参见 6.))直接包含_list我们希望垃圾收集器完成的地址。

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)找到的两个实例的根,则只会删除行。实际上,对列表的引用仅从值类型的 1 个副本中删除Data

于 2013-01-22T00:00:34.243 回答
4

如果您阅读Stephen Toub 对如何ConcurrentQueue工作的解释,那么这种行为是有道理的。BlockingCollection默认情况下使用ConcurrentQueue,它将其元素存储在 32 元素段的链表中。

出于并发访问的目的,链表中的元素永远不会被覆盖,因此它们不会被取消引用,直到消耗完整个 32 段的最后一个。由于您有 10 个元素的有限容量,假设您已经生产了 41 个元素并消耗了 31 个元素。这意味着您将拥有一个包含 31 个已消耗元素的片段加上一个排队元素,以及另一个包含剩余 9 个元素的片段。此时所有 41 个元素都被引用,所以如果每个元素都是 25MB,那么您的集合将占用 1GB!一旦下一个项目被消耗,头部段中的所有 32 个元素都将被取消引用并且可以被收集。

您可能认为队列中只需要 10 个元素,非并发队列就是这种情况,但不允许一个线程在另一个线程正在生产或消费时枚举队列中的元素元素。

.Net 4.5 框架没有泄漏的原因是,只要没有人枚举队列,它们就会将行为更改为元素一生成就清空。如果您开始枚举collection,即使使用 .Net 4.5 框架,您也应该会看到内存泄漏。

当你有 a 时设置_list = null起作用的原因class是你正在创建一个“盒子”包装器,它允许你在每个使用它的地方取消引用列表。在本地变量中设置值会更改队列引用的同一副本。

当您拥有 a 时设置_list = null不起作用的原因struct是您只能更改 a 的副本struct。它位于该队列段中的“原始”版本实际上是不可变的,因为ConcurrentQueue不提供更改它的方法。换句话说,您只更改本地变量中值的副本,而不是更改队列中的副本。

于 2013-01-22T01:35:38.030 回答