众所周知,内存管理和如何避免内存泄漏(memory leak)一直是软件开发的难题。不要说C、C++等非托管(unmanaged)语言,即使是Java、.NET等托管(managed)语言,尽管有着完善的垃圾回收器(GC),内存泄漏也经常发生。不过,这并非GC的bug或设计缺陷,而是因为在开发时有太多能够导致内存泄漏的方式了,尤其是对于绑定(Binding)、事件(Event)、行为(Behavior)满天飞的WPF/UWP应用。

对于托管类应用,内存泄漏主要可以分为两大类:托管类内存泄漏(managed memory leak)和非托管类内存泄漏(unmanaged memory leak)。

1.托管类内存泄漏(managed memory leak)

这种泄漏发生的根本原因是由于无用的、本该被回收的托管类对象(managed objects)由于被“无意的”(unintended)的引用而导致无法被回收。

这与GC的工作原理有关:在进行垃圾回收时,应用将挂起所有线程,这样GC就可以遍历所有的GC Root对象,并将它们标记为”不可回收“,接着GC进一步将它们所引用的所有对象也都标记为”不可回收“。这个过程将一直递归进行下去,直到无法继续。所有未被标记为”不可回收“的对象都被GC视为垃圾对象,最终都将被回收。简而言之,GC对“无用”对象的识别机制很简单:判断对象是否被“GC Root”对象所引用。

可以被视为GC Root的对象主要包括三大类:

a.正在执行的线程的“活跃”栈(Live Stack of the running threads),包括正在执行的方法的参数、局部变量、寄存器变量等;
b.静态变量(Static variables);
c.通过interop传递给COM对象(其内存回收采用“引用计数”机制)的托管对象

如果一个对象被生存期更长的对象(例如全局对象或静态类)所引用,那么在进行GC时,即使它已经不会再被用到,也会被标记为”不可回收“,这就是内存泄漏 。当然,被没有被标记为”不可回收“的对象(“垃圾”对象)所引用是不会阻止被引用对象被回收的,这种情况就不算是内存泄漏。

通常,导致无用的对象被GC Root无意引用的常见场景有以下几种:(注意这里只讨论”无意“的引用,开发者通过静态变量或生存期超长的对象建立的”有意“的引用不在讨论之列)

1)Event订阅

普通的事件订阅(event subscribing),例如代码source.SomeEvent += new SomeEventHandler(someObject.MyEventHandler),将创建一个从source到someObject的强引用(strong reference),如果source对象的生存期比someObject的长,那么将产生内存泄漏。

如一个在WPF/UWP中非常常见的场景:

    public partial class UserControl1 : UserControl
{
public UserControl1()
{
InitializeComponent(); Application.Current.MainWindow.SizeChanged += this.MainWindow_SizeChanged; } private void MainWindow_SizeChanged(object sender, SizeChangedEventArgs e)
{
Debug.WriteLine($"主窗体size改变:{e.NewSize}");
}
}

UserControl1订阅了MainWindow的SizeChanged事件,那么MainWindow将保持一个对UserControl1的引用。如果MainWindow的生存期比UserControl1长,那么将产生内存泄漏。

  解决这类内存泄漏的方法有:

a)手动取消订阅(unsubscribe)

可以将上述代码修改如下:

    public partial class UserControl1 : UserControl
{
public UserControl1()
{
InitializeComponent();
this.Loaded += this.UserControl1_Loaded;
this.Unloaded += this.UserControl1_Unloaded;
} private void UserControl1_Unloaded(object sender, RoutedEventArgs e)
{
Application.Current.MainWindow.SizeChanged -= this.MainWindow_SizeChanged;
} private void UserControl1_Loaded(object sender, RoutedEventArgs e)
{
Application.Current.MainWindow.SizeChanged += this.MainWindow_SizeChanged;
} private void MainWindow_SizeChanged(object sender, SizeChangedEventArgs e)
{
Debug.WriteLine($"主窗体size改变:{e.NewSize}");
}
}

由于Framework的Loaded和Unloaded事件都是成对出现,因此,可以保证当UserControl1被从Visual Tree卸载时,对MainWindow的SizeChanged被及时订阅,从而解除MainWindow对UserControl1的引用,避免内存泄漏。

  b)弱事件模式(Weak Event Pattern):使用WeakReference或 WeakEventManager,或第三方的库(如Prism的EventAggregator)。这里只举一个WeakReference的例子,关于后者可以参考MSDN文档:https://docs.microsoft.com/en-us/dotnet/desktop/wpf/advanced/weak-event-patterns

