【问题标题】:Garbage collection in C# not carried out. Why?未执行 C# 中的垃圾收集。为什么?
【发布时间】:2015-04-06 05:41:28
【问题描述】:

我尝试了一个简单的实验来验证垃圾收集器的功能。参考 3.9 Automatic memory management (MSDN) 关于.NET 中的自动内存管理。对我来说,这听起来像是 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;
    }

结果如下:

内存分配前

内存分配后

离开函数作用域之前

离开函数作用域后

为什么数组a、b、c、d分配的内存被设置为null后,垃圾回收器没有释放?

【问题讨论】:

  • 您不能指望在设置 null 后立即发生垃圾回收。当然,您可以通过调用 GC.Collect 来强制执行它以查看差异。
  • 如果您需要确定性内存释放,那么您使用了错误的语言。
  • 一般情况下,不要试图理解垃圾回收,也不要乱搞。
  • 仅供参考,您使用了错误的工具;在任务管理器中查看总分配几乎不会告诉您 .NET 如何管理内存。如果您有兴趣了解垃圾收集器的工作原理,请使用 .NET 内存分析器。这就是它的用途。
  • 为什么这个问题会受到如此多的关注?非确定性是关于 .NET GC 的第一件事。这一定是第 1000 个这样的问题。

标签: c# .net memory-management garbage-collection


【解决方案1】:

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

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

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

您可能会多次调用您的测试(在循环中使用一些 Sleep())并观察内存使用量的缓慢增加。当您的程序开始消耗大量可用物理内存时,它的内存使用量将突然下降到接近于零。

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

【讨论】:

  • 仅供参考:您在此处描述的垃圾收集方式称为Tracing garbage collection。另一种选择是Refcounting garbage collection,其中收集器的行为与@EmmettYoung 预期的完全一样。
  • @DrKoch 当它达到大约 3-4 GB(取决于许多因素)时,它会突然下降到接近零。 这也不是确定性的。一旦其第 0 代段已满并且需要释放内存,GC 将执行一个集合。在服务器 GC 模式下使用 64 位进程,在 4GB 时也不一定会发生
  • @Yuval 是的,我当然简化了一点。幕后的事情要复杂得多。但我认为我的解释有助于初步了解总体情况。
  • @Yuval 我的回答包含“不多”“一大堆”。这个数量根本不准确,但我认为这正是对这种(一般的、概述类型的)答案有帮助的信息。
  • @DrKoch 也许您可以将“大约 3-4”的数字更改为“开始消耗大量可用物理内存”。当一个真正的意思是“很多”时,通常很想扔掉“一百万”。
【解决方案2】:

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

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

【讨论】:

  • 这是一个非常重要的一点,所有其他答案都缺失了。
  • 这确实是 关键。任务管理器报告的“内存使用情况”没有减少这一事实不是表明垃圾收集没有发生的证据。使用 malloc/free 或 new/delete 的 C 或 C++ 程序也可以观察到同样的情况。整个问题是基于一个错误的前提。
【解决方案3】:

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

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

...在以后的某个未指定的时间...

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)

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

但为什么这很重要?

它解释了为什么垃圾收集不会立即应用,无论是在您的场景中,还是在您调用 GC.Collect() 进行垃圾收集时。为了让垃圾收集器运行,您还需要一个线程切换。因此,非并发垃圾回收所需的最少代码是

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

如果您担心内存管理,请务必同时查看awesome answer about 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%
...

报告为&lt;unknown&gt; 的内容是从Windows 的角度来看的虚拟内存。在本例中,使用了 150 MB。

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

因此,从 .NET 的角度来看,您可以看到 8.5 MB 是免费的,但尚未归还给 Windows(尚未)并且仍会报告为在那里使用。

测量工作集

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

【讨论】:

    【解决方案4】:

    CLR 不会在每次内存释放时都运行垃圾收集器,因为它会消耗系统资源。因此,垃圾收集器会根据不断增长的内存大小定期调用。它将清除所有未引用的内存泄漏。

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

    【讨论】:

    • 我会说“不建议明确使用 [GC.Collect()]”有点过于笼统了。 GC 不是确定性的,但应用程序生命周期通常是可预测的。尤其是在游戏中(但不仅如此),在生成大量垃圾的“不经常调用”部分之后调用 GC.Collect 可以提高响应速度。例如,当加载大量对象并丢弃另一个对象时,在呈现新对象之前调用 GC.Collect 可以防止 GC“打嗝”呈现它们时,通过将集合“偏移”到加载时间。
    【解决方案5】:

    垃圾收集成本很高。您只希望它尽可能少地运行。理想情况下永远不会。因此,系统会尽量延迟垃圾回收,基本上直到内存耗尽为止。

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

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

    您所描述的称为 reference-counting garbage collector。但是,所有当前存在的 CLI VES 实现都使用 跟踪 GC。跟踪 GC 不计算引用;他们跟踪它们,仅在它们运行时。跟踪 GC 不会注意到对象是否仍然可访问直到它实际上跟踪对象图,并且它只会在需要运行集合时跟踪对象图,即当你用完内存。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-11-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多