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

经常在代码中看到有人将 赋值给引用类型,来达到让 GC 提前回收的目的,这样做真的有用吗?今天我们就来研究一下。

为了方便讲解,来一段测试代码,提前将 test1=,然后调用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 = ;

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:net5ConsoleApp2ConsoleApp1Program.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:net5ConsoleApp2ConsoleApp1Program.cs @ 37]
ebp+14: 0057f2c8
-> 0283A7C0 ConsoleApp1.Test

Found 1 unique roots (run '!gcroot -all' to see all roots).

是不是很惊讶,test1 虽被赋 ,但并没有被 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:net5ConsoleApp2ConsoleApp1Program.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:net5ConsoleApp2ConsoleApp1Program.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=只是抹去了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:net5ConsoleApp2ConsoleApp1Program.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: ld
IL_0051: stloc.0

} // end of method Program::ProcessRequest

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

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:net5ConsoleApp2ConsoleApp1Program.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:net5ConsoleApp2ConsoleApp1Program.cs @ 29:
02625430 33c9 xor ecx,ecx
02625432 894dfc mov dword ptr [ebp-4],ecx

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

二:可以得出结论了吗?

至少在 .NET5平台,ReleaseDebug模式下的test1 = ;是没有任何区别的,其实这里有个问题 ,.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:net5ConsoleApp2ConsoleApp2Program.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:net5ConsoleApp2ConsoleApp2Program.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:net5ConsoleApp2ConsoleApp2binDebugConsoleApp2.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=没有任何效果。

2. .NET Framework 4.5平台下

Debug 模式下有效果,可以起到 提前回收的目的。

Release模式下无效果,GC会自动激进的回收所有后续未使用到的引用对象。

3. 个人结论

总的来说,为了更好的平台兼容性,如果想提前回收,设置 test1 = ;是有一定效果的。

声明:本站部分文章内容及图片转载于互联 、内容不代表本站观点,如有内容涉及侵权,请您立即联系本站处理,非常感谢!

(0)
上一篇 2022年9月18日
下一篇 2022年9月18日

相关推荐