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