本文講述了 .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 的內存壓縮流程造成影響,使用時應儘量縮短對象的固定時間.
系列文章完