25

我制作了一个 64 位 WPF 测试应用程序。在我的应用程序运行并打开任务管理器的情况下,我观​​察我的系统内存使用情况。我看到我正在使用 2GB,并且我有 6GB 可用。

在我的应用程序中,我单击添加按钮以将新的 1GB 字节数组添加到列表中。我看到我的系统内存使用量增加了 1GB。我一共点击了 6 次添加,填满了我开始时可用的 6GB 内存。

我单击“删除”按钮 6 次以从列表中删除每个数组。删除的字节数组不应被我控件中的任何其他对象引用。

当我删除时,我没有看到我的记忆下降。但这对我来说没问题,因为我知道 GC 是非确定性的。我认为 GC 将根据需要收集。

因此,现在内存看起来已满,但希望 GC 在需要时收集,我再次添加。我的电脑开始滑入和滑出磁盘抖动昏迷。为什么GC没有收集?如果那不是做的时候,那是什么时候?

作为健全性检查,我有一个强制 GC 的按钮。当我推动它时,我很快就恢复了 6GB。这不是证明我的 6 个数组没有被引用,并且如果 GC 知道/想要的话,可以收集吗?

我读过很多说我不应该调用 GC.Collect() 但如果 GC 在这种情况下不收集,我还能做什么?

    private ObservableCollection<byte[]> memoryChunks = new ObservableCollection<byte[]>();
    public ObservableCollection<byte[]> MemoryChunks
    {
        get { return this.memoryChunks; }
    }

    private void AddButton_Click(object sender, RoutedEventArgs e)
    {
        // Create a 1 gig chunk of memory and add it to the collection.
        // It should not be garbage collected as long as it's in the collection.

        try
        {
            byte[] chunk = new byte[1024*1024*1024];

            // Looks like I need to populate memory otherwise it doesn't show up in task manager
            for (int i = 0; i < chunk.Length; i++)
            {
                chunk[i] = 100;
            }

            this.memoryChunks.Add(chunk);                
        }
        catch (Exception ex)
        {
            MessageBox.Show(string.Format("Could not create another chunk: {0}{1}", Environment.NewLine, ex.ToString()));
        }
    }

    private void RemoveButton_Click(object sender, RoutedEventArgs e)
    {
        // By removing the chunk from the collection, 
        // I except no object has a reference to it, 
        // so it should be garbage collectable.

        if (memoryChunks.Count > 0)
        {
            memoryChunks.RemoveAt(0);
        }
    }

    private void GCButton_Click(object sender, RoutedEventArgs e)
    {
        GC.Collect();
        GC.WaitForPendingFinalizers();
    }
4

3 回答 3

18

作为健全性检查,我有一个强制 GC 的按钮。当我推动它时,我很快就恢复了 6GB。这不是证明我的 6 个数组没有被引用,并且如果 GC 知道/想要的话,可以收集吗?

你最好问问When does the GC automatically collect "garbage" memory?。在我的头顶上:

  • 最常见的是,当第 0 代已满或对象分配不适合可用空间时。1
  • 通常情况下,当分配一块内存会导致GC 时OutOfMemoryException,会触发一次完整的 GC 以首先尝试回收可用内存。如果收集后没有足够的连续内存可用,则会抛出 OOM 异常。

开始垃圾收集时,GC 确定需要收集哪些代(0、0+1 或全部)。每一代都有一个由 GC 确定的大小(它可以随着应用程序的运行而改变)。如果只有第 0 代会超出其预算,那是唯一将收集垃圾的代。如果第 0 代幸存的对象会导致第 1 代超出其预算,那么第 1 代也将被收集并将其幸存对象提升到第 2 代(这是微软实现中的最高代)。如果也超出了第 2 代的预算,则会收集垃圾,但对象无法提升到更高的一代,因为不存在。

所以,这里有重要的信息,在最常见的 GC 启动方式中,只有在第 0 代和第 1 代都已满时才会收集第 2 代。此外,您需要知道超过 85,000 字节的对象不会存储在第 0、1 和 2 代的普通 GC 堆中。它实际上存储在所谓的大对象堆 (LOH) 中。LOH 中的内存仅在 FULL 收集期间(即第 2 代收集时)被释放;永远不会收集第 0 代或第 1 代。

为什么GC没有收集?如果那不是做的时候,那是什么时候?

现在应该很明显为什么 GC 从未自动发生。您只是在 LOH 上创建对象(请记住,int类型,您使用它们的方式,是在堆栈上分配的,不必收集)。您永远不会填满第 0 代,因此永远不会发生 GC。1

您还在 64 位模式下运行它,这意味着您不太可能遇到我上面列出的另一种情况,即当整个应用程序中没有足够的内存来分配某个对象时会发生集合。64 位应用程序的虚拟地址空间限制为 8TB,因此在遇到这种情况之前还需要一段时间。在此之前,您更有可能会用完物理内存和页面文件空间。

由于没有发生 GC,Windows 开始从页面文件中的可用空间为您的应用程序分配内存。

我读过很多说我不应该调用 GC.Collect() 但如果 GC 在这种情况下不收集,我还能做什么?

GC.Collect()如果您需要编写这种代码,请致电。更好的是,不要在测试之外编写这种代码。

总之,我没有对 CLR 中的自动垃圾收集主题做出公正的评价。我建议通过 msdn 博客文章阅读它(实际上非​​常有趣),或者正如已经提到的,Jeffery Richter 的优秀书籍,CLR Via C#,第 21 章。


1我假设您了解 GC 的 .NET 实现是一个分垃圾收集器。用最简单的话来说,这意味着新创建的对象位于编号较低的一代,即第 0 代。当垃圾收集运行时,发现某个代中的对象具有 GC 根(不是“垃圾”),它会被提升到下一代上来。这是一种性能改进,因为 GC 可能需要很长时间并损害性能。这个想法是,较高代的对象通常具有更长的寿命,并且在应用程序中的存在时间更长,因此它不需要像较低代那样检查该代是否存在垃圾。您可以在这篇维基百科文章中阅读更多内容. 您会注意到它也称为临时GC。

2如果你不相信我,在你删除一个块之后,有一个函数可以创建一大堆随机字符串或对象(我建议不要在这个测试中使用原语数组),你会看到你达到一定的空间量,将发生完整的 GC,释放您在 LOH 中分配的内存。

于 2012-04-04T19:12:07.170 回答
5

这是在 LOH(大对象堆)上进行的。这些仅在执行第 2 代收集时被清除。正如 Hans 在他的评论中所说,如果这是真实代码,您将需要更多 RAM。

对于咯咯笑,你可以打电话GC.GetGeneration(chunk)看看它会回来2

请参阅 Jeffrey Richter 第 3 版通过 C# 编写的 CLR(第 588 页)。

于 2012-04-04T17:59:04.023 回答
2

对于需要分配大量数据而不是释放它的真实代码,请考虑在处理完巨大对象后手动调用 GC( GC.Collect 最佳实践问题)。

您还可以通过将分配分配到更小的(小于 80K)块中来将对象从 LOH 转移到普通堆。

于 2012-04-04T18:02:02.507 回答