此文翻译(有改动)自 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
在何时自动回收“垃圾”内存?
在我的脑海里首先出现的是:
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 Heap
,LOH
)里的。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
中的内存释放。