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

我尝试过一个简单的实验来validation垃圾收集器的function。 引用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; } 

以下是结果:

在内存分配之前

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

内存分配后

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

在离开function范围之前

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

离开function范围后

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

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

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

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

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

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

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

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

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

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

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

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

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

因此,即使在测试期间发生了垃圾收集,您也不会在任务管理器或进程资源管理器中看到它,因为无论如何CLR都不会释放它。

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

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

…垃圾收集器可以(但不是必须)将对象视为不再使用。

……在某些未指明的时间……

所以GC.Collect()

至少对于旧的(非并发)版本的垃圾收集器来说,一个重要的事情是,垃圾收集器在不同的线程上运行。 您可以在调试器中validation:

 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) 

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

但为什么这很重要?

它解释了为什么垃圾收集不会立即应用,既不在您的方案中也不在您调用GC.Collect()来执行垃圾收集。 要运行垃圾收集器,还需要一个线程切换。 因此,非并发垃圾收集所需的最小代码是

 GC.Collect(); Thread.Sleep(0); 

如果您担心内存管理,请务必查看有关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%  84 986f000 ( 152.434 Mb) 67.09% 7.44% Image 189 2970000 ( 41.438 Mb) 18.24% 2.02% ... 

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

 0:003>!dumpheap -stat ... 00672208 32 8572000 Free ... 

因此,您可以看到8.5 MB从.NET的角度来看是免费的,但尚未返回到Windows(尚未),并且仍然会在此处报告。

测量工作集

如果您还没有修改任务管理器的默认列设置,那就更糟了,因为它会显示工作集,它只是RAM中的内存。 但是,某些内存可能已交换到磁盘,因此任务管理器可能无法报告。