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

为了方便讲解,来一段测试代码,提前将 test1=null ,然后调用 GC.Collect() 看看是否能提前回收。

平台采用: .net5


  1. public class Program
  2. {
  3. static void Main(string[] args)
  4. {
  5. ProcessRequest();
  6. }
  7. static void ProcessRequest()
  8. {
  9. var test1 = new Test() { a = 10 };
  10. Console.WriteLine($"query.a={test1.a}");
  11. var test2 = new Test() { a = 11 };
  12. Console.WriteLine($"query.a={test2.a}");
  13. //提前释放
  14. test1 = null;
  15. var test3 = new Test() { a = 12 };
  16. Console.WriteLine($"query.a={test3.a}");
  17. GC.Collect();
  18. Console.WriteLine("垃圾回收啦!");
  19. Console.ReadLine();
  20. }
  21. }
  22. public class Test
  23. {
  24. public int a;
  25. }

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

一:Debug 模式

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


  1. 0:000> !clrstack -a
  2. OS Thread Id: 0x4dd0 (0)
  3. Child SP IP Call Site
  4. 0057F2A4 79863539 System.Console.ReadLine() [/_/src/System.Console/src/System/Console.cs @ 463]
  5. 0057F2AC 04c405d1 ConsoleApp1.Program.ProcessRequest() [D:\net5\ConsoleApp2\ConsoleApp1\Program.cs @ 37]
  6. LOCALS:
  7. 0x0057F2D4 = 0x00000000
  8. 0x0057F2D0 = 0x0283cd54
  9. 0x0057F2CC = 0x0283cd90
  10. 0:000> !dumpheap -type Test
  11. Address MT Size
  12. 0283a7c0 04c39008 12
  13. 0283cd54 04c39008 12
  14. 0283cd90 04c39008 12
  15. 0:000> !gcroot 0283a7c0
  16. Thread 4dd0:
  17. 0057F2AC 04C405D1 ConsoleApp1.Program.ProcessRequest() [D:\net5\ConsoleApp2\ConsoleApp1\Program.cs @ 37]
  18. ebp+14: 0057f2c8
  19. -> 0283A7C0 ConsoleApp1.Test
  20. Found 1 unique roots (run '!gcroot -all' to see all roots).

是不是很惊讶,test1 虽被赋 null,但并没有被 GC.Collection 所回收,原因在于 test1 被栈中的 ebp+14 位置所持有?那这个位置是咋回事? 我们反编译下代码看看,简化后如下:


  1. 0:000> !U 04C405D1
  2. Normal JIT generated code
  3. ConsoleApp1.Program.ProcessRequest()
  4. ilAddr is 0268205C pImport is 052FB030
  5. Begin 04C40488, size 154
  6. D:\net5\ConsoleApp2\ConsoleApp1\Program.cs @ 22:
  7. 04c404aa b90890c304 mov ecx,4C39008h (MT: ConsoleApp1.Test)
  8. 04c404af e8182c9afb call 005e30cc (JitHelp: CORINFO_HELP_NEWSFAST)
  9. 04c404b4 8945ec mov dword ptr [ebp-14h],eax
  10. 04c404b7 8b4dec mov ecx,dword ptr [ebp-14h]
  11. 04c404ba ff152890c304 call dword ptr ds:[4C39028h] (ConsoleApp1.Test..ctor(), mdToken: 06000004)
  12. 04c404c0 8b4dec mov ecx,dword ptr [ebp-14h]
  13. 04c404c3 c741040a000000 mov dword ptr [ecx+4],0Ah
  14. 04c404ca 8b4dec mov ecx,dword ptr [ebp-14h]
  15. 04c404cd 894df8 mov dword ptr [ebp-8],ecx
  16. D:\net5\ConsoleApp2\ConsoleApp1\Program.cs @ 29:
  17. 04c4055c 33c9 xor ecx,ecx
  18. 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跟踪的内部用途的栈单元。


  1. 0:000> !U -gcinfo 04C405D1
  2. Normal JIT generated code
  3. ConsoleApp1.Program.ProcessRequest()
  4. ilAddr is 0268205C pImport is 052FCA58
  5. Begin 04C40488, size 154
  6. D:\net5\ConsoleApp2\ConsoleApp1\Program.cs @ 21:
  7. [EBP-08H] an untracked local
  8. [EBP-0CH] an untracked local
  9. [EBP-10H] an untracked local
  10. [EBP-14H] an untracked local
  11. [EBP-18H] an untracked local
  12. [EBP-1CH] an untracked local
  13. [EBP-20H] an untracked local
  14. [EBP-24H] an untracked local
  15. [EBP-28H] an untracked local
  16. [EBP-2CH] an untracked local
  17. [EBP-30H] an untracked local

