在C#中明确释放内存

37

我创建了一个使用了150MB内存(私有字节)的C#应用程序,主要是因为有一个很大的字典:

Dictionary<string, int> Txns = new Dictionary<string, int>();

我想知道如何释放这段内存。我已经尝试过这个:

Txns = null;
GC.Collect();

但是它似乎并没有在我的私有字节中产生太大的影响——它们从155mb降到145mb。

有什么线索吗?

谢谢。

-编辑-

好的,我用这段代码运行得更顺利了(它将私有字节降至50mb),但为什么呢?

Txns.Clear(); // <- makes all the difference
Txns = null;
GC.Collect();

对于所有说“不要使用GC.collect”的人,我理解(我并不想争论这个问题,除了说你可以看到我的C背景),但这并没有真正回答我的问题: 为什么垃圾收集器只有在我清空事务列表后才会释放内存?既然字典已经被取消引用,它不应该自动释放内存吗?


GC.Collect()不会强制回收所有可回收的内存,因为垃圾回收是一个多步骤的过程。我可以补充一点的是,你通常甚至不需要知道关于这个过程的细节... - peSHIr
有必要将Txns设置为null吗?这样做会有什么区别吗?或者只是清除内容就足够了(以释放内存)? - akjoshi
有多个级别的内存清理。GC 是一个复杂的部分...如果你想的话可以查看源代码文件。无论如何,它可以运行多个级别的搜索可清除的内存块。字典中的条目仍然被字典引用,因此它们是有引用的。GC 需要检查所有引用是否都被引用,这需要更长时间...通常浅层搜索就足够了。如果足够了,就不要再消耗更多用户时间的假设。 - Adas Lesniak
8个回答

19

私有字节反映了进程的内存使用情况。当对象被收集时,关联的内存段可能会或可能不会被释放到操作系统中。CLR在操作系统级别管理内存,由于分配和释放内存不是免费的,因此没有理由立即释放每个内存块,因为应用程序很可能稍后会请求更多内存。


3
听起来都是合理的,但如果我调用Txns.Clear()确实会释放内存给操作系统。而且我真的想对操作系统的内存好一点,因为不只有我在使用这个服务器。 - Chris
2
我想我问的是,为什么txns.Clear()会将内存归还给操作系统,而取消引用整个字典却不会? - Chris
4
重点是在托管应用程序中,您与操作系统内存直接打交道的步骤被CLR代替了。您可能能够构建情景并观察到直接效果,就像您所描述的那样,但不能保证这样做会起作用,因为它取决于CLR的实现细节。 - Brian Rasmussen

12
如果您调用GC.Collect(),它会开始执行其工作,但会立即返回而不是阻塞,因此您看不到任何效果。如果您只调用GC.WaitForPendingFinalizers(),它将阻止您的应用程序,直到GC.Collect()完成其工作。

1
哎呀!甚至无法想象启动一个线程,定期(而且相当频繁:每秒尝试两次!)调用GC.Collect,然后等待所有挂起的终结器的级别有多糟糕。你是完全不关心应用程序的可扩展性吗?那只是试图糊弄一个糟糕的症状,糟糕透了!我猜我只能在这里-1了... - peSHIr
11
@peSHIr: 同意,不过公平地说,它确实明确回答了问题。是否是个好主意是另一个问题。 - andy
@andy:勉强同意。那我是不是应该将问题减1呢?这似乎过于严厉和无用了。哦,算了吧。 - peSHIr
我不确定那完全正确。垃圾回收将(可能?)唤醒终结器线程,线程接着会根据需求运行终结器。因此等待终结器并不是真正等待垃圾回收结束(因为GC发生在另一个线程上)。 - Brian Rasmussen
嗨,感谢您的回答,但似乎没有什么区别,因为我尝试了这个方法,但是私有字节并没有减少: //Txns.Clear(); Txns = null; for (int x = 0; x < 20; x++) { GC.Collect(); GC.WaitForPendingFinalizers(); } 现在请大家注意,这只是概念证明代码!它不会进入生产环境。但是我唯一能让它释放内存的方法是调用字典的Clear方法,这实在是不应该的,因为我已经取消引用整个字典了。 - Chris
显示剩余2条评论

6

很可能您在其他地方有一个隐藏的字典引用。因此,字典不会被收集,但如果您使用Clear()清除它,则其内容将被收集。

正如其他人已经指出的那样,强制GC不是推荐的做法。这可能导致内存被推入不经常收集的更高“代”,从而浪费比长期获得更多的内存。


