垃圾收集即使在需要时也没有发生

26
我做了一个64位的WPF测试应用程序。在我的应用程序运行并打开任务管理器时,我观察系统内存使用情况。我看到我正在使用2GB,并且有6GB可用。
在我的应用程序中,我点击一个添加按钮,将一个新的1GB字节数组添加到列表中。我看到我的系统内存使用量增加了1GB。我总共点击了6次添加按钮,填满了我开始时可用的6GB内存。
我点击一个删除按钮6次,从列表中删除每个数组。被删除的字节数组不应该被我控制的任何其他对象引用。
当我删除时,我没有看到内存减少。 但这对我来说没关系,因为我知道垃圾回收是非确定性的之类的。 我认为垃圾回收会根据需要进行收集。
所以现在内存看起来已经满了,但是期望垃圾回收在需要时进行收集,我再次进行添加操作。 我的电脑开始陷入磁盘抖动的昏迷状态。 为什么垃圾回收没有进行? 如果那不是时候,那什么时候呢?
作为一个合理性检查,我有一个按钮来强制进行垃圾回收。当我按下按钮时,我很快就能够回收6GB的内存。这难道不证明我的6个数组没有被引用,并且如果垃圾回收知道/愿意的话,它们本可以被回收吗?
我读了很多关于不应该调用GC.Collect()的文章,但是在这种情况下,如果垃圾回收不进行回收,我还能做什么呢?
    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 expect 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();
    }

16
大型对象堆的行为在CLR的任何背景介绍书中都有详细描述。请去图书馆借阅Richter的任何作品。如果在实际的代码中需要分配许多1GB数组,则需要更多的RAM。任何机器都可能会被人工代码拖垮。 - Hans Passant
2
你也可以从这个链接开始:http://msdn.microsoft.com/en-us/magazine/cc534993.aspx - Matt Burland
3个回答

19
作为一种合理性检查,我有一个强制进行GC的按钮。当我按下它时,我很快就能回收6GB内存。这是否证明了我的6个数组没有被引用,并且如果GC知道/想要回收,它们本来可以被收集起来?
你最好询问“垃圾回收器何时会自动回收‘废弃’内存?”。我能想到的是:
- 最常见的情况是当第0代满或对象分配无法适应可用空闲空间时。 - 较常见的情况是当分配一块内存会导致OutOfMemoryException时,将触发完整垃圾回收以首先尝试回收可用内存。如果在回收后仍无足够连续的内存可用,则将抛出OOM异常。
启动垃圾回收时,GC确定需要收集哪些代(0、0+1或全部)。每个代的大小由GC确定(它可以随应用程序运行而改变)。如果只有第0代将超出预算,那么只有该代的垃圾将被收集。如果生存于第0代的对象将导致第1代超出其预算,则第1代也将被收集,并将其生存的对象提升到第2代(这是Microsoft实现中最高的代)。如果第2代的预算也超出了,垃圾将被收集,但对象无法升级到更高的代,因为不存在更高的代。
所以,在常见情况下,GC将仅在第0代和第1代都满时才收集第2代,这是最重要的信息。此外,您需要知道大于85,000字节的对象不存储在正常的GC堆中,而是存储在称为“大对象堆”(LOH)的特殊堆中。 LOH中的内存仅在进行完整收集(即收集第2代时)期间释放,而不是仅在收集第0代或第1代时释放。

为什么垃圾回收器没有进行回收?如果现在不是回收的时机,那么什么时候是呢?

现在应该很明显为什么自动垃圾回收器没有起作用。您仅仅在LOH上创建对象(请记住,像您使用的这种int类型都是在堆栈上分配的,不需要被回收)。您永远不会填满第0代,因此垃圾回收器从未触发。1

您还在64位模式下运行它,这意味着您很少会遇到我上面列出的另一种情况:当整个应用程序中没有足够的内存来分配某个对象时,将会发生一个回收事件。64位应用程序具有8TB的虚拟地址空间限制,因此在此之前需要很长时间。更可能的是,在发生这种情况之前,您会耗尽物理内存和页面文件空间。

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

我读了很多文章,说我不应该调用GC.Collect(),但如果在这种情况下没有垃圾回收,我还能做什么呢?

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

总之,我没有完全涵盖CLR中自动垃圾回收的主题。我建议通过msdn博客文章或已经提到的Jeffery Richter的出色书籍,CLR Via C#,第21章来学习它(实际上非常有趣)。


1 我假设你已经了解.NET实现的垃圾回收器是一种分代垃圾回收器。简单地说,这意味着新创建的对象属于较低的一代,例如第0代。当运行垃圾回收时,如果发现某个代中的对象有一个GC根(即不是"垃圾"),它将被提升到上一代。这是一种性能优化措施,因为垃圾回收可能需要很长时间并影响性能。想法是较高代中的对象通常具有更长的生命周期,并且会在应用程序中存在更长时间,因此不需要像较低代那样频繁检查该代的垃圾。您可以在这篇维基百科文章中阅读更多相关内容。你会发现它也被称为短暂垃圾回收器。

2 如果你不相信我,在删除其中一个块之后,创建一个函数来创建大量随机字符串或对象(我建议避免测试基元数组),在达到一定空间后,你会发现进行了全局垃圾回收,释放了你在大对象堆中分配的内存。


5

这是位于大对象堆(Large Object Heap)中。只有在进行第二代垃圾回收时才会清除它们。正如Hans在评论中所说,如果这是真实代码,你需要更多的RAM。

为了好玩,您可以调用 GC.GetGeneration(chunk) 查看它将返回 2

请参阅Jeffrey Richter的CLR via C#,第3版(第588页)。


2

对于需要分配大量内存并释放的实际代码,请考虑在完成大型对象后手动调用GC(GC.Collect最佳实践问题)。

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接