二:Release 模式

大家或许都知道 Release 是一种高度优化的激进模式,我也很好奇在这种模式下 compile 或者 JIT 会做出怎么样的优化。

1. 编译器层面的优化

要寻找这个答案,我们用 ILSpy 打开生成的 IL代码,简化后如下:


  1. .method private hidebysig static
  2. void ProcessRequest () cil managed
  3. {
  4. // Method begins at RVA 0x2058
  5. // Code size 144 (0x90)
  6. .maxstack 3
  7. .locals init (
  8. [0] class ConsoleApp1.Test test1,
  9. [1] class ConsoleApp1.Test test2,
  10. [2] class ConsoleApp1.Test test3
  11. )
  12. IL_0050: ldnull
  13. IL_0051: stloc.0
  14. } // end of method Program::ProcessRequest

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

2. JIT优化

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


  1. 0:000> !dumpheap -type Test
  2. Address MT Size
  3. 02eaab38 02634b10 12
  4. 02ead344 02634b10 12
  5. 02ead380 02634b10 12
  6. Statistics:
  7. MT Count TotalSize Class Name
  8. 02634b10 3 36 ConsoleApp1.Test
  9. Total 3 objects
  10. 0:000> !U /d 0262549d
  11. Normal JIT generated code
  12. ConsoleApp1.Program.ProcessRequest()
  13. ilAddr is 025B2058 pImport is 04AFB108
  14. Begin 02625370, size 131
  15. D:\net5\ConsoleApp2\ConsoleApp1\Program.cs @ 22:
  16. 02625370 55 push ebp
  17. 02625371 8bec mov ebp,esp
  18. 0262538a b9104b6302 mov ecx,2634B10h (MT: ConsoleApp1.Test)
  19. 0262538f e83cddfefd call 006130d0 (JitHelp: CORINFO_HELP_NEWSFAST)
  20. 02625394 8945f0 mov dword ptr [ebp-10h],eax
  21. 02625397 8b4df0 mov ecx,dword ptr [ebp-10h]
  22. 0262539a e871f9ffff call 02624d10
  23. 0262539f 8b4df0 mov ecx,dword ptr [ebp-10h]
  24. 026253a2 c741040a000000 mov dword ptr [ecx+4],0Ah
  25. 026253a9 8b4df0 mov ecx,dword ptr [ebp-10h]
  26. 026253ac 894dfc mov dword ptr [ebp-4],ecx
  27. D:\net5\ConsoleApp2\ConsoleApp1\Program.cs @ 29:
  28. 02625430 33c9 xor ecx,ecx
  29. 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 模式

我们直接看托管代码


  1. 0:006> !dumpheap -type Test
  2. Address MT Size
  3. 02564bfc 00754ddc 12
  4. 02564c70 00754ddc 12
  5. Statistics:
  6. MT Count TotalSize Class Name
  7. 00754ddc 2 24 ConsoleApp2.Test
  8. Total 2 objects

