.NetFramework 4.8 和 .Net 5 之间的垃圾收集行为差异

Garbage collection behaviour difference between .NetFramework 4.8 and .Net 5

为了在已经经常发生内存泄漏的地方检测潜在的内存泄漏,我使用了如下所示构建的测试。主要思想是拥有一个实例,不再引用它并让垃圾收集器收集它。我不想关注这是否是一项好技术(在我的特定情况下它做得很好)但我想关注以下问题:

下面的代码在 .NetFramework 4.8 上运行良好,但在 .Net 5 上运行不正常。为什么?

[Test]
public void ConceptualMemoryLeakTest()
{
    WeakReference weakReference;
    {
        object myObject = new object();
        weakReference = new WeakReference(myObject);
        myObject = null;
        Assert.Null(myObject);
    }
    Assert.True(weakReference.IsAlive); // instance not collected by GC
    GC.Collect();
    GC.WaitForPendingFinalizers();
    GC.WaitForFullGCComplete();
    GC.Collect();
    Assert.False(weakReference.IsAlive); // instance collected by GC
}

你可以看到主要思想是使用WeakReference并使用IsAlive来确定实例是否被GC删除。新 CLR(源自 dotnet core 的规则)中的规则是如何变化的?我知道这里所做的并不依赖于指定的内容。相反,我只是利用我在 NetFramework 4.8 中看到的 CLR 行为。

您是否知道如何再次获得同样适用于 .Net 5 的类似内容?

原因很可能是 tiered compilation。简而言之,分层编译将(对于某些条件下的某些方法)首先编译方法的粗略、低优化版本,然后在必要时准备更好的优化版本。这在 .NET 5(和 .NET Core 3+)中默认启用,但在 .NET 4.8 中不可用。

在你的情况下,结果是你的方法是用提到的“快速”编译编译的,并且没有优化到足以让你的代码按预期工作(即 myObject 变量的生命周期延长到结束方法)。即使您在启用优化且未附加任何调试器的情况下在发布模式下编译也是如此。

您可以 disable 分层编译,方法是添加:

<TieredCompilation>false</TieredCompilation>

对于您的 .NET 5 项目的 csproj 文件中的一些 <PropertyGroup>,您将观察到与 .NET 4.8 情况下相同的行为。

另一种选择(除了将变量移动到另一个方法并从中返回 WeakReference 之外)是使用:

[MethodImpl(MethodImplOptions.AggressiveOptimization)]

ConceptualMemoryLeakTest 方法的属性。

实际上,根据评论和答案中提供的提示,我意识到将实例移至单独的方法并防止内联它有效:

[MethodImpl(MethodImplOptions.NoInlining)]
private WeakReference CreateWeakReference()
{
    object myObject = new object();
    return new WeakReference(myObject);
}

[Test]
public void ConceptualMemoryLeakTest()
{
    WeakReference weakReference = CreateWeakReference();
    Assert.True(weakReference.IsAlive);
    GC.Collect();
    GC.WaitForPendingFinalizers();
    GC.WaitForFullGCComplete();
    GC.Collect();
    Assert.False(weakReference.IsAlive);
}

这不需要

<TieredCompilation>false</TieredCompilation>