我花了2天时间,研究了.Net的垃圾处理机制,最终理解了Dispose Patter。好吧,先从一些概念开始。
memory management
- Processes don’t access physical memory directly, they use virtual space address.
- Every process has its own, separate virtual address space.
- By default, on 32-bit computers, each process has a 2-GB user-mode virtual address space.
- There are two heaps — managed heap and native heap using the virtual address space.
- Garbage Collector works only on managed heap — it can only manage the objects created in the .Net scope, and that’s why it’s called MANAGED heap. GC can always do the cleaning job for you.
- The other heap is called native heap which GC can do nothing with. It can only be accessed by the OS, so you have to use/release the native resource explicitly by calling system functions.
GC
One more time, GC only works on managed heap. It checks the objects on the heap, removed the dead ones and move live ones to compact the space.
Generations
Objects on the heap are organized into three generations based on the lifetime of them.
Generation 0 contains short-lived objects. Garbage collection occurs most frequently in this generation.
Generation 2 contains long-lived objects.
Generation 1 contains short-lived objects and acts like a buffer between G0 and G2.
Objects that are not reclaimed in a garbage collection are known as survivors and are promoted to the next generation.
Trigger
GC will be triggered if any one of the three conditions is met:
- The system has low physical memory.
- The memory that is used by allocated objects on the managed heap surpasses an acceptable threshold.
- The GC.Collect() method is called.
Finalizer
“Finalizers (which are also called destructors) are used to perform any necessary final clean-up when a class instance is being collected by the garbage collector.”
Marks
- Finalizers cannot be defined in structs. They are only used with classes.
- A class can only have one finalizer.
- Finalizers cannot be inherited or overloaded.
- Finalizers cannot be called. They are invoked automatically.
- A finalizer does not take modifiers or have parameters.
Two Queues
There are two queues for the objects which have finalizers. One is Finalization Queue — all the reference to the finalizable objects are added to this queue. The other one is ReadyToFinalize Queue — it only contains the reference of the objects which are about to be finalized.
How it works
When a object with a finalizer is created on the managed heap, a reference to the object is added to the Finalization Queue.
When GC begins to work and detects dead objects, it will check if there’s the reference for the detected object in the Finalization Queue. If it matches, then GC will remove the reference from the Finalization Queue and add it to the ReadyToFinalize Queue. At this moment, the object is not considered garbage and its memory is not reclaimed.
There’s a special thread monitoring the ReadyToFinalize Queue. When the ReadyToFinalize Queue is not empty, it will call each object’s finalizer and remove it from the Queue after the finalization is done.
The next time the GC is invoked, these objects no longer have references in the Finalization Queue. They are considered to be real garbage now, and the memory for them is reclaimed.
Disposal
“Although the garbage collector is able to track the lifetime of an object that encapsulates an unmanaged resource, it doesn’t know how to release and clean up the unmanaged resource.”
To dispose the objects using native resources, dispose pattern should be implemented.\
using finalizer
public class DisposableBase : IDisposable
{
private bool _disposed;
// Create a 500-byte native resource
private IntPtr _nativeResourceHandle = CreateNativeResource(500);
private FakeManagedResource _managedResource;
// This is a fake windows api
[DllImport("Kernel32.dll", EntryPoint = "CreateNativeResource")]
internal extern static IntPtr CreateNativeResource(uint size);
[DllImport("kernel32.dll", SetLastError = true)]
internal static extern bool CloseHandle(IntPtr hObject);
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (this._disposed)
{
return;
}
if (disposing)
{
_managedResource.Dispose();
}
CloseHandle(_nativeResourceHandle);
_disposed = true;
}
~DisposableBase()
{
Dispose(false);
}
}
- Why to have two overload Dispose method?
Although we could put all clean job in finalizer, it’s not good. After the object is dead, it may still occupy the memory until GC is invoked twice. We should always actively dispose it as soon as it is no longer needed. And also, we have to clean native resource in finalizer to make sure native resource will be released before the object is collected or there will be memory leak.
Therefore, we have two Dispose overloads.
The one with bool parameter is the one which actually does the cleaning job. If the parameter is true, then it’s called from the other Dispose(); if the parameter is false, it’s called from finalizer.
The one without parameter is defined in IDisposable, which should be always called explicitly and immediately after use. - Why to call
GC.SuppressFinalize(this)?
To prevent finalizer dispose the object again. - Why to call
_managedResource.Dispose()only whenif (disposing)?
If it’s called from finalizer, the _managedResource could have already been reclaimed by GC.
using safehandle
SafeHandle is actually a wrapper. It takes the responsibility of releasing the native resource, and you can treat it as a managed object with IDisposable interface. The only thing you need to do is to inherit SafeHandle class or its subclass, and implement the ReleaseHandle() method.
public class NativeSafeHandle : SafeHandleZeroOrMinusOneIsInvalid
{
[DllImport("kernel32.dll", SetLastError = true)]
internal static extern bool CloseHandle(IntPtr hObject);
public NativeSafeHandle(IntPtr handle) : base(true)
{
SetHandle(handle);
}
protected override bool ReleaseHandle()
{
CloseHandle(this.handle);
return true;
}
}
- SafeHandleZeroOrMinusOneIsInvalid is a subclass of SafeHandle. The value of either 0 or -1 indicates an invalid handle.
base(true)indicates that the SafeHandle does own the the handle.return true;indicates the resource is released successfully.
public class DisposableWithSafeHandle : IDisposable
{
private bool _disposed;
// Initialize the safe handle
private NativeSafeHandle _safeHandle = new NativeSafeHandle(IntPtr.SomeHandle);
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (this._disposed)
{
return;
}
if (disposing)
{
_safeHandle.Dispose();
// Dispose any other managed objects here.
}
_disposed = true;
}
}