【问题标题】:Why .NET 5 GC doesn't collect (or at least calling Finalize) clearly dereferenced objects?为什么 .NET 5 GC 不收集(或至少调用 Finalize)明确取消引用的对象?
【发布时间】:2021-04-15 20:48:23
【问题描述】:

我想测试垃圾收集器,但很难做到。

我编写了以下简单的测试代码:

using System;

class Foo
{
   int  i;
        
   public Foo(int v)
   {
      i = v;
      Console.WriteLine($"{i} was born");
   }
   ~Foo()
   {
       Console.WriteLine($"{i} has died");
   }
}

public class Program
{
   [STAThread]
   public static void Main(string[] args)
   {
       Foo n1 = new Foo(1);
       Foo n2 = new Foo(2);
       Foo n3 = new Foo(3);
       Foo n4 = new Foo(4);
            
       Console.WriteLine("built everything");
       GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, true, true);
       GC.WaitForPendingFinalizers();
       System.Threading.Thread.Sleep(1000);
                        
       n1 = null;
       n2 = null;
       n3 = n4;
            
       Console.WriteLine("deref n1..n3");
       GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, true, true);
       GC.WaitForPendingFinalizers();
       System.Threading.Thread.Sleep(1000);
       Console.WriteLine("done.");
   }
}

并注意到在 .NET475 和 .NET5 Fiddle 下,

1 出生
2 出生
3 出生
4 出生
建造一切
deref n1..n3
完成。

垃圾收集器要么不调用我的终结器,要么不完全收集我取消引用的对象。
(注意:我的机器上也是这样,没有 Online Fiddle)

有趣的是,在尝试Roslyn3.8 Fiddle 时,它似乎有效。

1 出生
2 出生
3 出生
4 出生
建造一切
deref n1..n3
3人死亡
2人死亡
1人死亡
完成。

(注意:它甚至可以在没有我对 GC 的所有“强制一切”的情况下工作。)

我目前对 .NET GC 存在不信任危机 :)
为什么 .NET 编译器缺少我的终结器?它甚至收集了我的东西吗?

【问题讨论】:

  • “我现在对 .NET GC 存在不信任危机”——为什么?您是否有一个现实世界场景,其中在绝对需要时不收集对象?事实是,从未保证终结器会运行,也没有任何对象被收集。 GC 一直以“咨询”为基础。在您的示例中,如果您只是将所有对象创建代码移动到一个单独的方法中,GC 将完全按照您的意愿工作。 .NET 中对 GC 进行的数十或数百种优化中的一种可能是导致新行为的原因,但这并不重要。
  • 如果您真的关心,请随意查看此处描述的各种更改:devblogs.microsoft.com/dotnet/performance-improvements-in-net-5。当然,如果更改是在早期版本中进行的,则您需要查看该早期版本的记录更改。我没有看到任何需要在这里解决的实际实际问题。很明显,无论出于何种原因,.NET 都将这些对象视为仍然可访问,直到当前方法的堆栈帧消失(即方法已返回)。没有规定必须这样做。
  • 这是调试还是发布模式?
  • @PavelAnikhouski:我责怪对象被“收集”的概念造成了很多不必要的混乱。在许多情况下,GC 的行为更像是保龄球置瓶机:它将需要保留的所有内容移动到新位置,然后清除存储空间,而不知道或关心有多少对象占用了它。通过在访问所有强可达对象和彻底清除存储空间之间检查所有具有已注册终结器的对象来适应终结,因此可以使终结对象可访问,但...

标签: c# .net garbage-collection roslyn finalizer


【解决方案1】:

我同意 cmets,您应该非常小心地尝试从这些示例中理解 GC。你可以,只是要小心,你真的需要了解你在做什么。

这里是调试模式或 .NET Core 3.1/.NET 5 JIT Tier0 (QuickJIT) 的默认行为 - 在两个对象范围内都延长到方法结束(或者换句话说,运行时不关心使其尽可能短)。使用Tiered compilation disabled 运行它,你会看到。

或者将优化属性添加到Main 方法(转换为禁用方法的Tier0):

