【问题标题】:Working of Process Memory/Garbage Collector进程内存/垃圾收集器的工作
【发布时间】:2015-07-26 20:30:47
【问题描述】:

我一直在尝试了解进程内存的工作原理。所以我尝试了以下一段代码

    public void OpenFormWithoutList()
    {
        Form2 form = null;
        int index = 0;
        while (index < 5000)
        {
            form = new Form2();
            form.ShowDialog();
            index++;
        }
    }
    public void OpenFormWithList()
    {
        Form2 form = null;
        List<Form> list = new List<Form>();
        int index = 0;
        while (index < 5000)
        {
            form = new Form2();
            list.Add(form);
            form.ShowDialog();
            index++;
        }
        list = null;
    }

在 Form2.cs 中,我在 OnLoad 事件中关闭了表单,因此控件应该再次返回到父表单 (Form1)。

当我从一开始分别运行这两个方法时,以下是方法执行后的观察:

开始:20 MB OpenFormWithList(): 29MB

开始:20MB OpenFormWithoutList(): 25MB

当调用 OpenFormWithoutList() 时,GC 正在收集表单,因此内存使用量不会达到 29MB。但是一旦这些方法结束,那么内存使用也不会回到开始阶段,即 20MB。

那么为什么内存没有被清除,究竟是什么在消耗内存?

【问题讨论】:

  • 旁注:list = null; 充其量是多余的。 GC 和 JIT 协作以了解变量的生命周期。他们已经知道,在方法结束时,list 变量不再是“活动”引用,并且不能使用该变量来保持被引用对象的活动状态。
  • 请记住,每个表单所需的资源句柄数量有限。它可能会使您的测试复杂化。最好避免使用任何形式的 UI 进行内存测试。

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


【解决方案1】:

请记住,垃圾回收不会在任何实例处置后立即释放内存。它已被优化为仅在内存压力时触发和释放内存。因此,如果您想测试内存泄漏,您应该在读取计数器读数之前手动执行垃圾收集。

GC.Collect();
GC.WaitForPendingFinalizers();

【讨论】:

  • WaitForPendingFinalizers 调用之后,您需要另一个GC.Collect - 否则将不会回收正在完成的实例使用的内存。
  • @Luaan:如果有Finalization的Disposable对象还没有被dispose,需要再次调用GC.Collect()。
【解决方案2】:

.NET 使用分代垃圾收集器,并且没有确定性内存分配之类的东西(当然,除非你到处使用不安全的代码和结构)。

这里最相关的部分是,在每次分配时,运行时都会检查自上次尝试进行垃圾收集以来已分配了多少内存 - 如果超过某个阈值,则开始收集。因此它将遍历整个内存(对于第 2 代收集 - 较低代的收集仅通过较低代的堆),记下所有没有引用它们的对象,并将它们清除。最后,它将compact堆 - 移动所有对象以在堆中创建一个连续的空间。这一点非常重要,因为 .NET 不会在堆的中间分配 [1] - 它类似于一个允许从中间“弹出”的强化堆栈。

完成后,所有幸存的对象都被提升到下一代堆(除非它们已经处于最大代,在撰写本文时为两个)。

这是有列表的变体和没有列表的变体之间的区别。有了更多的分配,旧的表单实例就如你所料地被回收了——但前提是首先有足够的分配。还有其他隐藏的成本——很可能,第一次初始化需要加载一些库或一些共享的初始化。这就是为什么您总是希望在进行任何测试之前使用某种形式的热身。此外,无论如何,进程内存并不是那么重要 - 如果您想解决内存问题,CLR Profiler 或类似的东西会更有用。

您可以通过调用GC.Collect 来强制垃圾收集器完成其工作,但这是不建议的。你不应该真的需要它,几乎从来没有。只是习惯于对内存分配和释放没有完美的控制——你在一个多线程、抢先式多任务、内存虚拟化系统上,现在很可能是分布式的。无论如何,对内存的精确控制都是一种错觉:D

另一个重要的一点是对编译器和运行时的另一件事的误解。将null 分配给本地并没有真正做任何事情 - 如果您在调试器之外运行,则一旦不再使用本地将有资格进行收集。如果您在调试器内部运行,则所有本地变量都会保留在整个范围内(当然是为了帮助调试)。此外,当您没有合理的值来初始化它们时,请避免初始化它们 - 您正在剥夺编译器对显示意外代码路径的帮助。

[1] 请注意,这仅适用于主堆。大对象堆确实允许在中间分配,而且它不紧凑。从 .NET 4.5 开始,可以选择在 LOH 上手动强制进行堆收集。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2017-03-15
    • 1970-01-01
    • 2016-11-02
    • 1970-01-01
    • 1970-01-01
    • 2014-03-31
    • 2012-04-08
    • 2014-02-05
    相关资源
    最近更新 更多