没有,没有对字典的隐藏引用。 - Chris
1
你对于将对象推入更高的代中的评论,能否详细说明一下?这听起来像是一个需要注意的问题。 - Chris
等一下,我认为如果你调用 gc.collect 而没有参数,它会简单地收集 所有 代。这不会排除将东西推入更高代的问题吗?谢谢。 - Chris
GC.Collect()会收集所有的代。这里有两个问题。1)只调用一次:仍然被引用的内存被推迟到正常收集,2)定期调用它会浪费更多的CPU,因为垃圾回收器必须每次处理所有的代。所以无论你如何使用GC.Collect(),你都会失去一些东西。当然,在那些特殊情况下,这些影响对你不重要。 - David Schmitt
在这个阶段运行内存分析器将证明没有隐藏的引用存在。 - crokusek

4

我不确定Dictionary是否有Dispose(),但肯定有Clear()。在设置任何指向null的引用之前,请调用其中任意一个。

然后,只需让垃圾收集器自行处理即可。几乎从来不明智地手动调用GC.Collect(),因为它可能无法达到您想要/需要/期望的效果,并且会影响性能。静态代码分析(=FxCop)不是毫无原因地警告您Reliability rule CA2001吗?除非您真的知道自己在做什么,否则请勿这样做。即使您确实知道如何操作,也不要这样做。;-)

你确定字典那么大吗?难道它只占用了10 MB的内存,其余部分都被应用程序占用了吗?这个问题可能会帮助你:你使用过分析器查看实际消耗内存的位置吗?


这个字典确实很大。当我运行带有.Clear更改的代码时,内存使用量从160mb降至50mb。因此,我认为这个字典大小为110mb。 - Chris

1
好的,我有一个理论... 字典是KeyValuePair的集合,KeyValuePair又是引用类型。
你的字典包含这些keyValuePairs。当你说:
Txns = null

它将 'Txns' 引用从那些 KeyValuePair 集合中释放出来。但是,那些 KeyValuePair 仍然引用着实际的 150 Mb 内存,并且它们处于作用域中,因此无法进行垃圾回收。

但是当你使用以下方法时:

Txns.Clear();
Txns = null;
GC.Collect();

在这里,clear方法还释放了150Mb的数据,这些数据来自它们各自的KeyValuePair对象引用。因此,这些对象已经可以被垃圾回收了。
这只是我在这里做出的一个猜测。欢迎评论 :)

KeyValuePairs不在范围内。要使它们在范围内,您需要设置额外的变量或数组来保存KeyValuePairs的副本,类似于这样:var arrayOfReferencesToKeyValuePairs = Txns.ToArray(); 在这种情况下,一旦Txns为null,就无法访问任何KeyValuePairs,垃圾收集器也会看到这一点。 - jnm2

1

编辑:

公平地说,将引用设置为null并不会释放内存,它只是将其容器分配给另一个地址,在这种情况下是null。根据MSDN的说法,调用Clear()会执行以下操作:“Count属性设置为0,并且从集合元素中对其他对象的引用也被释放。容量保持不变。”

...

你不应该手动调用垃圾回收。如果你正在使用没有本地资源的托管对象,请相信垃圾回收器在你之后会清理。

除了你的字典大小,你不需要担心内存问题,内存不是你的问题,而是垃圾回收器的问题。

调用Clear()将删除其中包含的任何对象的引用,但容量保持不变。

技术上讲,收集内存是一项昂贵而耗时的操作。原因是,GC不仅处理堆内存并清理你的堆,它还有点像碎片整理。它试图将内存移动到连续的块中,以加快某些代码请求大内存块的速度。

附:你的字典大小有155MB?


它很大,我正在对一百万笔交易进行对账。 - Chris
天啊,这是大量的交易。我假设这只是一次性的事情,还是你需要经常这样做? - Chris
其实我每天都会犯错,大约有半打次。 - Chris
好的,如果是这样的话,有必要用代码来完成吗?为什么不直接使用数据库来处理这些事务呢?有什么原因不能这样做吗? - Chris

1

你需要回收内存吗?内存是可用的,只是没有被回收。你不应该清除字典,而是要持有一个弱引用,让运行时来完成它的工作。

如果你想深入了解正在发生的事情,请查看.NET Memory Profiler。这将让你看到你的对象正在发生什么,它属于哪一代,哪些内存被使用等等。 :)


我并不需要立即回收内存,但在我们的情况下,如果可能的话,我更愿意尽快将其归还给操作系统。我想这个问题变得更多是一个理论上的“如何做到这一点?”而不是一个“我真的需要这样做”的事情... - Chris

0

Windows有两个内存可用性事件。我期望CLR会对此做出响应。如果有足够的内存可用,聪明的做法是不运行垃圾回收器。因此,为了确保您确实观察到了糟糕的CLR行为,请使用另一个使用大量内存堆的虚拟应用程序重复此测试。


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