最近在分析dump時,發現有程序的卡死和WeakReference有關,在以前只知道怎么用,但不清楚底層邏輯走向是什么樣的,借著這個dump的契機來簡單研究下。
用過WeakReference的朋友都知道這里面又可以分為弱短和弱長兩個概念,對應著構造函數中的trackResurrection參數,同時它也是對底層GCHandle.Alloc 方法的封裝,參考源碼如下:
public WeakReference(object? target, bool trackResurrection){ Create(target, trackResurrection);}private void Create(object target, bool trackResurrection){ nint num = GCHandle.InternalAlloc(target, trackResurrection ? GCHandleType.WeakTrackResurrection : GCHandleType.Weak); _taggedHandle = (trackResurrection ? (num | 1) : num); ComAwareWeakReference.ComInfo comInfo = ComAwareWeakReference.ComInfo.FromObject(target); if (comInfo != null) { ComAwareWeakReference.SetComInfoInConstructor(ref _taggedHandle, comInfo); }}public enum GCHandleType{ // // Summary: // This handle type is used to track an object, but allow it to be collected. When // an object is collected, the contents of the System.Runtime.InteropServices.GCHandle // are zeroed. Weak references are zeroed before the finalizer runs, so even if // the finalizer resurrects the object, the Weak reference is still zeroed. Weak = 0, // // Summary: // This handle type is similar to System.Runtime.InteropServices.GCHandleType.Weak, // but the handle is not zeroed if the object is resurrected during finalization. WeakTrackResurrection = 1}
從上面的 GCHandleType 的注釋來看。
可能這么說有點抽象,畫張圖如下:
圖片
為了方便講述兩者的區別,使用 對象復活 來做測試。
因為在 ScanForFinalization 方法之前做的判斷,所以與垃圾對象的聯系會被馬上切斷,參考代碼如下:
class Program { static void Main() { WeakReferenceCase(); GC.Collect(); GC.WaitForPendingFinalizers(); Console.WriteLine(weakHandle.Target ?? "Person 引用被切斷"); Console.ReadLine(); } public static GCHandle weakHandle; static void WeakReferenceCase() { var person = new Person() { ressurect = false }; weakHandle = GCHandle.Alloc(person, GCHandleType.Weak); } } public class Person { public bool ressurect = false; ~Person() { if (ressurect) { Console.WriteLine("Person 被永生了,不可能被消滅的。。。"); GC.ReRegisterForFinalize(this); } else { Console.WriteLine("Person 析構已執行..."); } } }
圖片
因為是在 ScanForFinalization 之后做的判斷,這時候可能會存在 對象復活 的情況,所以垃圾又變成不垃圾了,如果是這種情況就不能切斷,參考代碼如下:
static void WeakReferenceCase(){ var person = new Person() { ressurect = true }; weakHandle = GCHandle.Alloc(person, GCHandleType.WeakTrackResurrection);}
圖片
在 coreclr 里有一個 struct 枚舉強對應 GCHandleType 結構體,而且名字看的更加清楚,代碼如下:
typedef enum{ HNDTYPE_WEAK_SHORT = 0, HNDTYPE_WEAK_LONG = 1,}HandleType;
接下來看下剛才截圖源碼上的驗證。
void gc_heap::mark_phase(int condemned_gen_number, BOOL mark_only_p){ // null out the target of short weakref that were not promoted. GCScan::GcShortWeakPtrScan(condemned_gen_number, max_generation, &sc); dprintf(3, ("Finalize marking")); finalize_queue->ScanForFinalization(GCHeap::Promote, condemned_gen_number, mark_only_p, __this); // null out the target of long weakref that were not promoted. GCScan::GcWeakPtrScan(condemned_gen_number, max_generation, &sc);}BOOL CFinalize::ScanForFinalization(promote_func* pfn, int gen, BOOL mark_only_p, gc_heap* hp){ for (unsigned int Seg = startSeg; Seg <= gen_segment(0); Seg++) { Object** endIndex = SegQueue(Seg); for (Object** i = SegQueueLimit(Seg) - 1; i >= endIndex; i--) { CObjectHeader* obj = (CObjectHeader*)*i; if (!g_theGCHeap->IsPromoted(obj)) { if (method_table(obj)->HasCriticalFinalizer()) { MoveItem(i, Seg, CriticalFinalizerListSeg); } else { MoveItem(i, Seg, FinalizerListSeg); } } } } if(finalizedFound) GCToEEInterface::EnableFinalization(true); return finalizedFound;}
源碼中有幾個注意點:
gc 在標記時,將有根的對象mt的第一位設為 1 來表示當前已經標記過,即有用對象,未被標記的即為垃圾對象。
從簡化的源碼看,一旦有垃圾對象被送入到 終結器隊列的 預備區 時,就會通過 GCToEEInterface::EnableFinalization(true) 啟動終結器線程,所以在測試代碼中加了 GC.WaitForPendingFinalizers(); 就是為了等待終結器線程執行完畢然后才判斷 Target,這樣結果就會更加準確。
有些朋友會好奇那個 weakHandle.Target=null 的邏輯到底在 coreclr 的何處,這個比較簡單,可以用 windbg 下 ba 斷點即可,我們還是拿弱引用來舉例,截圖如下:
圖片
WeakReference 的內部玩法有很多,更深入的理解還需要對 g_HandleTableMap 進行深度挖掘,后面有機會再聊吧,有時候dump分析還是挺苦逼的,需要對相關領域底層知識有一個足夠了解,否則談何修復呢?
本文鏈接:http://www.tebozhan.com/showinfo-26-97278-0.html聊一聊 C# 弱引用底層是怎么玩的
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com