关于 c#:为什么垃圾收集器不垃圾我的实例?

Why the garbage collector does not garbage my instances?

本问题已经有最佳答案,请猛点这里访问。

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

这是我对 GC 的了解:

  • 它会定期清理东西(以及何时清理)
  • 它清理不再引用的实例

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

1
2
3
4
5
6
7
8
9
10
11
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 编写的小测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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 过程中的任何内容。


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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对象已创建

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

enter