[MethodImpl(MethodImplOptions.AggressiveOptimization)]
public static void Main(string[] args)
{
   ...

您可以在 JIT 生成的代码及其对应的 GCInfo 级别观察到这种行为(IL 级别还不够)。

未优化版本 (Tier0) - 如您所见,分配结果(raxJIT_New 调用后)存储在堆栈中(rbp-8rbp-10、@ 987654328@ 和 rbp-20)。这些堆栈位置报告为Untracked,在 GCInfo 命名法中意味着“在整个方法生命周期内被视为可访问”

!U /d -gcinfo 00007ffd4a605ec0
Normal JIT generated code
Finalizers.Program.Main(System.String[])
ilAddr is 0000024B68962050 pImport is 0000020B3E9FF5C0
Begin 00007FFD4A605EC0, size 142

...
Code size: 142
Untracked: +rbp+10 +rbp-8 +rbp-10 +rbp-18 +rbp-20
...
.\Program.cs @ 12:
mov     rcx,7FFD4A6D4D90h (MT: Finalizers.Foo)
call    coreclr!JIT_New (00007ffd`aa0806f0)
mov     qword ptr [rbp-8],rax
...
.\Program.cs @ 13:
mov rcx,7FFD4A6D4D90h (MT: Finalizers.Foo)
call    coreclr!JIT_New (00007ffd`aa0806f0)
mov     qword ptr [rbp-10h],rax
...
.\Program.cs @ 14:
mov rcx,7FFD4A6D4D90h (MT: Finalizers.Foo)
call    coreclr!JIT_New (00007ffd`aa0806f0)
mov     qword ptr [rbp-18h],rax
...
.\Program.cs @ 15:
mov rcx,7FFD4A6D4D90h (MT: Finalizers.Foo)
call    coreclr!JIT_New (00007ffd`aa0806f0)
mov     qword ptr [rbp-20h],rax
...

优化 (Tier1) - 这里您看到分配的结果确实被丢弃了(rax 被逐个调用覆盖)。此外,您还可以看到,safepoints(GC 可能暂停线程的地方)没有报告任何根。如果 GCInfo 想说在给定的安全点,寄存器中有一个根,他们会这样做。

0:007> !U /d -gcinfo 00007ffd4a5e5f20
Normal JIT generated code
Finalizers.Program.Main(System.String[])
ilAddr is 00000151A7672050 pImport is 000001C2D08FF1A0
Begin 00007FFD4A5E5F20, size 1cc

...
.\Program.cs @ 12:
0000003a is a safepoint: 
mov     rcx,7FFD4A6B4240h (MT: Finalizers.Foo)
call    coreclr!JIT_New (00007ffd`aa0806f0)
00000049 is a safepoint: 
mov     rcx,rax
mov     edx,1
call    00007ffd`4a5e5b00 (Finalizers.Foo..ctor(Int32), mdToken: 0000000006000003)
.\Program.cs @ 13:
00000056 is a safepoint: 
mov rcx,7FFD4A6B4240h (MT: Finalizers.Foo)
call    coreclr!JIT_New (00007ffd`aa0806f0)
00000065 is a safepoint: 
mov     rcx,rax
mov     edx,2
call    00007ffd`4a5e5b00 (Finalizers.Foo..ctor(Int32), mdToken: 0000000006000003)

.\Program.cs @ 14:
00000072 is a safepoint: 
mov rcx,7FFD4A6B4240h (MT: Finalizers.Foo)
call    coreclr!JIT_New (00007ffd`aa0806f0)
00000081 is a safepoint: 
mov     rcx,rax
mov     edx,3
call    00007ffd`4a5e5b00 (Finalizers.Foo..ctor(Int32), mdToken: 0000000006000003)

.\Program.cs @ 15:
0000008e is a safepoint: 
mov rcx,7FFD4A6B4240h (MT: Finalizers.Foo)
call    coreclr!JIT_New (00007ffd`aa0806f0)
0000009d is a safepoint: 
mov     rcx,rax
mov     edx,4
call    00007ffd`4a5e5b00 (Finalizers.Foo..ctor(Int32), mdToken: 0000000006000003)
...

【讨论】:

  • "IL 级别不足以分析 GCInfo" -- 是的。但是如果当地人根本不存在,“当地人的活力”就无关紧要了。您的帖子未能解释为什么即使在对象引用被立即丢弃的优化情况下,对象仍然没有被收集和完成。这实际上是被问到的问题(基本上,虽然不是这么多的话)。
  • 啊,我明白了。但现在有一个不同的问题:为什么,鉴于 C# 编译器完全删除了局部变量,JIT 编译器显然重新引入它们并存储方法的返回值?说 IL 没有讲述整个故事是一回事 - 这当然是真的 - 但完全发现(并说)JIT 编译器实际上添加新代码是另一回事一开始甚至不在 IL 中,就像您在上面显示的第 0 层代码中的情况一样。
  • 嗯,这确实是一个完全不同的故事——总的来说,它就像一个旧的抽象(基于堆栈的 IL 代码)与实现(JITted 机器代码)。这里抽象泄漏,再次¯_(ツ)_/¯
猜你喜欢
  • 2012-01-31
  • 1970-01-01
  • 2012-11-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-12-08
  • 2015-10-27
  • 2015-03-05
相关资源
最近更新 更多