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


    经常在代码中看到有人将 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;
        }
    
    

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

    一: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 = 0x00000000
            0x0057F2D0 = 0x0283cd54
            0x0057F2CC = 0x0283cd90
    
    0:000> !dumpheap -type Test
     Address       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.Test
    
    Found 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 154
    
    D:\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],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; 是有一定效果的。

    图片名称
  • 相关阅读:
    “摸鱼”就能得出设计灵感?他的经验分享得看
    C语言学习教程
    【干货】Java开发者快速上手.NET指南
    企业级java笔试题,面试题第二篇
    我发布了一款基于RBAC权限模型实现的通用后台管理系统
    【无标题】Java 函数式接口
    计算机组成原理笔记(王道考研) 第一章:计算机系统概述
    【Android】Kotlin学习小结
    你不应该依赖CSS 100vh,这就是原因
    【已解决】ORA-00001: unique constraint (USR_JXZX_DSJKF_MODEL.SYS_C00912833) violated
  • 原文地址:https://www.cnblogs.com/huangxincheng/p/16257664.html