經常在代碼中看到有人將 null
賦值給引用類型,來達到讓 GC 提前回收的目的,這樣做真的有用嗎?今天我們就來研究一下。
為了方便講解,來一段測試代碼,提前將 test1=null
,然后調用 GC.Collect()
看看是否能提前回收。
平臺采用: .net5
public?class?Program{static?void?Main(string[]?args){ProcessRequest();}static?void?ProcessRequest(){var?test1?=?new?Test()?{?a?=?10?};Console.WriteLine($"query.a={test1.a}");var?test2?=?new?Test()?{?a?=?11?};Console.WriteLine($"query.a={test2.a}");//提前釋放test1?=?null;var?test3?=?new?Test()?{?a?=?12?};Console.WriteLine($"query.a={test3.a}");GC.Collect();Console.WriteLine("垃圾回收啦!");Console.ReadLine();}}public?class?Test{public?int?a;}
接下來我們從 Debug
和 Release
兩種模式下觀察。
一:Debug 模式
要找到這個答案,我們用 windbg 附加一下,找到 test1
然后用 !gcroot
查看下引用即可。
0:000>?!clrstack?-a
OS?Thread?Id:?0x4dd0?(0)
Child?SP???????IP?Call?Site
0057F2A4?79863539?System.Console.ReadLine()?[/_/src/System.Console/src/System/Console.cs?@?463]
0057F2AC?04c405d1?ConsoleApp1.Program.ProcessRequest()?[D:\net5\ConsoleApp2\ConsoleApp1\Program.cs?@?37]LOCALS:0x0057F2D4?=?0x000000000x0057F2D0?=?0x0283cd540x0057F2CC?=?0x0283cd900:000>?!dumpheap?-type?TestAddress???????MT?????Size
0283a7c0?04c39008???????12?????
0283cd54?04c39008???????12?????
0283cd90?04c39008???????12?????0:000>?!gcroot?0283a7c0
Thread?4dd0:0057F2AC?04C405D1?ConsoleApp1.Program.ProcessRequest()?[D:\net5\ConsoleApp2\ConsoleApp1\Program.cs?@?37]ebp+14:?0057f2c8->??0283A7C0?ConsoleApp1.TestFound?1?unique?roots?(run?'!gcroot?-all'?to?see?all?roots).
是不是很驚訝,test1 雖被賦 null,但并沒有被 GC.Collection
所回收,原因在于 test1 被棧中的 ebp+14
位置所持有?那這個位置是咋回事?我們反編譯下代碼看看,簡化后如下:
0:000>?!U?04C405D1
Normal?JIT?generated?code
ConsoleApp1.Program.ProcessRequest()
ilAddr?is?0268205C?pImport?is?052FB030
Begin?04C40488,?size?154D:\net5\ConsoleApp2\ConsoleApp1\Program.cs?@?22:
04c404aa?b90890c304??????mov?????ecx,4C39008h?(MT:?ConsoleApp1.Test)
04c404af?e8182c9afb??????call????005e30cc?(JitHelp:?CORINFO_HELP_NEWSFAST)
04c404b4?8945ec??????????mov?????dword?ptr?[ebp-14h],eax
04c404b7?8b4dec??????????mov?????ecx,dword?ptr?[ebp-14h]
04c404ba?ff152890c304????call????dword?ptr?ds:[4C39028h]?(ConsoleApp1.Test..ctor(),?mdToken:?06000004)
04c404c0?8b4dec??????????mov?????ecx,dword?ptr?[ebp-14h]
04c404c3?c741040a000000??mov?????dword?ptr?[ecx+4],0Ah
04c404ca?8b4dec??????????mov?????ecx,dword?ptr?[ebp-14h]
04c404cd?894df8??????????mov?????dword?ptr?[ebp-8],ecxD:\net5\ConsoleApp2\ConsoleApp1\Program.cs?@?29:
04c4055c?33c9????????????xor?????ecx,ecx
04c4055e?894df8??????????mov?????dword?ptr?[ebp-8],ecx
雖然 !gcroot
上顯示的是 ebp+14
,反向就是 ebp-14
,仔細看上面的匯編代碼,可以發現 test1
實例被放在了 ebp-14
和 ebp-8
兩個棧位置,而 test1=null
只是抹去了 ebp-8
的棧單元,所以它能被回收的時機只能是等 ProcessRequest()
方法銷毀之后,這也就是 Debug 模式下的 方法作用域,應該是為了 Debug 調試用的,從 gcinfo
上也可以看出來,ebp-14
是禁止被GC跟蹤的內部用途的棧單元。
0:000>?!U?-gcinfo?04C405D1
Normal?JIT?generated?code
ConsoleApp1.Program.ProcessRequest()
ilAddr?is?0268205C?pImport?is?052FCA58
Begin?04C40488,?size?154D:\net5\ConsoleApp2\ConsoleApp1\Program.cs?@?21:[EBP-08H]?an?untracked??local[EBP-0CH]?an?untracked??local[EBP-10H]?an?untracked??local[EBP-14H]?an?untracked??local[EBP-18H]?an?untracked??local[EBP-1CH]?an?untracked??local[EBP-20H]?an?untracked??local[EBP-24H]?an?untracked??local[EBP-28H]?an?untracked??local[EBP-2CH]?an?untracked??local[EBP-30H]?an?untracked??local
二:Release 模式
大家或許都知道 Release
是一種高度優化的激進模式,我也很好奇在這種模式下 compile
或者 ?JIT
會做出怎么樣的優化。
1. 編譯器層面的優化
要尋找這個答案,我們用 ILSpy 打開生成的 IL代碼,簡化后如下:
.method?private?hidebysig?static?void?ProcessRequest?()?cil?managed?{//?Method?begins?at?RVA?0x2058//?Code?size?144?(0x90).maxstack?3.locals?init?([0]?class?ConsoleApp1.Test?test1,[1]?class?ConsoleApp1.Test?test2,[2]?class?ConsoleApp1.Test?test3)IL_0050:?ldnullIL_0051:?stloc.0}?//?end?of?method?Program::ProcessRequest
從 idnull
上來看,沒有做任何優化,居然直接翻譯了,哎。。。
2. JIT優化
查看 JIT 層面的優化,只能看最終的匯編代碼
和 托管堆
啦。
0:000>?!dumpheap?-type?TestAddress???????MT?????Size
02eaab38?02634b10???????12?????
02ead344?02634b10???????12?????
02ead380?02634b10???????12?????Statistics:MT????Count????TotalSize?Class?Name
02634b10????????3???????????36?ConsoleApp1.Test
Total?3?objects0:000>?!U?/d?0262549d
Normal?JIT?generated?code
ConsoleApp1.Program.ProcessRequest()
ilAddr?is?025B2058?pImport?is?04AFB108
Begin?02625370,?size?131D:\net5\ConsoleApp2\ConsoleApp1\Program.cs?@?22:
02625370?55??????????????push????ebp
02625371?8bec????????????mov?????ebp,esp
0262538a?b9104b6302??????mov?????ecx,2634B10h?(MT:?ConsoleApp1.Test)
0262538f?e83cddfefd??????call????006130d0?(JitHelp:?CORINFO_HELP_NEWSFAST)
02625394?8945f0??????????mov?????dword?ptr?[ebp-10h],eax
02625397?8b4df0??????????mov?????ecx,dword?ptr?[ebp-10h]
0262539a?e871f9ffff??????call????02624d10
0262539f?8b4df0??????????mov?????ecx,dword?ptr?[ebp-10h]
026253a2?c741040a000000??mov?????dword?ptr?[ecx+4],0Ah
026253a9?8b4df0??????????mov?????ecx,dword?ptr?[ebp-10h]
026253ac?894dfc??????????mov?????dword?ptr?[ebp-4],ecxD:\net5\ConsoleApp2\ConsoleApp1\Program.cs?@?29:
02625430?33c9????????????xor?????ecx,ecx
02625432?894dfc??????????mov?????dword?ptr?[ebp-4],ecx
從匯編代碼看,Release
模式下也是采用雙棧保存的,也就是 方法級作用域
。
二:可以得出結論了嗎?
至少在 .NET5
平臺, Release
和 Debug
模式下的 test1 = null;
是沒有任何區別的,其實這里有個問題 , .NET5
下沒區別,不代表其他平臺下也沒有問題,畢竟不同的 JIT
會作用不同的抉擇,接下來我們將同樣的代碼搬到 .NET Framework 4.5
下看看情況。
1. .NET Framework 4.5 平臺
Debug 模式
我們直接看托管代碼
0:006>?!dumpheap?-type?TestAddress???????MT?????Size
02564bfc?00754ddc???????12?????
02564c70?00754ddc???????12?????Statistics:MT????Count????TotalSize?Class?Name
00754ddc????????2???????????24?ConsoleApp2.Test
Total?2?objects
居然是 2 個了,那為什么會這樣呢?我們還是看下匯編。
0:000>?!U?/d?023509a6
Normal?JIT?generated?code
ConsoleApp2.Program.ProcessRequest()
Begin?02350880,?size?187
D:\net5\ConsoleApp2\ConsoleApp2\Program.cs?@?21:
023508b1?b9dc4da200??????mov?????ecx,0A24DDCh?(MT:?ConsoleApp2.Test)
023508b6?e839286cfe??????call????00a130f4?(JitHelp:?CORINFO_HELP_NEWSFAST)
023508bb?8945ec??????????mov?????dword?ptr?[ebp-14h],eax
023508be?8b4dec??????????mov?????ecx,dword?ptr?[ebp-14h]
023508c1?ff15fc4da200????call????dword?ptr?ds:[0A24DFCh]?(ConsoleApp2.Test..ctor(),?mdToken:?06000004)
023508c7?8b45ec??????????mov?????eax,dword?ptr?[ebp-14h]
023508ca?c740040a000000??mov?????dword?ptr?[eax+4],0Ah
023508d1?8b45ec??????????mov?????eax,dword?ptr?[ebp-14h]
023508d4?8945f8??????????mov?????dword?ptr?[ebp-8],eax
D:\net5\ConsoleApp2\ConsoleApp2\Program.cs?@?28:
0235097b?33d2????????????xor?????edx,edx
0235097d?8955f8??????????mov?????dword?ptr?[ebp-8],edx0:000>?dp?ebp-14h?L1
0019f4e8??02472358?0:000>?!do?02472358
Name:????????ConsoleApp2.Test
MethodTable:?00a24ddc
EEClass:?????00a21330
Size:????????12(0xc)?bytes
File:????????D:\net5\ConsoleApp2\ConsoleApp2\bin\Debug\ConsoleApp2.exe
Fields:MT????Field???Offset?????????????????Type?VT?????Attr????Value?Name
637342a8??4000001????????4?????????System.Int32??1?instance???????10?a0:000>?dp?0019f4e8?L1
0019f4e8??02472358
0:000>?!do?02472358
Free?Object
Size:????????24(0x18)?bytes
大家可以仔細看看輸出內容,雖然也是兩個 棧位置
存放著 test1,但GC做了不同的處理,它無視 ebp-14
還牽引著 test1
的事實 ,直接將它標記為 free,這就有點意思了。
Release 模式
我們直接用 !dumpheap -type Test
看托管堆。
0:006>?!dumpheap?-type?TestAddress???????MT?????SizeStatistics:MT????Count????TotalSize?Class?Name
Total?0?objects
居然發現,不僅 test1
沒有了,test2,test3
都沒有了。。。這就是所謂的 激進式回收
。
三:結論
1. ?.NET5
平臺下
Release 和 Debug 模式下設置 test1=null
沒有任何效果。
2. .NET Framework 4.5
平臺下
Debug 模式下有效果,可以起到 提前回收
的目的。
Release模式下無效果,GC會自動激進的回收所有后續未使用到的引用對象。
3. 個人結論
總的來說,為了更好的平臺兼容性,如果想提前回收,設置 test1 = null;
是有一定效果的。