任何有经历的.NET开发人员都知道,即使.NET应用程序具有废物收回器,内存走漏一直会发作。 并不是说废物收回器有bug,而是咱们有多种办法能够(轻松地)导致保管语言的内存走漏。
内存走漏是一个偷偷摸摸的坏家伙。 很长时刻以来,它们很容易被忽视,而它们也会渐渐破坏应用程序。 随着内存走漏,你的内存耗费会增加,然后导致GC压力和功能问题。 终究,程序将在发作内存不足反常时溃散。
在本文中,咱们将介绍.NET程序中内存走漏的最常见原因。 一切示例均运用C#,但它们与其他语言也相关。
定义.NET中的内存走漏
在废物收回的环境中,“内存走漏”这个术语有点违反直觉。 当有一个废物收回器(GC)负责搜集一切东西时,我的内存怎么会走漏呢?
这里有两个中心原因。 第一个中心原因是你的目标仍被引证但实际上却未被运用。 由于它们被引证,因而GC将不会搜集它们,这样它们将永久保存并占用内存。 例如,当你注册了事件但从不注销时,就有可能会发作这种状况。 咱们称其为保管内存走漏。
第二个原因是当你以某种方式分配非保管内存(没有废物收回rar)而且不开释它们。 这并不难做到。 .NET本身有很多会分配非保管内存的类。 简直一切触及流、图形、文件、rar压缩文件破解系统或网络调用的操作都会在背面分配这些非保管内存。 一般这些类会完结 Dispose 办法,以开释内存。 你自己也能够运用特殊的.NET类(如Marshal)或PInvoke轻松地分配非保管内存。
许多人都以为保管内存走漏根本不是内存走漏,由于它们仍然被引证,而且理论上能够被收回。 这是一个定义问题,我的观点是它们确实是内存走漏。 它们具有无法分配给另一个实例的内存,终究将导致内存不足的反常。 关于本文,我会将保管内存走漏和非保管内存走漏都归为内存走漏。
以下是最常见的8种内存泄露的状况。 前6个是保管内存走漏,后2个是非保管内存走漏:
1.订阅Events
.NET中的Events因导致内存走漏而臭名远扬。 原因很简单:订阅事件后,该目标将保留对你的类的引证。 除非你运用不捕获类成员的匿名办法。 考虑以下示例:
public class MyClass { public MyClass(WiFiManager wiFiManager) { wiFiManager.WiFiSignalChanged += OnWiFiChanged; } private void OnWiFiChanged(object sender, WifiEventArgs e) { // do something } }
假定wifiManager的寿命超过MyClass,那么你就现已造成了内存走漏。 wifiManager会引证MyClass的任何实例,而且废物收回器永久不会收回它们。
Event确实很危险,我写了整整一篇关于这个论题的文章,名为《5 Techniques to avoid Memory Leaks by Events in C# .NET you should know.》
所以,你能够做什么呢? 在提到的这篇文章中,破解rar密码有几种很好的形式能够避免和Event有关的内存走漏。 无需具体阐明,其间一些是:
- 注销订阅事件。
- 运用弱句柄(weak-handler)形式。
- 假如可能,请运用匿名函数进行订阅,而且不要捕获任何类成员。
2.在匿名办法中捕获类成员
虽然能够很明显地看出事件机制需求引证一个目标,可是引证目标这个工作在匿名办法中捕获类成员时却不明显了。
这里是一个比如:
public class MyClass { private JobQueue _jobQueue; private int _id; public MyClass(JobQueue jobQueue) { _jobQueue = jobQueue; } public void Foo() { _jobQueue.EnqueueJob(() => { Logger.Log($"Executing job with ID {_id}"); // do stuff }); } }
在代码中,类成员_id是在匿名办法中被捕获的,因而该实例也会被引证。 这意味着,虽然JobQueue存在并现已引证了job托付,但它还将引证一个MyClass的实例。
处理方案可能非常简单——分配局部变量:
public class MyClass { public MyClass(JobQueue jobQueue) { _jobQueue = jobQueue; } private JobQueue _jobQueue; private int _id; public void Foo() { var localId = _id; _jobQueue.EnqueueJob(() => { Logger.Log($"Executing job with ID {localId}"); // do stuff }); } }
经过将值分配给局部变量,不会有任何内容被捕获,而且避免了潜在的内存走漏。
3.静态变量
我知道有些开发人员以为运用静态变量一直是一种不好的做法。 虽然有些极点,但在议论内存走漏时确实需求留意它。
让咱们考虑一下废物搜集器的工作原理。 基本思想是GC遍历一切GC Root目标并将其标记为“不行搜集”。 然后,GC转到它们引证的一切目标,并将它们也标记为“不行搜集”。 最终,GC搜集剩余的一切内容。
那么什么会被以为是一个GC Root?
- 正在运转的线程的实时仓库。
- 静态变量。
- 经过interop传递到COM目标的保管目标(内存收回将经过引证计数来完结)。
这意味着静态变量及其引证的一切内容都不会被废物收回。 这里是一个比如:
public class MyClass { static List<MyClass> _instances = new List<MyClass>(); public MyClass() { _instances.Add(this); } }
假如你出于某种原因而决议编写上述代码,那么任何MyClass的实例将永久留在内存中,然后导致内存走漏。
4.缓存功用
开发人员喜爱缓存。 假如一个操作能只做一次而且将其结果保存,那么为什么还要做两次呢?
确实如此,可是假如无限期地缓存,终究将耗尽内存。 考虑以下示例:
public class ProfilePicExtractor { private Dictionary<int, byte[]> PictureCache { get; set; } = new Dictionary<int, byte[]>(); public byte[] GetProfilePicByID(int id) { // A lock mechanism should be added here, but let's stay on point if (!PictureCache.ContainsKey(id)) { var picture = GetPictureFromDatabase(id); PictureCache[id] = picture; } return PictureCache[id]; } private byte[] GetPictureFromDatabase(int id) { // ... } }
这段代码可能会节省一些贵重的数据库访问时刻,rar解压包破解可是价值却是使你的内存紊乱。
你能够做一些工作来处理这个问题:
- 删去一段时刻未运用的缓存。
- 约束缓存大小。
- 运用WeakReference来保存缓存的目标。 这依赖于废物搜集器来决议何时清除缓存,但这可能不是一个坏主意。 GC会将仍在运用的目标推行到更高的世代,以使它们的保存时刻更长。 这意味着经常运用的目标将在缓存中停留更长时刻。
5.错误的WPF绑定
WPF绑定实际上可能会导致内存走漏。 经历法则是一直绑定到DependencyObject或INotifyPropertyChanged目标。 假如你不这样做,WPF将创立从静态变量到绑定源(即ViewModel)的强引证,然后导致内存走漏。
这里是一个比如:
<UserControl x:Class="WpfApp.MyControl" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="https://blog.csdn.net/dafengit/article/details/106073709"> <TextBlock Text="{Binding SomeText}">TextBlock> UserControl>
这个View Model将永久留在内存中:
public class MyViewModel { public string _someText = "memory leak"; public string SomeText { get { return _someText; } set { _someText = value; } } }
而这个View Model不会导致内存走漏:
public class MyViewModel : INotifyPropertyChanged { public string _someText = "not a memory leak"; public string SomeText { get { return _someText; } set { _someText = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof (SomeText))); } }
是否调用PropertyChanged实际上并不重要,重要的是该类是从INotifyPropertyChanged派生的。 由于这会告知WPF不要创立强引证。
另一个和WPF有关的内存走漏问题会发作在绑定到调集时。 假如该调集未完结INotifyCollectionChanged接口,则会发作内存走漏。 你能够经过运用完结该接口的ObservableCollection来避免此问题。
6.永不终止的线程
咱们现已评论过了GC的工作方式以及GC root。 我提到过实时仓库会被视为GC root。 实时仓库包括正在运转的线程中的一切局部变量和调用仓库的成员。
假如出于某种原因,你要创立一个永久运转的不履行任何操作而且具有对目标引证的线程,那么这将会导致内存走漏。
这种状况很容易发作的一个比如是运用Timer。考虑以下代码:
public class MyClass { public MyClass() { Timer timer = new Timer(HandleTick); timer.Change(TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5)); } private void HandleTick(object state) { // do something }
假如你并没有真正的停止这个timer,那么它会在一个独自的线程中运转,而且由于引证了一个MyClass的实例,因而会阻挠该实例被搜集。
7.没有收回非保管内存
到目前为止,咱们仅仅议论了保管内存,也便是由废物搜集器管理的内存。 非保管内存是完全不同的问题,你将需求显式地收回内存,而不仅仅是避免不必要的引证。
这里有一个简单的比如。
public class SomeClass { private IntPtr _buffer; public SomeClass() { _buffer = Marshal.AllocHGlobal(1000); } // do stuff without freeing the buffer memory }
在上述办法中,咱们运用了Marshal.AllocHGlobal办法,它分配了非保管内存缓冲区。 在这背面,AllocHGlobal会调用Kernel32.dll中的LocalAlloc函数。 假如没有运用Marshal.FreeHGlobal显式地开释句柄,则该缓冲区内存将被视为占用了进程的内存堆,然后导致内存走漏。
要处理此类问题,你能够添加一个Dispose办法,以开释一切非保管资源,如下所示:
public class SomeClass : IDisposable { private IntPtr _buffer; public SomeClass() { _buffer = Marshal.AllocHGlobal(1000); // do stuff without freeing the buffer memory } public void Dispose() { Marshal.FreeHGlobal(_buffer); } }
由于内存碎片问题,非保管内存走漏比保管内存走漏更严重。 废物收回器能够移动保管内存,然后为其他目标腾出空间。 可是,非保管内存将永久卡在它的方位。
8.添加了Dispose办法却不调用它
在最终一个示例中,咱们添加了Dispose办法以开释一切非保管资源。 这很棒,可是当有人运用了该类却没有调用Dispose时会发作什么呢?
为了避免这种状况,你能够在C#中运用using语句:
using (var instance = new MyClass()) { // ... }
这适用于完结了IDisposable接口的类,而且编译器会将其转化为下面的方式:
MyClass instance = new MyClass();; try { // ... } finally { if (instance != null) ((IDisposable)instance).Dispose(); }
这非常有用,由于即使抛出反常,也会调用Dispose。
你能够做的另一件事是使用Dispose Pattern。 下面的示例演示了这种状况:
public class MyClass : IDisposable { private IntPtr _bufferPtr; public int BUFFER_SIZE = 1024 * 1024; // 1 MB private bool _disposed = false; public MyClass() { _bufferPtr = Marshal.AllocHGlobal(BUFFER_SIZE); } protected virtual void Dispose(bool disposing) { if (_disposed) return; if (disposing) { // Free any other managed objects here. } // Free any unmanaged objects here. Marshal.FreeHGlobal(_bufferPtr); _disposed = true; } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } ~MyClass() { Dispose(false); } }
这种形式可保证即使没有调用Dispose,Dispose也将在实例被废物收回时被调用。 另一方面,假如调用了Dispose,则finalizer将被抑制(SuppressFinalize)。 抑制finalizer很重要,由于finalizer开支很大而且会导致功能问题。
但是,dispose-pattern不是满有把握的。 假如从未调用Dispose而且由于保管内存走漏而导致你的类没有被废物收回,那么非保管资源也将不会被开释。