.NET陷阱之三:“正确”使用控件也会造成内存泄露

在我们的代码中,有时会在控件中添加对数据对象的引用。比如使用树节点的Tag属性保存相应的对象,以便在界面操作中能简单的进行访问。因为其它地方不会引用这些数据,所以我们期望在控件被销毁时,垃圾回收机制能回收相应的内存。但当软件运行了一段时间后,内存使用量会变得非常大。下面是简化后的示例代码: 

 1 using System;
 2 using System.Windows.Forms;
 3 
 4 namespace MemoryLeak
 5 {
 6     public class MainForm : Form
 7     {
 8         private Button holderButton;
 9         private Button controlButton;
10         private FlowLayoutPanel panel;
11         private object checkGc;
12 
13         public MainForm()
14         {
15             DumpMemoryUsage("before allocate checkGc.");
16             checkGc = MakeLargeObject();
17             DumpMemoryUsage("after allocate checkGc.");
18 
19             holderButton = new Button();
20             holderButton.Enabled = false;
21             holderButton.AutoSize = true;
22             holderButton.Text = "The button holds large object.";
23             DumpMemoryUsage("before allocate holderButton.Tag.");
24             holderButton.Tag = MakeLargeObject();
25             DumpMemoryUsage("after allocate holderButton.Tag.");
26 
27             controlButton = new Button();
28             controlButton.AutoSize = true;
29             controlButton.Text = "The button controls holderButton.";
30             controlButton.Click += (sender, e) =>
31             {
32                 DumpMemoryUsage("before release checkGc and holderButton.Tag.");
33                 panel.Controls.Remove(holderButton);
34                 holderButton.Dispose();
35                 holderButton = null;
36 
37                 checkGc = null;
38                 DumpMemoryUsage("after release checkGc and holderButton.Tag.");
39             };
40 
41             panel = new FlowLayoutPanel();
42             panel.AutoSize = true;
43             panel.FlowDirection = FlowDirection.TopDown;
44             panel.Controls.Add(controlButton);
45             panel.Controls.Add(holderButton);
46 
47             Controls.Add(panel);
48         }    
49 
50         private void DumpMemoryUsage(string msg)
51         {
52             GC.Collect();
53             Console.WriteLine(msg);
54             Console.WriteLine(GC.GetTotalMemory(true));
55         }
56 
57         private object MakeLargeObject()
58         {
59             var largeObject = new object[100];
60             for (int i = 0; i < largeObject.Length; ++i)
61             {
62                 var array = new int[100][];
63                 largeObject[i] = array;
64                 for (int j = 0; j < array.Length; ++j)
65                 {
66                     array[j] = new int[100];
67                 }
68             }
69 
70             return largeObject;
71         }
72     }
73 
74     static class Program
75     {
76         static void Main()
77         {
78             Application.Run(new MainForm());
79         }
80     }
81 }

代码中的checkGc变量是为了在输出中确认垃圾回收已经进行了。下面是输出结果:

 1 before allocate checkGc.
 2 281576
 3 after allocate checkGc.
 4 4605632
 5 before allocate holderButton.Tag.
 6 4606384
 7 after allocate holderButton.Tag.
 8 8930480
 9 before release checkGc and holderButton.Tag.
10 8940016
11 after release checkGc and holderButton.Tag.
12 4616824

由第4行的输出可以看出,代码中创建的每个大对象占用了大约4M的内存。问题在于,我们在代码的第32-38行中已经将holderButter从panel中移除,调用了其Dispose方法,将其设置为null,另外也将checkGc设置为null。但第12的的输出却表明,只有一个大对象被回收了!为了找出问题所在,我使用ANTS Memory Profier查看了相应的内存使用情况,如下图所示:

从中可以看出,确实有一个对象没有被回收。继续查看此对象的引用链:

原来是Control.cachedLayoutEventArgs在作怪!

现在问题比较清楚了:虽然我们已经销毁了holderButton,并不再引用它,但是.NET的内部代码仍然在引用它,而holderButton.Tag所引用的对象自然也不能被回收了。

针对我们的问题,只需要在36行的位置加上holderButton.Tag = null就可以了。而更通用的情况,则应该在Disposed事件中(或重写相应的方法)将对数据的引用设置为null。

在网上搜索cachedLayoutEventArgs,发现也有人遇到相关的问题,可以参考http://book.3you.cc/bc/Print.asp?ArticleID=297177的内容。

posted @ 2013-04-03 11:21  Bruce Bi  阅读(915)  评论(3编辑  收藏  举报