居然是 2 个了,那为什么会这样呢? 我们还是看下汇编。


  1. 0:000> !U /d 023509a6
  2. Normal JIT generated code
  3. ConsoleApp2.Program.ProcessRequest()
  4. Begin 02350880, size 187
  5. D:\net5\ConsoleApp2\ConsoleApp2\Program.cs @ 21:
  6. 023508b1 b9dc4da200 mov ecx,0A24DDCh (MT: ConsoleApp2.Test)
  7. 023508b6 e839286cfe call 00a130f4 (JitHelp: CORINFO_HELP_NEWSFAST)
  8. 023508bb 8945ec mov dword ptr [ebp-14h],eax
  9. 023508be 8b4dec mov ecx,dword ptr [ebp-14h]
  10. 023508c1 ff15fc4da200 call dword ptr ds:[0A24DFCh] (ConsoleApp2.Test..ctor(), mdToken: 06000004)
  11. 023508c7 8b45ec mov eax,dword ptr [ebp-14h]
  12. 023508ca c740040a000000 mov dword ptr [eax+4],0Ah
  13. 023508d1 8b45ec mov eax,dword ptr [ebp-14h]
  14. 023508d4 8945f8 mov dword ptr [ebp-8],eax
  15. D:\net5\ConsoleApp2\ConsoleApp2\Program.cs @ 28:
  16. 0235097b 33d2 xor edx,edx
  17. 0235097d 8955f8 mov dword ptr [ebp-8],edx
  18. 0:000> dp ebp-14h L1
  19. 0019f4e8 02472358
  20. 0:000> !do 02472358
  21. Name: ConsoleApp2.Test
  22. MethodTable: 00a24ddc
  23. EEClass: 00a21330
  24. Size: 12(0xc) bytes
  25. File: D:\net5\ConsoleApp2\ConsoleApp2\bin\Debug\ConsoleApp2.exe
  26. Fields:
  27. MT Field Offset Type VT Attr Value Name
  28. 637342a8 4000001 4 System.Int32 1 instance 10 a
  29. 0:000> dp 0019f4e8 L1
  30. 0019f4e8 02472358
  31. 0:000> !do 02472358
  32. Free Object
  33. Size: 24(0x18) bytes

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

  1. Release 模式

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


  1. 0:006> !dumpheap -type Test
  2. Address MT Size
  3. Statistics:
  4. MT Count TotalSize Class Name
  5. Total 0 objects

居然发现,不仅 test1 没有了,test2,test3 都没有了。。。这就是所谓的 激进式回收

三:结论

1. .NET5 平台下

Release 和 Debug 模式下设置 test1=null 没有任何效果。

2. .NET Framework 4.5 平台下

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

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

3. 个人结论

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

