簡介
"終結(jié)"一般被分為確定性終結(jié)(顯示清除)與非確定性終結(jié)(隱式清除)
確定性終結(jié)主要
提供給開發(fā)人員一個(gè)顯式清理的方法,比如try-finally,using。
非確定性終結(jié)主要
提供一個(gè)注冊的入口,只知道會(huì)執(zhí)行,但不清楚什么時(shí)候執(zhí)行。比如IDisposable,析構(gòu)函數(shù)。
為什么需要終結(jié)機(jī)制?
首先糾正一個(gè)觀念,終結(jié)機(jī)制不等于垃圾回收。它只是代表當(dāng)某個(gè)對象不再需要時(shí),我們順帶要執(zhí)行一些操作。更加像是附加了一種event事件。
所以網(wǎng)絡(luò)上有一種說法,IDisposable是為了釋放內(nèi)存。這個(gè)觀念并不準(zhǔn)確。應(yīng)該形容為一種兜底更為貼切。
如果是一個(gè)完全使用托管代碼的場景,整個(gè)對象圖由GC管理,那確實(shí)不需要。在托管環(huán)境中,終結(jié)機(jī)制主要用于處理對象所持有的,不被GC和runtime管理的資源。
比如HttpClient,如果沒有終結(jié)機(jī)制,那么當(dāng)對象被釋放時(shí),GC并不知道該對象持有了非托管資源(句柄),導(dǎo)致底層了socket連接永遠(yuǎn)不會(huì)被釋放。
如前所述,終結(jié)器不一定非得跟非托管資源相關(guān)。它的本質(zhì)是”對象不可到達(dá)后的do something“.
比如你想收集對象的創(chuàng)建與刪除,可以將記錄代碼寫在構(gòu)造函數(shù)與終結(jié)器中
終結(jié)機(jī)制的源碼
源碼
namespace Example_12_1_3
{
internal class Program
{
static void Main(string[] args)
{
TestFinalize();
Console.WriteLine("GC is start. ");
GC.Collect();
Console.WriteLine("GC is end. ");
Debugger.Break();
Console.ReadLine();
Console.WriteLine("GC2 is start. ");
GC.Collect();
Console.WriteLine("GC2 is end. ");
Debugger.Break();
Console.ReadLine();
}
static void TestFinalize()
{
var list = new List<Person>(1000);
for (int i = 0; i < 1000; i++)
{
list.Add(new Person());
}
var personNoFinalize = new Person2();
Console.WriteLine("person/personNoFinalize分配完成");
Debugger.Break();
}
}
public class Person
{
~Person()
{
Console.WriteLine("this is finalize");
Thread.Sleep(1000);
}
}
public class Person2
{
}
}
IL
.method family hidebysig virtual
instance void Finalize () cil managed
{
.override method instance void [mscorlib]System.Object::Finalize()
.maxstack 1
IL_0000: nop
.try
{
IL_0001: nop
IL_0002: ldstr "this is finalize"
IL_0007: call void [mscorlib]System.Console::WriteLine(string)
IL_000c: nop
IL_000d: call string [mscorlib]System.Console::ReadLine()
IL_0012: pop
IL_0013: leave.s IL_001d
}
finally
{
IL_0015: ldarg.0
IL_0016: call instance void [mscorlib]System.Object::Finalize()
IL_001b: nop
IL_001c: endfinally
}
IL_001d: ret
}
匯編
0199097B nop
0199097C mov ecx,dword ptr ds:[4402430h]
01990982 call System.Console.WriteLine(System.String) (72CB2FA8h)
01990987 nop
01990988 call System.Console.ReadLine() (733BD9C0h)
0199098D mov dword ptr [ebp-40h],eax
01990990 nop
01990991 nop
01990992 mov dword ptr [ebp-20h],offset Example_12_1_3.Person.Finalize()+045h (00h)
01990999 mov dword ptr [ebp-1Ch],0FCh
019909A0 push offset Example_12_1_3.Person.Finalize()+06Ch (019909BCh)
019909A5 jmp Example_12_1_3.Person.Finalize()+057h (019909A7h)
可以看到,C#的析構(gòu)函數(shù)只是一種語法糖。IL重寫了System.Object.Finalize方法。在底層的匯編中,直接調(diào)用的就是Finalize()
終結(jié)的流程
補(bǔ)充一個(gè)細(xì)節(jié),實(shí)際上f-reachable queue 內(nèi)部還分為Critical/Normal兩個(gè)區(qū)間,其區(qū)別在于是否繼承自CriticalFinalizerObject。
目的是為了保證,即使在AppDomain或線程被強(qiáng)行中斷的情況下,也一定會(huì)執(zhí)行。
一般也很少直接繼承CriticalFinalizerObject,更常見是選擇繼承SafeHandle.
不過在.net core中區(qū)別不大,因?yàn)?net core不支持終止線程,也不支持卸載AppDomain。
眼見為實(shí)
使用windbg看一下底層。
1. 創(chuàng)建Person對象,是否自動(dòng)進(jìn)入finalize queue?
可以看到,當(dāng)new obj 時(shí),finalize queue中已經(jīng)有了Person對象的析構(gòu)函數(shù)
2. GC開始后,是否移動(dòng)到F-Reachable queue?
可以看到代碼中創(chuàng)建的1000個(gè)Person的析構(gòu)函數(shù)已經(jīng)進(jìn)入了F-Reachable queue
sosex !finq/!frq 指令同樣可以輸出
3. 析構(gòu)對象是否被"復(fù)活"?
GC發(fā)生前,在TestFinalize方法中創(chuàng)建了兩個(gè)變量,person=0x02a724c0,personNoFinalize=0x02a724cc。
可以看到所屬代都為0,且托管堆中都能找到它們。
GC發(fā)生后
可以看到,Person2對象因?yàn)楸换厥斩谕泄芏阎姓也坏搅?,Person對象因?yàn)檫€未執(zhí)行析構(gòu)函數(shù),所以還存在gcroot 。因此并未被回收,且內(nèi)存代從0代提升到1代
4. 終結(jié)線程是否執(zhí)行,是否被移出F-Reachable queue
在GC將托管線程從掛起到恢復(fù)正常后,且F-Reachable queue 有值時(shí),終結(jié)線程將亂序執(zhí)行。
并將它們移出隊(duì)列
5. 析構(gòu)函數(shù)的對象是否在第二次GC中釋放?
等到第二次GC發(fā)生后,由于對象析構(gòu)函數(shù)已經(jīng)被執(zhí)行,不再擁有g(shù)croot,所以托管堆最終釋放了該對象,
6. 析構(gòu)函數(shù)如果沒有及時(shí)執(zhí)行完成,又觸發(fā)了一次GC。會(huì)不會(huì)再次升代?
答案是肯定的
Finaze Queue/F-Reachable Queue 底層結(jié)構(gòu)
眼見為實(shí)
每個(gè)不同的代,維護(hù)在不同的內(nèi)存地址中,但彼此之間的內(nèi)存地址又緊密聯(lián)系在一起。
與GC代優(yōu)點(diǎn)細(xì)微區(qū)別的是,沒有LOH概念,大對象分配在0代中。Person3對象是一個(gè) new byte[8500000]。 其他行為與GC代保持一致
終結(jié)的開銷
如果一個(gè)類型具有終結(jié)器,將使用慢速分支執(zhí)行分配操作
且在分配時(shí)還需要額外進(jìn)入finalize queue而引入的額外開銷
終結(jié)器對象至少要經(jīng)歷2次GC才能夠被真正釋放
至少兩次,可能更多。終結(jié)線程不一定能在兩次GC之間處理完所有析構(gòu)函數(shù)。此時(shí)對象從1代升級(jí)到2代,2代對象觸發(fā)GC的頻率更低。導(dǎo)致對象不能及時(shí)被釋放(析構(gòu)函數(shù)已經(jīng)執(zhí)行完畢,但是對象本身等了很久才被釋放)。
對象升代/降代時(shí),finalize queue也要重復(fù)調(diào)整
與GC分代一樣,也分為3個(gè)代和LOH。當(dāng)一個(gè)對象在GC代中移動(dòng)時(shí),對象地址也需要也需要在finalization queue移動(dòng)到對應(yīng)的代中.
由于finalize queue與f-reachable queue 底層由同一個(gè)數(shù)組管理,且元素之間并沒有留空。所以升代/降代時(shí),與GC代不同,GC代可以見縫插針的安置對象,而finalize則是在對應(yīng)的代末尾插入,并將后面所有對象右移一個(gè)位置
眼見為實(shí)
點(diǎn)擊查看代碼
public class BenchmarkTester
{
[Benchmark]
public void ConsumeNonFinalizeClass()
{
for (int i = 0; i < 1000; i++)
{
var obj = new NonFinalizeClass();
obj.Age = i;
}
}
[Benchmark]
public void ConsumeFinalizeClass()
{
for (int i = 0; i < 1000; i++)
{
var obj = new FinalizeClass();
obj.Age = i;
}
}
}
非常明顯的差距,無需解釋。
總結(jié)
使用終結(jié)器是比較棘手且不完全可靠。因此最好避免使用它。只有當(dāng)開發(fā)人員沒有其他辦法(IDisposable)來釋放資源時(shí),才應(yīng)該把終結(jié)器作為最后的兜底。
轉(zhuǎn)自https://www.cnblogs.com/lmy5215006/p/18456380
該文章在 2024/10/12 9:43:19 編輯過