.NET 虽然拥有强大易用的垃圾回收机制, 但并不是因为这样, 你就可以对资源管理放任不管, 其实在稍不注意的时候, 可能就造成了资源泄露, 甚至因此导致系统崩溃, 到那时再来排查问题就已经是困难重重.
一, 知识点简单介绍
常见的资源泄露有:
内存泄漏: 非托管资源没有释放, 非静态对象注册了静态实例.
GDI 泄露: 字体.
句柄泄露: Socket 或线程.
用户对象泄露: 移除的对象未释放.
二, 具体实例
1. 内存泄漏
很常见的现象是分不清哪些对象需要释放, 对于控件, Stream 等一些非托管资源也只管新增, 却没有释放, 功能是实现了, 却埋了颗不小的雷.
- private void button1_Click(object sender, EventArgs e)
- {
- for(int i=0;i<1000;i++)
- this.Controls.Add(new TabPage());
- }
- private void button1_Click(object sender, EventArgs e)
- {
- new Form2.ShowDialog();
- }
如果你觉得写这样的代码很 Cool, 很简洁, 你在项目中也有这么写代码, 那你就碰到大麻烦了, 你试试在上面 Form2 中开个大一点的数组来检查内存, 然后运行, 按几下按钮, 你就会发现, 内存一直增加, 即使你调用了 GC 也无济于事. 所以, 对于此类非托管资源要记住释放, 用完即废可以采用 using 关键字.
- public Form2()
- {
- InitializeComponent();
- MyApp.FormChanged += FormChanged;
- }
上面这个例子中, MyApp 是一个静态类, 如果在实例对象中向这种类里面注册了事件, 而又没有取消注册, 这样也会遇到大麻烦, 即使在外部已经记得调用了 Form2 的 Dispose 也是没用的.
解决方案
注意托管资源和非托管资源的释放区别, 非托管资源是需要手动释放的.
使用 using 关键字, 避免忘记 Dispose 的情况, 如上面的 ShowDialog 问题.(using 中还起到了 try-catch 的作用, 避免由于异常未调用 Dispose 的情况)
使用 UnLoad 事件或者析构函数, 对注册的全局事件进行取消注册.
特别注意自定义
组件的稳定性
更重要, 发生问题时影响也更广.
注意继承 IDisposable 接口, 进行资源释放
2. GDI 泄露
一般会跟字体相关, 例如我曾在 Android 上用 Cocos2d 做一个小游戏时频繁地切换字体, Dev 控件的 Font 属性赋值也会有这种现象.
XXX.Font = new Font(...)
解决方案
这个问题我目前是采用字体池来解决, 类似线程池的概念, 相同 Key 值取同一个对象. 若有更好方案欢迎留言讨论
3. 句柄泄露
一般跟 Socket 和 Thread(线程) 有关
- for(int i=0;i<1000;i++){
- new Thread(()=>{
- Thread.Sleep(1000);
- }).Start();
- }
解决方案
Socket 的场景暂时没遇到.
线程问题采用线程池相关的辅助类能有效解决, 例如 ThreadPool,Task,Parallel.
4. 用户对象泄露
一般跟移除的对象未释放有关
- private void button1_Click(object sender, EventArgs e)
- {
- tab.Remove(tabPage);
- }
三, 最后特别奉送一个内存释放的大招
- [DllImport("kernel32.dll", EntryPoint = "SetProcessWorkingSetSize")]
- public static extern int SetProcessWorkingSetSize(IntPtr process, int minSize, int maxSize);
- /// <summary>
- /// 释放内存
- /// </summary>
- public static void ClearMemory()
- {
- GC.Collect();
- GC.WaitForPendingFinalizers();
- if (Environment.OSVersion.Platform == PlatformID.Win32NT)
- {
- SetProcessWorkingSetSize(System.Diagnostics.Process.GetCurrentProcess().Handle, -1, -1);
- }
- }
调用以上 API 能让你的内存一下爆减, 是不是很给力, 一调用内存就降下来了. But, 先别高兴太早, 这其实是伪释放, 只是暂时解决内存大量泄漏导致系统崩溃的应急处理方案. 具体原因参考: SetProcessWorkingSetSize 函数的骗局, 关键信息: 物理内存转虚拟内存, 涉及磁盘读写. 好处坏处都贴出来了, 是否需要使用请君自己斟酌.
四, 总结
实际上由于各个开发人员的水平跟接触面不同, 又没有经过统一的培训 (各个人对资源释放的理解与关注度不同, 或者写代码时就没考虑内存未被释放这种问题), 发现问题的时候项目往往已经做到了一个阶段, 系统也比较庞大了, 这种时候才发现内存泄露的问题确实是很头疼的.
资源泄露的场景往往是相互关联的, 发生最多的就是内存泄漏, 而除了写法可能有问题外, 也可能是因为句柄泄露或用户对象泄露引起的.
五, 参考资料
C# 内存泄露与资源释放 经验总结
字体池技术实现
.NET 多线程知识快速学习 https://www.cnblogs.com/yokeqi/p/11885819.html
来源: http://www.bubuko.com/infodetail-3301343.html