C#中的垃圾收集没有进行.为什么?

Xeg*_*ara 24 .net c# garbage-collection memory-management

我尝试过一个简单的实验来验证垃圾收集器的功能.引用3.9关于.NET中自动内存管理的自动内存管理(MSDN).对我来说,它听起来像是C++中的共享指针.如果对象的引用计数器变为零,则它将被垃圾收集器解除分配.

所以我尝试在主窗体中创建一个函数.该函数在我的主窗体的Shown事件函数内调用,该函数在构造函数之后执行.这是实验代码.

    public void experiment()
    {
        int[] a = new int[100000];
        int[] b = new int[100000];
        int[] c = new int[100000];
        int[] d = new int[100000];

        a = null;
        b = null;
        c = null;
        d = null;
    }
Run Code Online (Sandbox Code Playgroud)

以下是结果:

在内存分配之前

http://i.stack.imgur.com/ZhUKc.png

内存分配后

http://i.stack.imgur.com/NyXJ6.png

在离开功能范围之前

http://i.stack.imgur.com/MnTrm.png

离开功能范围后

http://i.stack.imgur.com/MiA6T.png

为什么垃圾收集器即使在设置为null之后也不释放由数组a,b,c,d分配的内存?

DrK*_*och 66

.NET垃圾收集器是一个高度优化,复杂的软件.它经过优化,可以让您的程序尽可能快地运行,并且不会使用太多的内存.

因为释放内存的过程需要一些时间,所以垃圾收集器经常等待运行它,直到程序使用大量内存.然后它会立即完成所有工作,这会导致程序在相对较长的时间后出现小延迟(而不是之前的许多较小的延迟,这会降低程序的速度).

所有这些意味着,垃圾收集器运行的时间是不可预测的.

您可以多次调用您的测试(在循环中使用一些Sleep())并观察缓慢累积的内存使用情况.当您的程序开始消耗大部分可用物理内存时,其内存使用量将突然降至接近零.

