.net 框架 4 中的高内存问题,但在框架 4.5 中不存在
本文关键字:框架 不存在 问题 高内存 net 内存 | 更新日期: 2023-09-27 17:53:24
我有以下一段代码(.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 方法完成后立即在"数据"结构中收集引用。但是,内存会继续快速增长,直到程序崩溃或自行关闭,这种情况在低端计算机上更常见(在某些计算机上内存不会增加(。此外,当我在DoWork方法的末尾添加以下行"_list = null;"并将"数据"转换为类(从结构(时,内存不会增加。
这里可能发生了什么。我在这里需要一些建议。
更新:在安装了 .net Framework 4 的计算机上出现问题(未安装 4.5(
我在计算机上尝试过的结果是:
- 将数据作为类,在 DoWork 结束时没有
_list = null
->内存增加 - 使用数据作为结构,在DoWork结束时没有
_list = null
->内存增加 - 将数据作为类,在DoWork结束时使用
_list = null
->内存稳定在150MB - 将数据作为结构,在DoWork结束时使用
_list = null
->内存增加
在评论_list = null
的情况下,看到这个结果并不奇怪。因为仍然有对_list的引用。即使再也没有调用DoWork
,GC 也无法知道。
在第三种情况下,垃圾回收器具有我们期望它具有的行为。
对于第四种情况,当您将Data
作为参数传递时,BlockingCollection 会存储collection.Add(new Data(l));
,但是该怎么办?
- 创建一个新的结构
data
,data._list
等于l
(即,类型List
是一个类(引用类型(,data._list
在结构中等于Data
l
的地址(。 - 然后,在
collection.Add(new Data(l));
中将其作为参数传递,然后创建在 1 中创建的data
的副本。然后复制l
的地址。 - 阻塞集合将
Data
元素存储在数组中。 - 当
DoWork
执行_list = null
时,它仅在当前结构中删除对有问题的List
的引用,而不是存储在BlockingCollection
中的所有复制版本中。 - 然后,除非您清除
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 个实例,方法表(该类型的一种引用(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
将显示两个实例及其地址:02618a9c
和 0733880c
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
这里我们有数组前 3 个元素的 _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
。因为转储堆只分析堆。然后!DumpArray -details 026187bc
,指针仍然在这里具有相同的值。如果你比较我们之前(用!GCRoot
(在执行行之前和之后找到的两个实例的根,只会删除行。实际上,对列表的引用仅从值类型Data
的 1 个副本中删除。
如果你读过斯蒂芬·图布(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
因为它不提供更改它的方法。换句话说,您只更改局部变量中值的副本,而不是在队列中更改副本。