面试官:工作3年,你连 .NET 内存泄露都不知道?
原文连接:
https://michaelscodingspot.com/ways-to-cause-memory-leaks-in-dotnet/
任何有经验的.NET开发人员都知道,即使.NET应用程序具有垃圾回收器,内存泄漏始终会发生。并不是说垃圾回收器有bug,而是我们有多种方法可以(轻松地)导致托管语言的内存泄漏。
内存泄漏是一个偷偷摸摸的坏家伙。很长时间以来,它们很容易被忽视,而它们也会慢慢破坏应用程序。随着内存泄漏,你的内存消耗会增加,从而导致GC压力和性能问题。最终,程序将在发生内存不足异常时崩溃。
在本文中,我们将介绍.NET程序中内存泄漏的最常见原因。所有示例均使用C#,但它们与其他语言也相关。
定义.NET中的内存泄漏
在垃圾回收的环境中,“内存泄漏”这个术语有点违反直觉。当有一个垃圾回收器(GC)负责收集所有东西时,我的内存怎么会泄漏呢?
这里有两个核心原因。第一个核心原因是你的对象仍被引用但实际上却未被使用。由于它们被引用,因此GC将不会收集它们,这样它们将永久保存并占用内存。例如,当你注册了事件但从不注销时,就有可能会发生这种情况。我们称其为托管内存泄漏。
第二个原因是当你以某种方式分配非托管内存(没有垃圾回收)并且不释放它们。这并不难做到。.NET本身有很多会分配非托管内存的类。几乎所有涉及流、图形、文件系统或网络调用的操作都会在背后分配这些非托管内存。
通常这些类会实现 Dispose 方法,以释放内存。你自己也可以使用特殊的.NET类(如Marshal)或PInvoke轻松地分配非托管内存。
许多人都认为托管内存泄漏根本不是内存泄漏,因为它们仍然被引用,并且理论上可以被回收。这是一个定义问题,我的观点是它们确实是内存泄漏。它们拥有无法分配给另一个实例的内存,最终将导致内存不足的异常。
对于本文,我会将托管内存泄漏和非托管内存泄漏都归为内存泄漏。
以下是最常见的8种内存泄露的情况。前6个是托管内存泄漏,后2个是非托管内存泄漏:
1. 订阅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.》
所以,你可以做什么呢?在提到的这篇文章中,有几种很好的模式可以防止和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
});
}
}
解决方案可能非常简单——分配局部变量:
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 Root?
正在运行的线程的实时堆栈。 静态变量。 通过interop传递到COM对象的托管对象(内存回收将通过引用计数来完成)。
这意味着静态变量及其引用的所有内容都不会被垃圾回收。这里是一个例子:
public class MyClass
{
static List_instances = new List ();
public MyClass()
{
_instances.Add(this);
}
}
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)
{
// ...
}
}
你可以做一些事情来解决这个问题:
删除一段时间未使用的缓存。 限制缓存大小。 使用WeakReference来保存缓存的对象。这依赖于垃圾收集器来决定何时清除缓存,但这可能不是一个坏主意。GC会将仍在使用的对象推广到更高的世代,以使它们的保存时间更长。这意味着经常使用的对象将在缓存中停留更长时间。
5. 错误的WPF绑定
<UserControl x:Class="WpfApp.MyControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<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)));
}
}
}
6. 永不终止的线程
这种情况很容易发生的一个例子是使用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
}
}
7. 没有回收非托管内存
这里有一个简单的例子。
public class SomeClass
{
private IntPtr _buffer;
public SomeClass()
{
_buffer = Marshal.AllocHGlobal(1000);
}
// do stuff without freeing the buffer memory
}
要解决此类问题,你可以添加一个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方法却不调用它
为了避免这种情况,你可以在C#中使用using语句:
using (var instance = new MyClass())
{
// ...
}
这适用于实现了IDisposable接口的类,并且编译器会将其转化为下面的形式:
MyClass instance = new MyClass();;
try
{
// ...
}
finally
{
if (instance != null)
((IDisposable)instance).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);
}
}
总结
一招搞定github下载速度到2MB/s
微信突然更新,这些功能被禁用了