.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的内容。