堵塞和防止托管内存泄漏






4.88/5 (9投票s)
使用windbg、性能分析器和弱引用解决托管内存泄漏
引言
"预防胜于治疗。" ——伊拉斯谟
我的专长之一是发现并消除内存泄漏,尽管网上关于这个主题的信息很多,但我认为我应该分享我的方法,并可能为一般的知识添砖加瓦。在小型应用程序上,使用常规的windbg方法(如下所示,还有一些更巧妙的方法)诊断泄漏相当容易,但在大型应用程序上,这种方法往往变得更难和更复杂,因为有更多类型和更复杂的代码,并且在相同的对象树中,分辨哪些类型需要检查以及哪个实例正在泄漏不再那么容易。
快速概览
问题:
- 订阅后未取消订阅
- 第三方代码控件和软件
- 未释放对象(异常或编码不当)
- 缓存集合和静态集合
解决方案
- 使用windbg查找和解决特定问题
- 插入创建调用堆栈
- 内存分析器
- 使用弱引用堵塞特定漏洞
- 在基础设施中使用弱引用以防止程序员犯错
- 缓存字典中的弱引用
- 基础设施事件中的弱引用
- 包装第三方控件时作为屏障的弱引用
1. 使用windbg查找和解决特定问题
清除程序中托管泄漏的传统方法是:
- 下载并安装windbg(微软的win32调试器)
- 运行c:\program files\debugging tools for windows\windbg.exe
- 将其附加到有内存泄漏的重量级进程(F6)
- 然后输入以下内容启动sos:
- .loadby sos mscorwks (.net 2 - 3.5)
- .loadby sos clr (.net 4.0+)
- !dumpheap -stat -type
- 在列表中找到一个感兴趣的对象
- !dumpheap -mt
- 选择其中一个实例(保存其地址)
- !gcroot
- 这会给你所有引用你正在查看的实例的根,以及一个大致的更改代码的方向
这里有一个关于这种方法的很好的例子。(他在最后关于事件处理程序的部分有一个有趣的小额外内容)。嗯,做了一段时间之后,你会有这样的感觉:“这种方法缺少一个重要的信息”,这个信息会告诉你你正在查看哪个实例以及它在程序中属于哪里,这通常由内存分析器以创建调用堆栈的形式提供——这意味着对象创建时的调用堆栈(向你展示是谁创建了它以及它属于哪里)。
现在我们知道了这一点,我们可以在windebugging会话中请求这些信息了。
2. 插入创建调用堆栈
在应用程序代码中为你感兴趣的对象插入创建调用堆栈相当简单,你只需创建一个新的字符串成员,如下所示:
#if DEBUG
//callstack to view from windbg
string m_strCreationCallstack = Environment.StackTrace;
#endif
然后,你只需从Windbg中查看即可:
- !do <实例地址>
- 找到m_strCreationCallstack并复制其地址
- !do <找到的地址>
- 现在你知道它来自哪里了...
3. 内存分析器
有几种.NET内存分析器:
这些都是商业软件,价格不菲,但它们有一些好处... 它们以丰富的图形界面呈现,分析器会检索关于你的软件的大量信息,其中一些(比如.net memory profiler)会有有用的弹出提示建议在哪里查找问题。另一方面,这些程序是内存消耗大户,存储大量信息,有时难以导航(取决于GUI),并且通过分析器API附加,这通常意味着从启动时就与你的应用程序一起运行。总而言之:你通常会在软件开发环境中使用分析器,而不是在生产/预生产/测试环境中使用,所以如果测试人员告诉你他发现了泄漏,那这不是要使用的工具(回到windbg)。
4. 使用弱引用
现在,你可能会想“嗯——这听起来很有用”,确实如此,但它也有些危险,因为仅由弱引用持有的对象可能会在没有警告的情况下被GC回收并消失(这随时可能发生,因为GC独立于应用程序并有其自身的规则)。为了防止这种机制出现问题,我们有System.WeakReference对象,它是弱引用概念的包装器,允许我们检查IsAlive,以确保对象在使用前仍然存在。
弱引用可以通过几种方式来防止API用户(你的应用程序程序员)创建托管泄漏(如果你想知道这就是我引用中暗示的预防措施),当遇到困难的内存问题时,它们也可以作为最后的手段使用。
弱引用与特定、难以解决的问题:
例如,在我参与的一个项目中,我们有UIActions(它们是类似于WPF Command的命令设计模式的实现),这些Action对象在应用程序启动时初始化,供GUI中的所有按钮、菜单和快捷方式使用,这些Action对象旨在成为轻量级对象,只有少量逻辑代码来决定按钮是否应该启用和启动操作。但随着时间的推移,情况并非如此,很快程序员就在这些Actions中编码了成员,这导致了内存问题,因为在静态对象中保留成员会将引用的对象束缚在静态(或在本例中是单例对象)上。显而易见的解决方案是不保留任何成员,但在几个地方仍然需要它们——因此引入了弱引用,并将IsAlive添加到IsNull检查中。
用法相当直接:
WeakReference m_objDataMember = new WeakReference(); public DataRow MyProperty { get { if ( m_objDataMember != null && m_objDataMember.Target != null) { object hold = m_objDataMember.Target; if (m_objDataMember.IsAlive ) { return (DataRow)m_objDataMember.Target; } else return null; } } set { m_objDataMember.Target = value; } }
基础设施中的弱引用包装器:
C#应用程序中的主要问题之一
基础设施中的弱引用包装器:
C#应用程序(特别是GUI客户端应用程序)中的主要问题之一是,订阅(+=)一个对象的事件会让你在内存中保留,直到你取消订阅(-=)或对象调用Events.Dispose()或从内存中移除(这些情况通常非常接近,因为Events.Dispose传统上只从dispose中调用),程序员会忘记取消订阅。所以我们需要保护他们。
弱引用在事件中可以以两种主要方式使用:
A. 在注册方(+= WeakRefHandler) - 可以构造一个代理对象,我喜欢称之为 DelegateContainer,它包含一个包装请求的委托函数的函数,将调用转发给一个弱持有的成员对象(这将是你想要注册的原始EventHandler)
B. 在注册器方(事件发布者) - 当你拥有事件的所有权时,你可以做一些非常巧妙的事情:通过重写add和remove函数并构造你自己的事件,该事件使用弱引用来存储注册对象,你实现了一个 真正的弱事件。注册到此事件将不意味着以任何方式附加到内存树。
委托容器
这种方法并不完美,因为你需要为每个你想要包装的事件处理程序创建不同的包装器(MouseEventHandler != EventHandler),甚至不要想“我要建立一个很棒的新基础设施泛型工厂魔法,通过反射吐出这些东西”,因为你会陷入困境,你不想去那里... 如果你确实想探索这种可能性,请记住,没有一种解决方案是完美的(简单的API代码/良好的性能/严密的解决方案),并且查阅这些资料:
我认为还需要一个例子:(这是简单的弱事件,我倾向于使用它,因为通常有一个特定的地方我想用这种方法来堵塞漏洞。)
public class DelegateContainer { //public delegate void del<d>(d a); private EventHandler m_handler = null; private WeakReference m_weakRef = null; private MethodInfo m_observerMethod = null; private DelegateContainer() { m_handler = new EventHandler(callObserver); } private void callObserver(object obj, EventArgs e) { object hold = m_objDataMember.Target; if (m_weakRef.IsAlive) { if (m_observerMethod != null) { m_observerMethod.Invoke(m_weakRef.Target, new object[] {obj, e}); } } } private EventHandler initWeakDelegate(EventHandler handler) { m_weakRef = new WeakReference(handler.Target); m_observerMethod = handler.Method; return m_handler; } private void register(EventHandler caster, EventHandler handler) { //caster m_weakRef = new WeakReference(handler.Target); m_observerMethod = handler.Method; //return m_handler; } public static EventHandler createWeakDelegate(EventHandler e) { DelegateContainer cont = new DelegateContainer(); return cont.initWeakDelegate(e); } }
构建真正的弱事件
我在这里想展示的最后一种方法适用于你拥有事件发布者类的代码时,这让你能够为事件构建一个不同的后端,从而创建一个真正的弱事件,订阅是正常进行的(+=),并且永远不会在内存中持有你的对象:
public override event ListChangedEventHandler ListChanged
{
add
{
//collect info
object objTarg = value.Target;
int intHashKey = objTarg.GetHashCode();
//create invocation list if needed
if (m_colListChanged == null)
m_colListChanged = new Dictionary<int, KeyValuePair<MethodInfo, WeakReference>>();
lock (m_colListChanged)
{
//check that we don't have this delegate yet
if (!m_colListChanged.ContainsKey(intHashKey))
{
//check if we have this callerObject yet, if not - add it.
if (!m_colWeakRefs.ContainsKey(intHashKey))
{
WeakReference objWref = new WeakReference(objTarg);
m_colWeakRefs.Add(intHashKey, objWref);
}
//add the delegate to our list
m_colListChanged.Add(intHashKey, new KeyValuePair<MethodInfo,
WeakReference>(value.Method, m_colWeakRefs[intHashKey]));
}
}
}
remove
{
lock (m_colListChanged)
{
int intHashKey = value.Target.GetHashCode();
if (m_colListChanged.Keys.Contains(intHashKey))
m_colListChanged.Remove(intHashKey);
}
}
}
这是用于收集事件订阅的代码,现在是事件调用代码:
<span class="Apple-tab-span" style="white-space: pre;"> </span>
public void InvokeListChanged(object sender, ListChangedEventArgs e) { //a list to save removed items to keep from messing up the iterator List<int> colCodesToRemove = new List<int>(); if (m_colListChanged != null) { List<int> keyList = m_colListChanged.Keys.ToList(); foreach (int code in keyList) { //the keyVal Pair containing: [Method to Call], Object which registered(+=) the method KeyValuePair<MethodInfo, WeakReference weakWrapper; object objInvokeValue = null; if (m_colListChanged.ContainsKey(code)) objInvokeValue = m_colListChanged[code]; //get the Pair and cast it weakWrapper = (KeyValuePair<MethodInfo, WeakReference>)objInvokeValue; //check that it's still alive and has not been collected object hold = weakWrapper.Value.Target; if (weakWrapper.Value.IsAlive) { weakWrapper.Key.Invoke(weakWrapper.Value.Target, new object[] { sender, e }); } else { colCodesToRemove.Add(code); } } //remove all dead subscribers for (int i = 0; i < colCodesToRemove.Count; ++i) { int code = colCodesToRemove[i]; lock (m_colListChanged) { if (m_colListChanged.ContainsKey(code)) m_colListChanged.Remove(code); } } } } WeakReference m_objDataMember = new WeakReference(); public DataRow MyProperty { get { if (m_objDataMember != null && m_objDataMember.Target != null) { object hold = m_objDataMember.Target; if (m_objDataMember.IsAlive) return (DataRow)m_objDataMember.Target; else return null; } } set { m_objDataMember.Target = value; } }