为什么在应该发生垃圾回收的时候却没有发生?

为什么在应该发生垃圾回收的时候却没有发生?


此文翻译(有改动)自 StackOverflow 的一个问题和采纳答案,原文链接请点击这里

问题

我做了一个 64 位 的 WPF 测试程序。我把程序运行起来并打开任务管理器,同时观察系统内存使用情况。可以看到程序用了 2 GB 的内存,并且还有 6 GB 可用内存。

在我的程序中,点击一个添加按钮会添加 1 GB 的字节数组到一个列表中,并可以观察到系统内存使用量增加了 1 GB。我总共点击了 6 次添加按钮以将这 6 GB 可用内存全部填满。

接着我点击了 6 次移除按钮将之前添加的数组从列表中移除。这些被移除的字节数组应该没有被任何对象引用了。

当我移除后,并没有看见内存使用量下降。但我可以理解这个,因为我知道 GC 并不是立即执行的。我明白 GC 会在将来需要的时候发生的。

因此现在内存看起来虽然是满的,但 GC 在需要的时候会发生的。我再次点击添加按钮,却发生了 OutOfMemoryException 。为什么 GC 没有发生呢?如果现在还不是回收发生的时机,那什么时候才是呢?

为了验证,我有一个按钮用于强制 GC 。当我按下这个按钮时,很快就获得了 6 GB 可用内存。这不就证明了我的 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();
}

采纳答案

为了验证,我有一个按钮用于强制 GC 。当我按下这个按钮时,很快就获得了 6 GB 可用内存。这不就证明了我的 6 个数组没有被任何对象引用并且可以被 GC 回收吗?

把你的问题换一种问法:GC 在何时自动回收“垃圾”内存?

在我的脑海里首先出现的是:

  • 大多数时候,当第 0 代满了或者没有足够的空余空间分配某个对象时会发生垃圾回收
  • 某些时候,当分配一大块内存时可能引发 OutOfMemoryException 异常,一次完整 GC 将会被触发并尝试回收可用内存。如果在回收后仍然没有足够的连续内存,那么 OOM 异常将会被抛出。

当开始一次垃圾收集时,GC 将会决定哪些代需要被回收(0,0+1,或者所有)。每一代都有一个由 GC 决定的大小(在程序运行时可能会被改变)。如果只有第 0 代超过了容量,那么它就是唯一将被垃圾回收的代。如果从第 0 代垃圾回收中存活下来的对象造成第 1 代超过了容量,那么第 1 代也将被回收,并且存活下来的对象会提升到第 2 代(这是微软实现中最高的代)。如果第 2 代也超过了容量,那么垃圾也会被回收,但是对象不会再提升至更高的代,因为不存在更高的代了。

因此,这里隐含着很重要的信息,按照 GC 大多数时候运行的方式,第 2 代只会在第 0 代和第 1 代均满了的情况下才会被回收。同时,你还应该知道大于 85000 字节的对象不是存放在标记着第 0,1,2 代的普通 GC 堆里的。它实际上是存放在一个叫做大型对象堆(Large Object HeapLOH)里的。LOH 里的内存只会在发生完整回收时才会被释放(即第 2 代被回收时)。绝不会在第 0 代或者第 1 代回收发生时发生。

为什么 GC 没有发生呢?如果现在还不是回收发生的时机,那什么时候才是呢?

现在 GC 为什么没有自动开始应该很明显了。你只在 LOH(记住像你这种使用 int 类型的方式是在栈上分配的内存并且不需要被回收)上创建了对象。你从来没有填充过第 0 代,所以 GC 从来没有发生。

同时,你在 64 位模式下运行,这意味着你也不会触发我上面列举的其他情况,即当分配某个特定对象时整个应用程序发生内存不足。64 位应用程序拥有 8 TB 的虚拟地址空间,因此你是不大可能满足这种条件的。很大可能是在这种情况发生前已耗尽物理内存和页文件空间。

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

我读过很多文章都说不应该调用 GC.Collect(),但是如果 GC 在这种情形下不发生,我还能做什么呢?

如果你的代码必须要这样写,那么请调用 GC.Collect()。但是,最好不要在测试之外写这种代码。

最后,我没有对 CLR 中的自动垃圾回收下任何结论。我建议通过 MSDN 上的帖子来了解它(它真的非常有趣),或者像其他人已经提及的,通过 Jeffery Richter 写的很棒的书,CLR via C#,第 21 章。

如果你不相信我,当你移除一个数组后,用一个方法创建一组随机字符串或者对象(我建议在这个测试中使用原生类型的数组),当创建的对象达到一定数量时,将会发生一次完整 GC 将你分配在 LOH 中的内存释放。

posted @ 2014-08-12 20:49  platobeing  阅读(1020)  评论(4编辑  收藏  举报