资讯详情

过早的给方法中 引用对象 设为 null 可被 GC提前回收吗?

经常在代码中看到人 null 赋值给引用类型,实现让值 GC 提前回收真的有用吗?今天我们来研究一下。

为了方便解释,提前进行测试代码 test1=null ,然后调用 GC.Collect() 看看不能提前回收。

: .net5

publicclassProgram { staticvoidMain(string[]args) { ProcessRequest(); }  staticvoidProcessRequest() { vartest1=newTest(){a=10}; Console.WriteLine($"query.a={test1.a}");  vartest2=newTest(){a=11}; Console.WriteLine($"query.a={test2.a}");  ///提前释放 test1=null;  vartest3=newTest(){a=12}; Console.WriteLine($"query.a={test3.a}");  GC.Collect(); Console.WriteLine("垃圾回收!");  Console.ReadLine(); } }  publicclassTest { publicinta; }

接下来我们从 DebugRelease 观察两种模式。

一:Debug 模式

为了找到这个答案,我们使用它 windbg 附加,找到 test1 然后用 !gcroot 查看下引用即可。

0:000>!clrstack-a OSThreadId:0x4dd0(0) ChildSPIPCallSite 0057F2A479863539System.Console.ReadLine()[/_/src/System.Console/src/System/Console.cs@463] 0057F2AC04c405d1ConsoleApp1.Program.ProcessRequest()[D:\net5\ConsoleApp2\ConsoleApp1\Program.cs@37] LOCALS: 0x0057F2D4=0x00000000 0x0057F2D0=0x0283cd54 0x0057F2CC=0x0283cd90  0:000>!dumpheap-typeTest AddressMTSize 0283a7c004c3900812 0283cd5404c3900812 0283cd9004c3900812  0:000>!gcroot0283a7c0 Thread4dd0: 0057F2AC04C405D1ConsoleApp1.Program.ProcessRequest()[D:\net5\ConsoleApp2\ConsoleApp1\Program.cs@37] ebp 14:0057f2c8 ->0283A7C0ConsoleApp1.Test  Found1uniqueroots(run'!gcroot-all'toseeallroots).

不是很惊讶,test1 虽被赋 null,但并没有被 GC.Collection 原因是回收 test1 被栈中的 ebp 14 持有位置?这个位置怎么了?我们反编译代码,简化如下:

0:000>!U04C405D1 NormalJITgeneratedcode ConsoleApp1.Program.ProcessRequest() ilAddris0268205CpImportis052FB030 Begin04C40488,size154  D:\net5\ConsoleApp2\ConsoleApp1\Program.cs@22: 04c404aab90890c304movecx,4C39008h(MT:ConsoleApp1.Test) 04c404afe8182c9afbcall005e30cc(JitHep: 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],ecx

D:\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-14ebp-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 154

D:\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: ldnull
  IL_0051: stloc.0

 } // end of method Program::ProcessRequest

idnull 上来看,没有做任何优化,居然直接翻译了,哎。。。

2. JIT优化

查看 JIT 层面的优化,只能看最终的汇编代码托管堆 啦。

0:000> !dumpheap -type Test
 Address       MT     Size
02eaab38 02634b10       12     
02ead344 02634b10       12     
02ead380 02634b10       12     

Statistics:
      MT    Count    TotalSize Class Name
02634b10        3           36 ConsoleApp1.Test
Total 3 objects

0:000> !U /d 0262549d
Normal JIT generated code
ConsoleApp1.Program.ProcessRequest()
ilAddr is 025B2058 pImport is 04AFB108
Begin 02625370, size 131

D:\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],ecx

D:\net5\ConsoleApp2\ConsoleApp1\Program.cs @ 29:
02625430 33c9            xor     ecx,ecx
02625432 894dfc          mov     dword ptr [ebp-4],ecx

从汇编代码看,Release 模式下也是采用双栈保存的,也就是 方法级作用域

二:可以得出结论了吗?

至少在 .NET5 平台, ReleaseDebug 模式下的 test1 = null; 是没有任何区别的,其实这里有个问题 , .NET5 下没区别,不代表其他平台下也没有问题,毕竟不同的 JIT 会作用不同的抉择,接下来我们将同样的代码搬到 .NET Framework 4.5 下看看情况。

1. .NET Framework 4.5 平台

  1. Debug 模式

我们直接看托管代码

0:006> !dumpheap -type Test
 Address       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],edx

0: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 a

0:000> dp 0019f4e8 L1
0019f4e8  02472358
0:000> !do 02472358
Free Object
Size:        24(0x18) bytes

大家可以仔细看看输出内容,虽然也是两个 栈位置 存放着 test1,但GC做了不同的处理,它无视 ebp-14 还牵引着 test1 的事实 ,直接将它标记为 free,这就有点意思了。

  1. Release 模式

我们直接用 !dumpheap -type Test 看托管堆。

0:006> !dumpheap -type Test
 Address       MT     Size

Statistics:
      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; 是有一定效果的。

标签: 04c热过载继电器lrd

锐单商城拥有海量元器件数据手册IC替代型号,打造 电子元器件IC百科大全!

锐单商城 - 一站式电子元器件采购平台