本文讲述了 .NET GC 的一些细节知识,内容大部分来自于书籍 Under the Hood of .NET Memory Management
(注:本文假设你了解 .NET 的基础知识,譬如值类型,引用类型等)
深入
并发执行模式(工作站模式下)的一点细节
之前讲到工作站模式分为 并发 和 非并发 两种执行模式,其中非并发 执行模式比较容易理解,即在整个 GC 流程中应用线程(application thread)是暂停的(非并发执行模式一般适用于单核运行环境).
而对于并发执行模式,细节上则会复杂一些:
并发执行模式下, Gen 0 回收 和 Gen 1 回收 仍然会暂停应用线程,只有在 Full GC(即 Gen 2 回收)时才会有并发行为,并且在整个 GC 流程中一般只会造成应用线程 2 次(短期)暂停.
相关实现上,由于 Full GC 发生时,需要检查回收的内存范围(称为 GC domain)是确定的,所以应用线程可以在 Full GC 的同时于当前 GC domain 以外的内存范围中 申请对象,当然由于内存段的大小限制, 并发执行 GC 时,内存段上还会被设置特殊的内存区域(称为 No Go Zone),如果应用线程的对象申请达到了这个区域,则应用线程仍然会被暂停.
示意图如下:
(可以看到,Full GC 过程中,应用线程仍然可以申请对象(Object L, M, N 和 O))
并发执行模式虽然允许应用线程在 Full GC 过程中继续申请对象,但仍然有不少限制(申请对象不能触及 No Go Zone 区域;申请的对象即使不被引用也不能(被本次 GC )回收(譬如上面示意图中的 Object M)),为了解决这个问题, .NET 4.0 引入了 Background Workstation GC, .NET 4.5 甚至引入了 Background Server GC,有兴趣的朋友可以继续了解.
弱引用(Weak References)
对于一些大内存对象,如果每次使用时都进行创建和释放,则程序效率不高,但如果(创建之后)一直保留引用的话,内存消耗又比较大,使用弱引用可以缓解这个问题:
// load a big data structure
var bigDataObject = new BigDataStructure();
// get a weak reference to it
var weakRef = new WeakReference(bigDataObject, false);
// destroy the strong reference, keeping the weak reference
bigDataObject = null;
// ...
// some time later try and get a strong reference back
bigDataObject = (BigDataStructure)weakRef.Target;
// recreate if weak ref was reclaimed
if (bigDataObject == null)
{
bigDataObject = new BigDataStructure();
}
.NET 中, 弱引用被分为两类:
- short weak references
对于 short weak references, GC 如果发现其引用对象没有被遍历流程标记,即会清理其引用对象.
创建方式:
// pass false to WeakReference's constructor
var shortWeakRef = new WeakReference(object, false);
- long weak references
对于 long weak references, GC 如果发现其引用对象没有被遍历流程标记并且不在 Finalization Queue 中,即会清理其引用对象.
创建方式:
// pass true to WeakReference's constructor
var longWeakRef = new WeakReference(object, true);
GCHandle
GCHandle 可以用于追踪对象堆上的 Object ,一大用处就是支持托管程序和非托管程序之间的互操作.
GCHandle 的类型分为 4 种:
- Normal 用于追踪一般对象
- Weak 用于追踪 short weak references
- Weak 用于追踪 long weak references
- Pinned 用于固定对象的内存地址
以下是互操作的一段示例代码:
var buffer = new byte[512];
var h = GCHandle.Alloc(buffer, GCHandleType.Pinned);
var ptr = h.AddrOfPinnedObject();
// Call native API and pass buffer
// ...
if (h.IsAllocated)
{
h.Free();
}
由于非托管程序一般需要保证对象的内存地址不变,所以我们使用 GCHandleType.Pinned 来固定对象的内存地址,值得一提的是,使用 fixed 语句块也会固定对应的对象内存地址:
unsafe static void Main()
{
Person p = new Person();
p.age = 25;
// Pin p
fixed (int* a = &p.age)
{
// Do something
}
// p unpinned
}
之前提到 SOH 为了解决内存碎片问题会进行内存压缩,但是由于其不能调整固定内存地址的对象,所以使用 GCHandleType.Pinned 会对 SOH 的内存压缩流程造成影响,使用时应尽量缩短对象的固定时间.
系列文章完