进程内存/垃圾收集器的工作

本文关键字:收集器 工作 内存 进程 | 更新日期: 2023-09-27 18:05:01

我一直在努力了解进程内存的工作。所以我试着下面这段代码

    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)。

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

Start: 20mbOpenFormWithList (): 29 m

开始:20 mbOpenFormWithoutList (): 25 mb

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

那么为什么内存没有被清除,到底是什么在消耗内存呢?

进程内存/垃圾收集器的工作

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

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

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

这里最相关的部分是,在每次分配时,运行时检查自上次尝试垃圾收集以来已分配了多少内存-如果超过某个阈值,则开始收集。因此,它将遍历整个内存(对于第2代收集—低代收集只遍历低代堆),注意所有没有引用的对象,并清除它们。最后,它将压缩堆——移动所有对象以在堆中形成一个连续的空间。这是非常重要的,因为。net不会在堆的中间进行分配[1]——它就像一个增强的堆栈,允许从中间"弹出"。

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

这是带列表的变体与不带列表的变体之间的区别。有了更多的分配,旧的表单实例就像您所期望的那样被回收了——但是只有当首先有足够的分配时。还有其他隐藏的代价——很可能,第一次初始化需要加载一些库或一些共享的初始化。这就是为什么你总是想在做任何测试之前做一些热身活动。此外,进程内存并不是那么重要——如果你想解决内存问题,CLR Profiler或类似的东西会更有用。

您可以通过调用GC.Collect来强制垃圾收集器完成它的工作,尽管这是不明智的。你不应该真的需要它,基本上永远不需要。只是要习惯没有对内存分配和释放的完美控制——你在一个多线程、抢占式多任务、内存虚拟化的系统上,现在很可能是分布式的。对内存的精确控制是一种错觉:D

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

[1]注意,这只适用于主堆。大的对象堆确实允许在中间分配,而且它不会压缩。从。net 4.5开始,有一个选项可以在LOH上手动强制堆收集。