.NET框架4中存在高内存问题,但在4.5版本中不存在。

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。据我所知,垃圾回收器应该在“Data”结构体的DoWork方法完成后立即回收引用。然而,内存仍然以快速的速度增长,直到程序崩溃或自行降低,并且在低端机器上这种情况发生得更加频繁(在某些机器上内存不会增加)。此外,当我在DoWork方法末尾添加以下代码“_list = null;”并将“Data”转换为类(从结构体),内存不会增加。
这里可能发生了什么。我需要一些建议。
更新:该问题发生在安装了.NET Framework 4(未安装4.5)的计算机上。

实际上,这段代码是“继承”而来的。没有理由,但它可能会影响内存。 - Umer Azaz
你的代码似乎正常工作。对我来说,内存在150MB到300MB之间,并且被正确释放。我尝试了.NET 4/4.5的发布版和调试版。 - Euphoric
也许与你的Work方法中的while(true)有关? - Destrictor
2
如果您使用.NET Reflector等工具查看BlockingCollection,您将会发现.NET 4与最新的4.5版本之间有很多差异(这与此处未列出的4.5Update1不对应)。例如,内部的m_isDisposed字段现在被标记为volatile。所以,是的,它已经被更改了,可能已经修复了。您到底想要什么样的答案? - Simon Mourier
实际上,我想知道在 4.0 中使用结构体和将列表设置为 null 如何影响垃圾收集过程。在使用完对象后将其设置为 null 是一种通常不遵循的做法。如果必须这样做(由于 .net 4 中存在问题),那么我们可能需要重新审视我们的生产代码。 - Umer Azaz
显示剩余7条评论
2个回答

6
我在我的计算机上尝试了一下,结果如下:
  1. 使用Data作为类,并且在DoWork结尾没有使用_list = null -> 内存增加
  2. 使用Data作为结构体,并且在DoWork结尾没有使用_list = null -> 内存增加
  3. 使用Data作为类,并且在DoWork结尾使用了_list = null -> 内存稳定在150MB
  4. 使用Data作为结构体,并且在DoWork结尾使用了_list = null -> 内存增加
在没有注释掉_list = null的情况下,这个结果并不令人意外。因为还是有对_list的引用。即使永远不再调用DoWork,GC也无法知道它。
在第三种情况下,垃圾收集器表现出我们期望它具有的行为。
对于第四种情况,当你将Data作为参数传递给collection.Add(new Data(l));时,BlockingCollection会存储它,但接下来会发生什么呢?
  1. 创建一个新的结构体data,其data._list等于l(即类型List是类(引用类型),在结构体Data中,data._list等于l的地址)。
  2. 然后将其作为参数传递给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调试工具(在立即窗口中输入“ .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<...>>。有两个实例,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属性的值: 02618a9c, 6d50950800000000, 6d50950800000000。我怀疑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),只会有一行被移除。实际上,对于值类型 Data 的引用只被从一个副本中删除了。


感谢提供有关SOS的详细信息。我以前从未使用过它,但现在我一定会使用它。 - Umer Azaz
在这种情况下,值类型确实位于堆上。你自己说了,有一个 Data 数组。这个数组位于堆上,存储着 Data 元素。 - Gabe
确切地说,但并非所有的“数据”都在堆上,因为并非所有的“数据”都封装在数组中(例如,当作为方法参数传递时)。 - Cédric Bignon

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框架,也应该看到内存泄漏。
当你有一个class时,设置_list = null的原因是你创建了一个"box"包装器,允许你在使用列表的每个地方取消引用它。在本地变量中设置值会更改队列引用的相同副本。

当你有一个struct时,设置_list = null不起作用的原因是你只能改变struct的副本。那个在队列段中的“原始”版本实际上是不可变的,因为ConcurrentQueue没有提供一种改变它的方式。换句话说,你只是改变了本地变量中值的副本,而没有改变队列中的副本。


这非常有道理。非常感谢! - Umer Azaz

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接