还是上面的例子,用WeakReference改写后代码如下:

   public class WeakEventHandler<TEventArgs> where TEventArgs : SizeChangedEventArgs
{
public WeakReference Reference { get; } public MethodInfo Method { get; } public WeakEventHandler(SizeChangedEventHandler eventHandler)
{
this.Handler = eventHandler;
this.Reference = new WeakReference(eventHandler.Target);
Method = eventHandler.Method;
} public SizeChangedEventHandler Handler { get; } public void Invoke(object sender, TEventArgs e)
{
object target = Reference.Target;
if (null != target)
{
Method.Invoke(target, new object[] { sender, e });
}
} public static implicit operator SizeChangedEventHandler(WeakEventHandler<TEventArgs> weakHandler)
{ return weakHandler.Handler;
} } /// <summary>
/// Interaction logic for UserControl1.xaml
/// </summary>
public partial class UserControl1 : UserControl
{
public UserControl1()
{
InitializeComponent(); Application.Current.MainWindow.SizeChanged += new WeakEventHandler<SizeChangedEventArgs>(this.MainWindow_SizeChanged); } private void MainWindow_SizeChanged(object sender, SizeChangedEventArgs e)
{
Debug.WriteLine($"主窗体size改变:{e.NewSize}");
}
}

  

  c)尽可能利用匿名函数(anonymous method)并避免“捕获”对象的任何成员(member),如上面的例子可以改为:

    public partial class UserControl1 : UserControl
{
public UserControl1()
{
InitializeComponent(); Application.Current.MainWindow.SizeChanged += (s, e) =>
{
Debug.WriteLine($"主窗体size改变:{e.NewSize}");
}; }
}

2)在匿名函数中捕获对象的成员(member)

上面提到将event hander换成匿名函数并避免捕获对象的成员可以避免内存泄漏。换句话说,如果匿名函数捕获了对象的成员,就可能导致内存泄漏。如上面匿名函数的例子换成下面的:

    public partial class UserControl1 : UserControl
{
public UserControl1()
{
InitializeComponent(); Application.Current.MainWindow.SizeChanged += (s, e) =>
{
      Debug.WriteLine($"{e.NewSize.Width - this.Width}"); }; }
}

这里,由于UserControl1的成员Width被匿名函数捕获,结果导致整个UserControl1的实例也被MainWindow所引用,从而产生内存泄漏。

这类泄漏的解决办法可能很简单——使用局部变量代替对象的成员:

    public partial class UserControl1 : UserControl
{
public UserControl1()
{
InitializeComponent(); var w = this.Width;
Application.Current.MainWindow.SizeChanged += (s, e) =>
{
Debug.WriteLine($"{e.NewSize.Width - w}");
}; }
}

3)不正确的Binding(WPF)

  如果绑定的不是DependencyProperty而且没有实现INotifyPropertyChanged,那么将产生内存泄漏。这与WPF的Binding的实现机制有关:如果绑定的是DependencyProperty或一个实现了INotifyPropertyChanged的对象的属性,那么WPF将利用Weak events模式,不会产生内存泄漏。否则,WPF将不得不诉诸于订阅System.ComponentModel.PropertyDescriptor类的ValueChanged事件来监听绑定source的属性值的改变。问题在于,这将导致CLR创建一个从PropertyDescriptor到绑定source对象的一个强引用。多数情况下,CLR将用一个全局列表保存这个引用。这无疑将导致内存泄漏。

  不过这种内存泄漏只有当BindingMode为OneWay或TwoWay时才会发生。当BindingMode为OneTime或OneWayToSource时,CLR不会创建强引用,即使Binding的不是DependencyProperty而且没有实现INotifyPropertyChanged。

与此类似的是绑定没有实现INotifyCollectionChanged接口的Collection,这是WPF将创建一个到这个Collection的强引用,产生内存泄漏。

4)WPF中x:Name导致的内存泄漏

  如果Xaml的元素用x:Name进行了命名,那么WPF将创建一个到该元素的全局的强引用。例如:

     <local:UserControl1 x:Name="MyUserControl1"/>

