簡介
"終結"一般被分為確定性終結(顯示清除)與非確定性終結(隱式清除)
確定性終結主要
提供給開發人員一個顯式清理的方法,比如try-finally,using。
非確定性終結主要
提供一個注冊的入口,只知道會執行,但不清楚什么時候執行。比如IDisposable,析構函數。
為什么需要終結機制?
首先糾正一個觀念,終結機制不等于垃圾回收。它只是代表當某個對象不再需要時,我們順帶要執行一些操作。更加像是附加了一種event事件。
所以網絡上有一種說法,IDisposable是為了釋放內存。這個觀念并不準確。應該形容為一種兜底更為貼切。
如果是一個完全使用托管代碼的場景,整個對象圖由GC管理,那確實不需要。在托管環境中,終結機制主要用于處理對象所持有的,不被GC和runtime管理的資源。
比如HttpClient,如果沒有終結機制,那么當對象被釋放時,GC并不知道該對象持有了非托管資源(句柄),導致底層了socket連接永遠不會被釋放。
如前所述,終結器不一定非得跟非托管資源相關。它的本質是”對象不可到達后的do something“.
比如你想收集對象的創建與刪除,可以將記錄代碼寫在構造函數與終結器中
終結機制的源碼
源碼
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#的析構函數只是一種語法糖。IL重寫了System.Object.Finalize方法。在底層的匯編中,直接調用的就是Finalize()
終結的流程
補充一個細節,實際上f-reachable queue 內部還分為Critical/Normal兩個區間,其區別在于是否繼承自CriticalFinalizerObject。
目的是為了保證,即使在AppDomain或線程被強行中斷的情況下,也一定會執行。
一般也很少直接繼承CriticalFinalizerObject,更常見是選擇繼承SafeHandle.
不過在.net core中區別不大,因為.net core不支持終止線程,也不支持卸載AppDomain。
眼見為實
使用windbg看一下底層。
1. 創建Person對象,是否自動進入finalize queue?
可以看到,當new obj 時,finalize queue中已經有了Person對象的析構函數
2. GC開始后,是否移動到F-Reachable queue?
可以看到代碼中創建的1000個Person的析構函數已經進入了F-Reachable queue
sosex !finq/!frq 指令同樣可以輸出
3. 析構對象是否被"復活"?
GC發生前,在TestFinalize方法中創建了兩個變量,person=0x02a724c0,personNoFinalize=0x02a724cc。
可以看到所屬代都為0,且托管堆中都能找到它們。
GC發生后
可以看到,Person2對象因為被回收而在托管堆中找不到了,Person對象因為還未執行析構函數,所以還存在gcroot 。因此并未被回收,且內存代從0代提升到1代
4. 終結線程是否執行,是否被移出F-Reachable queue
在GC將托管線程從掛起到恢復正常后,且F-Reachable queue 有值時,終結線程將亂序執行。
并將它們移出隊列
5. 析構函數的對象是否在第二次GC中釋放?
等到第二次GC發生后,由于對象析構函數已經被執行,不再擁有gcroot,所以托管堆最終釋放了該對象,
6. 析構函數如果沒有及時執行完成,又觸發了一次GC。會不會再次升代?
答案是肯定的
Finaze Queue/F-Reachable Queue 底層結構
眼見為實
每個不同的代,維護在不同的內存地址中,但彼此之間的內存地址又緊密聯系在一起。
與GC代優點細微區別的是,沒有LOH概念,大對象分配在0代中。Person3對象是一個 new byte[8500000]。 其他行為與GC代保持一致
終結的開銷
如果一個類型具有終結器,將使用慢速分支執行分配操作
且在分配時還需要額外進入finalize queue而引入的額外開銷
終結器對象至少要經歷2次GC才能夠被真正釋放
至少兩次,可能更多。終結線程不一定能在兩次GC之間處理完所有析構函數。此時對象從1代升級到2代,2代對象觸發GC的頻率更低。導致對象不能及時被釋放(析構函數已經執行完畢,但是對象本身等了很久才被釋放)。
對象升代/降代時,finalize queue也要重復調整
與GC分代一樣,也分為3個代和LOH。當一個對象在GC代中移動時,對象地址也需要也需要在finalization queue移動到對應的代中.
由于finalize queue與f-reachable queue 底層由同一個數組管理,且元素之間并沒有留空。所以升代/降代時,與GC代不同,GC代可以見縫插針的安置對象,而finalize則是在對應的代末尾插入,并將后面所有對象右移一個位置
眼見為實
點擊查看代碼
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;
}
}
}
非常明顯的差距,無需解釋。
總結
使用終結器是比較棘手且不完全可靠。因此最好避免使用它。只有當開發人員沒有其他辦法(IDisposable)來釋放資源時,才應該把終結器作為最后的兜底。
轉自https://www.cnblogs.com/lmy5215006/p/18456380
該文章在 2024/10/12 9:43:19 編輯過