![](/style/images/good.png)
![](/style/images/bad.png)
过早的给方法中 引用对象 设为 null 可被 GC提前回收吗?
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;
}
接下来我们从 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 = 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-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 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
平台, Release
和 Debug
模式下的 test1 = null;
是没有任何区别的,其实这里有个问题 , .NET5
下没区别,不代表其他平台下也没有问题,毕竟不同的 JIT
会作用不同的抉择,接下来我们将同样的代码搬到 .NET Framework 4.5
下看看情况。
1. .NET Framework 4.5 平台
- 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,这就有点意思了。
- 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;
是有一定效果的。
![图片名称](https://images.cnblogs.com/cnblogs_com/huangxincheng/345039/o_210929020104%E6%9C%80%E6%96%B0%E6%B6%88%E6%81%AF%E4%BC%98%E6%83%A0%E4%BF%83%E9%94%80%E5%85%AC%E4%BC%97%E5%8F%B7%E5%85%B3%E6%B3%A8%E4%BA%8C%E7%BB%B4%E7%A0%81.jpg)
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK