【问题标题】:Why the garbage collector does not garbage my instances? [duplicate]为什么垃圾收集器不会垃圾我的实例? [复制]
【发布时间】:2020-03-06 13:55:55
【问题描述】:

我正在编写一些测试以更好地了解 .NET 垃圾收集器的工作原理,以便构建一个没有内存泄漏的框架。但在我的第一个非常简单的测试中,我遇到了意外的行为。

这是我对 GC 的了解:

  • 它会定期(在可能的情况下)清理东西
  • 它会清除不再引用的实例

这是我为验证我的知识而写的小课:

public class People
{
    private People _child;
    private WeakReference<People> _parent = new WeakReference<People>(null);

    public void AddChild(People child)
    {
        _child = child;
        _child._parent.SetTarget(this);
    }
}

基本上,父级可以引用其子级。根据我上面的知识,我希望当父母“死”时,它的孩子也会“死”。

这里的小技巧是使用WeakReference,这样子级就可以访问其父级,但不会创建可能导致内存泄漏的循环引用(这是我试图找出的一点:是两个实例仅相互引用垃圾收集?或者换句话说:在这种情况下我是否必须使用WeakReference?我的猜测是,如果它们直接相互引用,它们将不会被垃圾收集,但我实际上从未检查过它)。

这是我用xUnit写的小测试:

public class GCTests
{
    [Fact]
    public void TestGC()
    {
        var parent = new People();

        var weakParent = new WeakReference(parent);
        var child = new WeakReference(new People());

        parent.AddChild(child.Target as People);

        // Until now, there is a reference to the parent so the GC is not supposed to collect anything

        parent = null;

        // But now, no-one is referencing the parent so I expect the GC to collect it and removed it from memory

        // Forces the GC to collect unreferenced instances
        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();

        Assert.False(weakParent.IsAlive);
        Assert.False(child.IsAlive);
    }
}

Assert.False(weakParent.IsAlive) 上的测试失败,这意味着有人仍然有对实际父级的引用。

我还尝试使用Thread.Sleep(10000); 给 GC 时间来收集东西,但它仍然无法执行该断言。

所以我的问题是:为什么我的实例没有被垃圾回收

  • 我的哪一个断言是错误的?
  • 我在垃圾回收过程中或者WeakReference的使用过程中误会了什么?

有关信息,我正在使用的 xUnit 测试项目的目标是 .NET Core 3,但我希望它不会改变 GC 过程中的任何内容。

【问题讨论】:

  • 你检查过这个thread吗?
  • 因为你无法控制它。调用 Collect 并不能确保它会运行。一般来说,它在 Windows 上运行。但是你不能假设它会运行。您只能确保它仅在内存快用完时运行。 docs.microsoft.com/dotnet/api/system.gc.collect。当它运行时,您会在应用程序中看到延迟。因此,Collect 已针对某些因素进行了优化。
  • 在我的机器上,带有该代码的控制台应用程序在 .NET Framework 4.7.2 中“工作”,但在 .NET Core 3.0 中失败。这说明了您不能(也不应该)依赖GC.Collect 收集它可以收集的一切 的广泛原则。不过,一般建议在调用GC.Collect 时将其移至调用您的逻辑的函数中。它不会让它 100% 工作 - 但它更有可能做你期望它做的事情。 WeakReference weakParent, child; YourLogicHere(out weakParent, out child); GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect();
  • 你的很多断言都是错误的,但首先是你假设循环引用首先是一个问题。您也可能在附加调试器的情况下运行测试,或者在“调试”版本上运行测试,这两种情况都可能导致 GC 不那么激进。底线是,您无法保证会收集到无法访问的对象。您可以向 GC 提出建议,但它是有意设计的,它对何时收集东西有自己的规则。唯一真正硬性规定的规则是任何可到达的对象不会被收集。

标签: c# garbage-collection weak-references


【解决方案1】:

在这个问题的核心,您似乎想知道 GC 是否可以收集唯一引用是循环的对象。我想我可以通过创建两个仅相互引用的非常大的对象来测试这一点,然后让它们超出范围并创建第三个大对象,看看我是否可以引入一些内存压力来尝试让 GC免费的东西了。如果 GC 可以释放相互引用的对象,那么程序消耗的内存应该会在某个时候减少。如果 GC 不能,那么内存使用只会增加:

using System.Threading;

namespace ConsoleApp
{
    class Program
    {

        static void Main()
        {
            Thread.Sleep(2000);

            SetupBigThings();

            Thread.Sleep(2000);

            string big = new string('a', 1000000000);


            while (true)
            {
                Thread.Sleep(2000);
            }
        }

        static void SetupBigThings()
        {
            Thread.Sleep(1000);
            BigThing x = new BigThing('x');
            Thread.Sleep(1000);
            BigThing y = new BigThing('y') { OtherBigThing = x };
            x.OtherBigThing = y;
            Thread.Sleep(1000);

        }

    }

    class BigThing
    {
        public BigThing OtherBigThing { get; set; }

        private string big;

        public BigThing(char c)
        {
            big = new string(c, 750000000);
        }
    }
}

查看代码,我们应该会在 3 秒时看到内存峰值,然后在 4 秒时再次出现峰值。5 秒后,大对象超出范围,并且可能在 7 秒左右时会在下一个大对象时被 GC对象已创建

这就是图表所显示的内容:

因此,我假设 GC 确实可以收集彼此唯一引用的对象。简单地说“哪些对象有 0 个引用?”可能不会那么天真,而是会追逐引用路径,并且任何仅引用另一个已经考虑进行 GC 的节点的对象也被认为是 GC'able。虽然我不是 GC 内部工作的专家

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-01-12
    • 1970-01-01
    • 2010-12-19
    相关资源
    最近更新 更多