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