编译器带来的意外垃圾回收结果





5.00/5 (14投票s)
澄清 .NET GC 如何识别要回收的对象,一劳永逸... 希望如此...
引言
关于 .NET 垃圾回收器 (GC),仍然存在一些常见的误解。最常见的误解似乎是,回收的资格以某种方式与对象使用或变量作用域相关联。事实上,GC 对这两者都漠不关心。它只关心对象是活的还是死的(即,不可达的)。 表面上证明其中一个的运行时效果仅仅是由于源代码的不同 MSIL 表示形式造成的。 但让我们一步一步来。
“证明”GC 不关心使用情况
这是一个小程序,它是常见示例的一个变体,该示例据称证明了 GC 分析变量的使用情况。或者不是。程序很简单。它启动一个计时器,延迟和间隔为 1 秒,等待用户按下按键,然后强制回收,然后等待第二个按键退出。
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!");
}
如果 GC 考虑使用情况,它可以在方法体第一行之后自由地回收计时器,并且在强制回收时肯定会回收它。对于第一个实验,我使用未优化的调试版本运行了该程序(稍后您会看到原因)。结果如下
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 中,一个方法是一个作用域,原生代码表示中的函数也是一个作用域,但是这些作用域不必匹配。考虑这个毫无意义的方法
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 ([0] 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
} // end of method Program::Scope
已编译的原生代码中的作用域可能会因优化(例如内联)而有所不同。但与手头的问题更相关的是,仅仅因为我们在 C# 代码中有一个局部变量,并不意味着在已编译的 MSIL 中一定必须有一个局部变量。当没有优化编译时,计时器程序简化为第一行,看起来像这样。
.method public hidebysig static void Main(string[] args) cil managed
{
.entrypoint
// Code size 50 (0x32)
.maxstack 4
.locals init ([0] 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
} // end of method Program::Main
这是有道理的。我们声明了一个局部变量,所以在 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
} // end of method Program::Main
没错,优化的 MSIL 没有局部变量,这实际上解释了为什么计时器在发布版本中被回收,而不是在调试版本中。在发布版本中,没有引用保留在堆栈或任何其他地方。是的,该对象是“活的”,它正在做某事。但是对于 GC 来说,它是死的,因为它不可达;任何调用者都无法访问它。所以回收它没问题。就这么简单。
历史
- 2013-08-10: 更改标题并添加标签
- 2013-08-09: 初始发布