聊一聊 C# 弱引用底層是怎么玩的
一、背景
1. 講故事
最近在分析dump時,發(fā)現(xiàn)有程序的卡死和WeakReference有關(guān),在以前只知道怎么用,但不清楚底層邏輯走向是什么樣的,借著這個dump的契機(jī)來簡單研究下。
二、弱引用的玩法
1. 一些基礎(chǔ)概念
用過WeakReference的朋友都知道這里面又可以分為弱短和弱長兩個概念,對應(yīng)著構(gòu)造函數(shù)中的trackResurrection參數(shù),同時它也是對底層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 的注釋來看。
- Weak 會在終結(jié)器執(zhí)行之前判斷持有的對象是否為垃圾對象,如果是的話直接切斷引用。
- WeakTrackResurrection 會在終結(jié)器執(zhí)行之后判斷對象是否為垃圾對象,如果是的話直接切斷引用。
可能這么說有點(diǎn)抽象,畫張圖如下:
圖片
2. 一個簡單的測試?yán)?/h3>
為了方便講述兩者的區(qū)別,使用 對象復(fù)活 來做測試。
- Weak 的情況
因為在 ScanForFinalization 方法之前做的判斷,所以與垃圾對象的聯(lián)系會被馬上切斷,參考代碼如下:
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 析構(gòu)已執(zhí)行...");
}
}
}
圖片
- WeakTrackResurrection 的情況
因為是在 ScanForFinalization 之后做的判斷,這時候可能會存在 對象復(fù)活 的情況,所以垃圾又變成不垃圾了,如果是這種情況就不能切斷,參考代碼如下:
static void WeakReferenceCase()
{
var person = new Person() { ressurect = true };
weakHandle = GCHandle.Alloc(person, GCHandleType.WeakTrackResurrection);
}
圖片
3. coreclr源碼分析
在 coreclr 里有一個 struct 枚舉強(qiáng)對應(yīng) GCHandleType 結(jié)構(gòu)體,而且名字看的更加清楚,代碼如下:
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;
}
源碼中有幾個注意點(diǎn):
- 如何判斷一個對象為垃圾
gc 在標(biāo)記時,將有根的對象mt的第一位設(shè)為 1 來表示當(dāng)前已經(jīng)標(biāo)記過,即有用對象,未被標(biāo)記的即為垃圾對象。
- 終結(jié)器線程真的被啟動了嗎
從簡化的源碼看,一旦有垃圾對象被送入到 終結(jié)器隊列的 預(yù)備區(qū) 時,就會通過 GCToEEInterface::EnableFinalization(true) 啟動終結(jié)器線程,所以在測試代碼中加了 GC.WaitForPendingFinalizers(); 就是為了等待終結(jié)器線程執(zhí)行完畢然后才判斷 Target,這樣結(jié)果就會更加準(zhǔn)確。
4. 切斷邏輯在哪里
有些朋友會好奇那個 weakHandle.Target=null 的邏輯到底在 coreclr 的何處,這個比較簡單,可以用 windbg 下 ba 斷點(diǎn)即可,我們還是拿弱引用來舉例,截圖如下:
圖片
三、總結(jié)
WeakReference 的內(nèi)部玩法有很多,更深入的理解還需要對 g_HandleTableMap 進(jìn)行深度挖掘,后面有機(jī)會再聊吧,有時候dump分析還是挺苦逼的,需要對相關(guān)領(lǐng)域底層知識有一個足夠了解,否則談何修復(fù)呢?