永不终止的线程
我们已经讨论过了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的实例,因此会阻止该实例被收集。
没有回收非托管内存
到目前为止,我们仅仅谈论了托管内存,也就是由垃圾收集器管理的内存。非托管内存是完全不同的问题,你将需要显式地回收内存,而不仅仅是避免不必要的引用。
这里有一个简单的例子。
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);
}
}
由于内存碎片问题,非托管内存泄漏比托管内存泄漏更严重。垃圾回收器可以移动托管内存,从而为其他对象腾出空间。但是,非托管内存将永远卡在它的位置。
添加了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并且由于托管内存泄漏而导致你的类没有被垃圾回收,那么非托管资源也将不会被释放。