过早的给方法中 引用对象 设为 null 可被 GC提前回收吗?的更多相关文章

  1. Powermockito 针对方法中new 对象的模拟,以及属性中new 对象的模拟

    PowerMocker 是一个功能逆天的mock 工具. 一,Powermockito 针对方法中new 对象的模拟 // 如何才能mock掉 WeChatConfigUtil 这个类,让 weCha ...

  2. 在JVM中,新生代和旧生代有何区别?GC的回收方式有几种?server和client有和区别?

    在JVM中,新生代和旧生代有何区别?GC的回收方式有几种?server和client有和区别? 2014-04-12 12:09 7226人阅读 评论(0) 收藏 举报  分类: J2SE(5)  一 ...

  3. java方法中把对象置null,到底能不能加速垃圾回收

    今天逛脉脉,看见匿名区有人说java中把对做置null,这种做法很菜,不能加速垃圾回收,但是我看到就觉得呵呵了,我是觉得可以加速置null对象回收的. 测试的过程中,费劲的是要指定一个合理的测试堆大小 ...

  4. 浅谈String中的==和对象中引用对象类型的==

    @Test public void test02() { StringBuffer sb = new StringBuffer(); sb.append('a'); sb.append(11); Sy ...

  5. 用powermock 方法中new对象

    在单元测试中有时需要对方法体内new出来的对象进行方法隔离,powermock提供了这个功能,下面是一个段样例代码: UserBean user = mock(UserBean.class, RETU ...

  6. ①创建项目testpackage ②在pack2.B中添加方法f ③在类A中添加如下三个成员变量:int型的私有变量i float型的变量f double型的公有变量d 在pack1.B的main方法中为对象a的成员变量f和d分别赋值为2和3 在pack2.C的main方法中为对象a的成员变量d赋值为3

    package pack1; public class A { private int i; float f; public double d; public float getF() { retur ...

  7. Set重写hashCode和equals方法实现引用对象去重

    运作原理: 首先判断hashCode是否相同,如果不同,直接判定为两个不同的对象.如果hashCode相同,再去比较equals是否一样,如果一样,则为同一个对象.如果不一样,则是两个不同对象. 那么 ...

  8. JVM 中的对象及引用

    JVM中对象的创建过程 对象的内存分配 虚拟机遇到一条 new 指令时,首先检查是否被类加载器加载,如果没有,那必须先执行相应的类加载过程. 类加载就是把 class 加载到 JVM 的运行时数据区的 ...

  9. Andorid Binder进程间通信---Binder本地对象,实体对象,引用对象,代理对象的引用计数

    本文參考<Android系统源码情景分析>,作者罗升阳. 一.Binder库(libbinder)代码: ~/Android/frameworks/base/libs/binder --- ...

随机推荐

  1. it-术语

    QPS:每秒查询率QPS是对一个特定的查询服务器在规定时间内所处理流量多少的衡量标准. 因特网上,经常用每秒查询率来衡量域名系统服务器的机器的性能,其即为QPS. 对应fetches/sec,即每秒的 ...

  2. 什么是 spring bean?

    它们是构成用户应用程序主干的对象. Bean 由 Spring IoC 容器管理. 它们由 Spring IoC 容器实例化,配置,装配和管理. Bean 是基于用户提供给容器的配置元数据创建.

  3. 客户端回调 Watcher?

    客户端 SendThread 线程接收事件通知,交由 EventThread 线程回调 Watcher. 客户端的 Watcher 机制同样是一次性的,一旦被触发后,该 Watcher 就失效了.

  4. SpringCloud微服务治理技术入门(SCN)

    1.集群.分布式.微服务 首先先理解三个感念 什么是集群?: 同一个业务,部署在多个服务器上,目的是实现高可用,保证节点可用! 什么是分布式?: 一个业务分拆成多个子业务,部署在不同的服务器上,每个子 ...

  5. uniapp清理缓存

    <template> <view class="content"> <view>应用缓存:{{storageSize}}</view> ...

  6. Ubuntu16.04 安装和卸载MySQL数据库

    Ubuntu16.04 安装和卸载MySQL数据库 1 安装 安装非常简单,只需要三个命令 1.1 安装服务端 sudo apt-get install mysql-server 在这一步过程中会有提 ...

  7. 虚拟机VMware的安装与Xshell的应用

    先安装VMware 1.安装就按照提示一点点安装就行了 配置网络 打开VMware 这里的IOS映像文件在https://developer.aliyun.com/mirror/里下载 这里用方向键往 ...

  8. shell、bash和sh区别

    shell是你(用户)和Linux(或者更准确的说,是你和Linux内核)之间的接口程序.你在提示符下输入的每个命令都由shell先解释然后传给Linux内核. shell 是一个命令语言解释器(co ...

  9. QT-进制转换计算器

    适合初学者练手 用QT做的一个进制转换工具,主要涉及数据类型转换.//后面再加上基本的计算. Github地址:https://github.com/wsdassssss/Calculate.git ...

  10. Lumia一键刷稳定版 Win10 arm 及其报错处理

    前言 之前我发了一篇Lumia1520 刷Win10 arm双系统的文章,不过后来发现那个方法对小白来说太不友好,且系统也不稳定,所以我找到了更好的方法 刷机 我们可以利用刷机迷进行刷机,支持一键刷机 ...