那么用code behind动态地将MyUserControl1从其父容器移除并不能真正导致该元素可以被回收,虽然看似MyUserControl1已经被移除了。

        private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
rootPanel.Children.Remove(this.MyUserControl1);
this.MyUserControl1 = null; }

解决办法也很简单:

        private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
this.UnregisterName("MyUserControl1");
rootPanel.Children.Remove(this.MyUserControl1);
this.MyUserControl1 = null;
}

到目前为止,我们谈的都是托管内存(managed memory),这类内存是由GC管理的。非托管内存(unmanaged memory)则完全是另一回事事,下面简单讨论以下非托管内存泄漏。

2.非托管类内存泄漏(unmanaged memory leak)

下面通过一个简单的列子来说明这个问题:

public class SomeClass
{
private IntPtr _buffer; public SomeClass()
{
_buffer = Marshal.AllocHGlobal(1000);
} }

上面的对象在创建时通过Marshal.AllocHGlobal()分配了一块非托管内存。在底层,AllocHGlobal()调用了Win32的Kernel32.dll的LocalAlloc()函数。如果没有显式调用Marshal.FreeHGlobal()来释放这块内存,那么这块非托管类内存将被视为已占用,将长期停驻在堆内存,这正是典型的非托管类内存泄漏。

要解决这个问题,除了主动调用Marshal.FreeHGlobal(),还有一种简单直接的方法是在析构器里调用该方法,如:

    public class SomeClass
{
private IntPtr _buffer; public SomeClass()
{
_buffer = Marshal.AllocHGlobal(1000);
// do stuff without freeing the buffer memory
}
~SomeClass()
{
if (this._buffer != IntPtr.Zero)
{
Marshal.FreeHGlobal(_buffer);
_buffer =
IntPtr.Zero;
}
}
}

在SomeClass被回收时,Destructor必然被调用(除非对这个对象调用了GC.SuppressFinalize),进而调用Marshal.FreeHGlobal(),这块非托管内存被回收,避免了内存泄漏。这意味着,只要没有托管类内存泄漏导致SomeClass无法被回收,这块非托管内存都能被回收。

在Constructor里分配非托管内存,在Destructor里释放, 这个解决方案似乎完美无缺。但是该方案的问题有二:首先,如果SomeClass因为托管类内存泄漏无法被回收,那么其非托管资源将无法被释放;其次,一个无用的托管对象何时被回收也就是说其Destructor什么时候被调用是不确定的,取决于GC。对于后一种情形的严重性,不妨考虑一个极端的例子:程序创建了大量小托管对象,而且这些对象都分配大量非托管内存。尽管没有托管类内存泄漏,这些小托管对象都可以回收,但是由于GC只能看到托管内存,看不到非托管内存,于是它认为不需要进行垃圾回收。情况严重时,将导致应用占用大量内存,其影响不亚于内存泄漏。

第二个问题的解决方案是实现Dispose模式,并在对象不再使用时尽早调用Dispose方法。

  public class SomeClass : IDisposable
{ private IntPtr _buffer; // To detect redundant calls
private bool _disposed = false; public SomeClass()
{
_buffer = Marshal.AllocHGlobal(1000);
} ~SomeClass() => Dispose(false); // Public implementation of Dispose pattern callable by consumers.
public void Dispose()
{
Dispose(true);

       //加上这句后,则如果已经被disposed,则在回收时不需要调用析构器(调用析构器对性能有一定影响)
GC.SuppressFinalize(this);
} // Protected implementation of Dispose pattern.
protected virtual void Dispose(bool disposing)
{
if (_disposed)
{
return;
} if (disposing)
{
// TODO: dispose managed state (managed objects).
} // TODO: free unmanaged resources (unmanaged objects) and override a finalizer below.
// TODO: set large fields to null.
if (this._buffer != IntPtr.Zero)
{
Marshal.FreeHGlobal(_buffer);
_buffer =
IntPtr.Zero;
}
_disposed = true;
}
}

(上面的Dispose模式是MSDN和Resharper都推荐的模式,其中黑体字为新增代码)这意味着,当实现IDisposable接口的对象不再有用时,应该尽早调用其Dispose()方法。关于Dispose模式,这里有必要补充一点:在有些存在托管内存泄漏的情况下,我们不能被动依靠GC在销毁一个垃圾对象时调用它的析构器调用Dispose,因为由于托管内存泄漏,这个对象可能无法被GC回收。如下面的例子:

    public class SomeClass : IDisposable
{ // To detect redundant calls
private bool _disposed = false; public SomeClass()
{
Application.Current.Deactivated += this.Current_Deactivated;
} private void Current_Deactivated(object sender, EventArgs e)
{
//do something
}
~SomeClass() => Dispose(false); // Public implementation of Dispose pattern callable by consumers.
public void Dispose()
{
Dispose(true); //加上这句后,则如果已经被disposed,则在回收时不需要调用析构器(调用析构器对性能有一定影响)
GC.SuppressFinalize(this);
} // Protected implementation of Dispose pattern.
protected virtual void Dispose(bool disposing)
{
if (_disposed)
{
return;
} if (disposing)
{
// TODO: dispose managed state (managed objects).
Application.Current.Deactivated -= this.Current_Deactivated;
} // TODO: free unmanaged resources (unmanaged objects) and override a finalizer below.
// TODO: set large fields to null. _disposed = true;
}
}

由于被全局对象Application引用,在应用结束前这个对象都无法被回收的,因此其析构器也不会被调用。这时就必须手动调用其Dispose()方法,否则将产生托管内存泄漏。

值得一提的是,上面的例子中,由于我们必须手动调用Dispose(),所以也就不再需要析构器里那些代码,也不需要判断_disposed是否为true。因此Dispose模式在这个例子中可以大大简化:

    public class SomeClass : IDisposable
{
public SomeClass()
{
Application.Current.Deactivated += this.Current_Deactivated;
} private void Current_Deactivated(object sender, EventArgs e)
{
//do something
} // Public implementation of Dispose pattern callable by consumers.
public void Dispose()
{
Application.Current.Deactivated += this.Current_Deactivated;
} }

  总结:本文简单讨论了.NET/WPF中内存泄漏的类型,产生的原因,GC的工作原理,常见的内存泄漏场景以及相应的解决方案,正确的Dispose模式等。由于实际开发中内存泄漏问题的复杂性,本文未涉及的内存泄漏场景还有很多,如TextBox的Undo缓存泄漏、Attached Behaviors等。(原创文章,感谢阅读,欢迎批评指正,专注请注明出处)

参考文章:
http://dotnet.agilekiwi.com/blog/2010/04/memory-leaks-in-managed-code.html
https://michaelscodingspot.com/ways-to-cause-memory-leaks-in-dotnet
https://michaelscodingspot.com/find-fix-and-avoid-memory-leaks-in-c-net-8-best-practices/
https://stackoverflow.com/questions/18542940/can-bindings-create-memory-leaks-in-wpf/18543350#18543350
https://blog.jetbrains.com/dotnet/2014/09/04/fighting-common-wpf-memory-leaks-with-dotmemory/
https://docs.microsoft.com/en-us/dotnet/standard/garbage-collection/implementing-dispose

深入理解.NET/WPF内存泄漏的更多相关文章

  1. WPF 内存泄漏优化经历

    最近公司有个CS客户端程序,有个登录界面,有个程序的主界面,程序支持注销功能,但是在注销后,客户端的内存一直以40M-50M的速度递增,因此猜测,应该是WPF程序出现了内存泄漏.下面主要记录优化内存泄 ...

  2. wpf内存泄漏问题

    http://www.cnblogs.com/Cindys/archive/2012/05/17/2505893.html http://blogs.msdn.com/b/jgoldb/archive ...

  3. Webservice WCF WebApi 前端数据可视化 前端数据可视化 C# asp.net PhoneGap html5 C# Where 网站分布式开发简介 EntityFramework Core依赖注入上下文方式不同造成内存泄漏了解一下? SQL Server之深入理解STUFF 你必须知道的EntityFramework 6.x和EntityFramework Cor

    Webservice WCF WebApi   注明:改编加组合 在.net平台下,有大量的技术让你创建一个HTTP服务,像Web Service,WCF,现在又出了Web API.在.net平台下, ...

  4. 内存泄漏在 WPF 和 Silverlight 提防

    瑞奇韭菜礼物 ︰ 内存泄漏在 WPF 和 Silverlight 提防 内存泄漏在 WPF 和 Silverlight 提防 WPF 和 Silverlight 允许您定义您的用户界面,用最少的代码将 ...

  5. 深入理解Node.js中的垃圾回收和内存泄漏的捕获

    深入理解Node.js中的垃圾回收和内存泄漏的捕获 文章来自:http://wwsun.github.io/posts/understanding-nodejs-gc.html Jan 5, 2016 ...

  6. Android性能优化之利用LeakCanary检测内存泄漏及解决办法

    前言: 最近公司C轮融资成功了,移动团队准备扩大一下,需要招聘Android开发工程师,陆陆续续面试了几位Android应聘者,面试过程中聊到性能优化中如何避免内存泄漏问题时,很少有人全面的回答上来. ...

  7. Handler系列之内存泄漏

    本篇简单的讲一下平常使用Handler时造成内存泄漏的问题. 什么是内存泄漏?大白话讲就是分配出去的内存,回收不回来.严重会导致内存不足OOM.下面来看一下造成内存泄漏的代码: public clas ...

  8. java内存泄漏的几种情况

    转载于http://blog.csdn.net/wtt945482445/article/details/52483944 Java 内存分配策略 Java 程序运行时的内存分配策略有三种,分别是静态 ...

  9. Android内存泄漏分享

    内容概述 内存泄漏和内存管理相关基础. Android中的内存使用. 内存分析工具和实践. 以下内容不考虑非引用类型的数据,或者将其等同为对应的引用类型看待--一切皆对象. 内存泄漏概念 不再使用的对 ...

