.NET GC 精要(七)

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

系列文章完

發佈了149 篇原創文章 · 獲贊 146 · 訪問量 25萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章