有几个函数(比如GC.Collect())强制执行几个级别的垃圾收集,但强烈建议不要使用它们,除非你知道自己在做什么,因为这往往会使你的软件变慢并阻止垃圾收集器执行其工作以最佳方式.

  • 仅供参考:您在此处描述的垃圾收集方式是调用[追踪垃圾收集](http://en.wikipedia.org/wiki/Tracing_garbage_collection).另一种方法是[Refcounting garbage collection](http://en.wikipedia.org/wiki/Reference_counting#Use_in_garbage_collection),其中收集器的行为与预期的@EmmettYoung完全相同. (3认同)
  • @Yuval是的,我当然简化了一下.在幕后,事情要复杂得多**.但我认为我的解释有助于获得第一张一般的图片. (3认同)

pjc*_*c50 24

即使它在内部取消了内存,也没有义务将其返回给操作系统.它将假设将来会请求更多内存并回收页面.操作系统的编号不知道程序如何选择使用它声称的内存.

如果您确实要明确声明和释放内存,则必须通过Pinvoke不安全代码调用VirtualAlloc().

  • 这是一个非常重要的一点,所有其他答案都缺失了. (3认同)
  • 这真的是**关键.任务管理器报告的"内存使用率"没有下降的事实是*不是*没有发生垃圾收集的证据.使用malloc/free或new/delete的C或C++程序可以观察到同样的事情.整个问题是基于一个错误的前提. (3认同)

小智 5

CLR,因为它消耗的系统资源并不为每个内存释放运行垃圾收集器.因此,基于不断增长的内存大小,定期调用垃圾收集器.它会清除所有不相关的内存泄漏.

也可以使用GC.Collect()方法显式调用垃圾收集器,但不建议明确使用.


Jör*_*tag 5

垃圾收集很昂贵.您只希望它尽可能少地运行.理想情况下从不.因此,系统将尝试延迟垃圾收集,只要它可以,基本上直到你的内存不足.

分配内存很昂贵.一旦运行时分配了一些内存,它通常不会再次释放它,即使它当前不需要它,因为如果它在程序运行时间的一个时间内需要那么多内存,则可能需要它在将来的某个时间内存在类似数量的内存,并希望避免再次分配内存.

因此,即使如果您的测试过程中出现的垃圾收集,你不会看到它在任务管理器或Process Explorer中,因为CLR将不会释放也无妨.

您所描述的内容称为引用计数垃圾收集器.但是,CLI VES的所有当前现有实现都使用跟踪GC.跟踪GC不计算参考; 他们追踪它们,只有在它们运行时.跟踪GC 在实际跟踪对象图之前不会注意到对象是否仍然可访问,并且只有在需要运行集合时才会跟踪对象图,即当内存不足时.


Tho*_*ler 5

您链接到的文章中已经包含一些信息。有几种迹象表明您观察到的行为是正确的:

...垃圾收集器可以(但不要求)将对象视为不再使用。

...在某些未指定的时间...

GC.Collect()

至少对于旧的(非并行)垃圾收集器而言,重要的一点是,垃圾收集器在不同的线程上运行。您可以在调试器中进行验证:

0:003> !threads
ThreadCount: 2
UnstartedThread: 0
BackgroundThread: 1
PendingThread: 0
DeadThread: 0
Hosted Runtime: no
                                      PreEmptive   GC Alloc           Lock
       ID OSID ThreadOBJ    State     GC       Context       Domain   Count APT Exception
   0    1 1b08 0058f218      a020 Enabled  025553ac:02555fe8 0058b868     1 MTA
   2    2 1e9c 005a78c8      b220 Enabled  00000000:00000000 0058b868     0 MTA (Finalizer)
Run Code Online (Sandbox Code Playgroud)

终结器线程执行垃圾回收。在操作期间所有其他线程都被挂起,因此在重组期间没有线程可以修改对象。

但是为什么这么重要?

它说明了为什么垃圾回收不能立即应用,无论在您的方案中还是在您调用GC.Collect()进行垃圾回收时都没有。为了运行垃圾收集器,您还需要一个线程开关。因此,非并发垃圾回收所需的最低代码为

GC.Collect();
Thread.Sleep(0);
Run Code Online (Sandbox Code Playgroud)

如果您担心内存管理,请确保还查看有关IDisposable的出色答案

可用内存

此外,还没有人解释说,使用任务管理器查看内存消耗并不可靠。

.NET直接作用于虚拟内存,即使用虚拟内存管理器。它不使用堆,即堆管理器。相反,它使用自己的内存管理(称为托管堆)。

.NET从Windows(内核)获取内存。假设它从Windows那里获得了一块新的内存,里面没有.NET对象。从Windows的角度来看,内存已消失(提供给.NET)。但是,从.NET的角度来看,它是免费的,并且可由对象使用。

同样,您可以在调试器中观察到这一点:

0:003> !address -summary
--- Usage Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
Free                                     60          71cb9000 (   1.778 Gb)           88.91%
<unknown>                                84           986f000 ( 152.434 Mb)  67.09%    7.44%
Image                                   189           2970000 (  41.438 Mb)  18.24%    2.02%
...
Run Code Online (Sandbox Code Playgroud)

<unknown>从Windows的角度来看,报告的是虚拟内存。在这种情况下,将使用150 MB。

0:003>!dumpheap -stat
...
00672208       32      8572000      Free
...
Run Code Online (Sandbox Code Playgroud)

因此,从.NET角度来看,您可以看到8.5 MB可用空间,但尚未归还Windows(尚未),并且仍将报告为已使用。

测量工作集

如果您尚未修改任务管理器的默认列设置,则情况更糟,因为它将显示工作集,该工作集仅是RAM中的内存。但是,某些内存可能已交换到磁盘,因此任务管理器可能报告该内存。