随机推荐

  1. SSY的队列 hash+记忆化

    题目描述 \(SSY\) 是班集体育委员,总喜欢把班级同学排成各种奇怪的队形,现在班级里有 \(N\) 个身高互不相同的同学,请你求出这 \(N\) 个人的所有排列中任意两个相邻同学的身高差均不为给定 ...

  2. Spring Cloud Gateway原理

    1.使用 compile 'org.springframework.cloud:spring-cloud-starter-gateway' 2.包结构 actuate中定义了一个叫GatewayCon ...

  3. 【鸿蒙开发板试用报告】用OLED板实现FlappyBird小游戏(上)

    总是做各种Demo,是时候做个什么小应用来练练手了.踌躇了很久,果然还是搞个小游戏才有意思.想到几年前风靡全球的FlappyBird,一个屏幕一个按钮就足够了,正好适合.OLED屏幕.按键的驱动已经有 ...

  4. Flink处理函数实战之五:CoProcessFunction(双流处理)

    欢迎访问我的GitHub https://github.com/zq2599/blog_demos 内容:所有原创文章分类汇总及配套源码,涉及Java.Docker.Kubernetes.DevOPS ...

  5. .net core quartz job作业调度管理组件

    定时作业对于多数系统来说,都会用到,定时作业的实现方式可以有多种方式,简单的方式用Timer就可以实现,但是达不到通用的效果,本文采用Quartz基础组件来搭建一套企业通用的作业调度管理服务,希望对于 ...

  6. 金九银十已到!掌握这300道java高频面试题,助你面试BAT无忧!

    前言 不知不觉已经到了九月了,回首看年初的时候简直像做梦一样.不得不说时间真的是无情一般的流逝,题外话就不多说了!回归正题,现在已经到了今年最后一波大好的跳槽涨薪的时机了,错过了这一次可能你就得等到明 ...

  7. Java8用了这么久了,Stream 流用法及语法你都知道吗?

    1.简介 Stream流 最全的用法Stream 能用来干什么?用来处理集合,通过 使用Stream API 对集合数据进行操作,就类似于使用 SQL 执行的数据库查询,Stream API 提供了一 ...

  8. 如何使用Camtasia制作动态动画场景?

    也许在学习编辑视频的你知道Camtasia 2019(win系统),知道Camtasia的视频编辑功能,录制屏幕功能,但你可能想不到,Camtasia还可以制作动态动画场景.跟我一起学习一下吧! 一. ...

  9. guitar pro系列教程(十三):Guitar Pro教程之打谱使用技巧

    前面我们有讲过关于{cms_selflink page='index' text='Guitar Pro'}在声音方面的一些使用技巧,Guitar Pro在打谱,试听,伴奏方面对于刚学吉他作谱的朋友们 ...

  10. k8s内网安装部署(二)

    续上篇 https://www.cnblogs.com/wangql/p/13397034.html 一.kubeadm安装 1.kube-proxy开启ipvs的前置条件 modprobe br_n ...