垃圾回收器与变量作用域无关

在编程语言中,垃圾回收器(GC)的作用是自动管理内存,回收不再使用的内存资源。然而,对于GC是否考虑变量的使用情况,存在一些误解。一些人认为GC会根据变量的作用域或使用情况来决定是否回收内存,但实际上,GC只关心对象是否可达,即是否“活”的或“死”的。本文通过一个简单的C#程序示例,来探讨这个问题,并分析编译优化如何影响GC的行为。

实验一:GC不考虑变量使用情况

首先,来看一个简单的C#程序,该程序启动一个定时器,每隔一秒触发一次,并在用户按下任意键后强制执行垃圾回收,然后等待用户再次按键退出程序。如果GC真的考虑变量的使用情况,那么在方法体的第一行就应该可以回收定时器对象,并且在强制垃圾回收时也应该回收它。以下是程序的代码:

public static void Main(string[] args) { Timer timer = new Timer(state => Console.WriteLine("Tick..."), null, 1000, 1000); Console.WriteLine("Press any key to force a garbage collection."); Console.ReadKey(true); GC.Collect(); Console.WriteLine("Press any key to exit."); Console.ReadKey(true); Console.WriteLine("Good bye!"); }

在未优化的调试构建中运行此程序,结果如下:

Press any key to force a garbage collection. Tick... Tick... Press any key to exit. Tick... Tick... Good bye!

这似乎证明了GC不考虑变量的使用情况。但真的是这样吗?

实验二:GC考虑变量使用情况

接下来,使用优化的发布构建重新运行程序。结果如下:

Press any key to force a garbage collection. Tick... Tick... Press any key to exit. Good bye!

这似乎又证明了GC考虑变量的使用情况。但两个结论不可能同时成立,实际上它们都是错误的。GC只收集不可达的对象。那么,为什么在开启代码优化时,GC会回收一个看似可达的对象呢?

揭开谜团

首先,需要记住,在C#等高级编程语言中定义的变量作用域并不直接转化为编译后的代码。在MSIL(Microsoft Intermediate Language)中,方法是一个作用域,但在原生代码表示中,这些作用域可能并不匹配。考虑以下无意义的方法:

public static void Scope() { { int i = 0; } { int i = 0; } }

在C#中,这是两个不同的变量,都叫i,但每个都在自己的范围内。然而,编译器后端并不在乎。第一个作用域需要一个int类型的局部变量,但在退出第一个作用域后,C#源代码中就不存在了。实际上,在MSIL中仍然存在,这意味着它可以被第二个作用域中的整数i重用。因此,编译器只发出一个局部变量:

.method public hidebysig static void Scope() cil managed { // Code size 10 (0xa) .maxstack 1 .locals init ([int32 i]) IL_0000: nop IL_0001: nop IL_0002: ldc.i4.0 IL_0003: stloc.0 IL_0004: nop IL_0005: nop IL_0006: ldc.i4.0 IL_0007: stloc.0 IL_0008: nop IL_0009: ret }

编译后的本地代码中的作用域可能会因优化(如内联)而进一步不同。与问题更相关的是,仅仅因为在C#代码中有局部变量,并不意味着在编译的MSIL中必须有一个。以下是未优化时编译的定时器程序:

.method public hidebysig static void Main(string[] args) cil managed { .entrypoint // Code size 50 (0x32) .maxstack 4 .locals init ([class [mscorlib]System.Threading.Timer timer]) IL_0000: nop IL_0001: ldsfld class [mscorlib]System.Threading.TimerCallback GCTest.Program::'CS$<>9__CachedAnonymousMethodDelegate1' IL_0006: brtrue.s IL_001b IL_0008: ldnull IL_0009: ldftn void GCTest.Program::'<Main>b__0'(object) IL_000f: newobj instance void [mscorlib]System.Threading.TimerCallback::.ctor(object, native int) IL_0014: stsfld class [mscorlib]System.Threading.TimerCallback GCTest.Program::'CS$<>9__CachedAnonymousMethodDelegate1' IL_0019: br.s IL_001b IL_001b: ldsfld class [mscorlib]System.Threading.TimerCallback GCTest.Program::'CS$<>9__CachedAnonymousMethodDelegate1' IL_0020: ldnull IL_0021: ldc.i4 0x3e8 IL_0026: ldc.i4 0x3e8 IL_002b: newobj instance void [mscorlib]System.Threading.Timer::.ctor(class [mscorlib]System.Threading.TimerCallback, object, int32, int32) IL_0030: stloc.0 IL_0031: ret }

这很有意义。声明了一个局部变量,所以在MSIL中有一个局部变量。创建了一个新的实例,并将引用存储在局部变量中,MSIL也是如此。但一旦打开优化,编译器就会比更聪明。它说:“好吧,想写一个局部变量的引用,但不会再读它了。知道要做什么,干脆省略它!”

.method public hidebysig static void Main(string[] args) cil managed { .entrypoint // Code size 47 (0x2f) .maxstack 8 IL_0000: ldsfld class [mscorlib]System.Threading.TimerCallback GCTest.Program::'CS$<>9__CachedAnonymousMethodDelegate1' IL_0005: brtrue.s IL_0018 IL_0007: ldnull IL_0008: ldftn void GCTest.Program::'<Main>b__0'(object) IL_000e: newobj instance void [mscorlib]System.Threading.TimerCallback::.ctor(object, native int) IL_0013: stsfld class [mscorlib]System.Threading.TimerCallback GCTest.Program::'CS$<>9__CachedAnonymousMethodDelegate1' IL_0018: ldsfld class [mscorlib]System.Threading.TimerCallback GCTest.Program::'CS$<>9__CachedAnonymousMethodDelegate1' IL_001d: ldnull IL_001e: ldc.i4 0x3e8 IL_0023: ldc.i4 0x3e8 IL_0028: newobj instance void [mscorlib]System.Threading.Timer::.ctor(class [mscorlib]System.Threading.TimerCallback, object, int32, int32) IL_002d: pop IL_002e: ret }
沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485