1

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

 2 years ago
source link: https://www.cnblogs.com/huangxincheng/p/16257664.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

过早的给方法中 引用对象 设为 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; 是有一定效果的。

图片名称

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK