未执行 C# 中的垃圾收集。为什么?
Garbage collection in C# not carried out. Why?
我尝试了一个简单的实验来验证垃圾收集器的功能。引用 3.9 Automatic memory management (MSDN) about automatic memory management in .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 后垃圾收集器不释放内存?
.NET 垃圾收集器是一种高度优化的复杂软件。它经过优化,可以使您的程序 运行 尽可能快,并且 不会占用太多内存 。
因为释放内存的过程需要一些时间,所以垃圾收集器通常会等待 运行 直到您的程序使用 大量 内存。然后它会立即完成所有工作,这会导致您的程序在相对较长的时间后出现小的延迟(而不是更早的许多小延迟,这会减慢您的程序)。
这意味着,垃圾收集器的时间 运行s 不可预测。
您可以多次调用您的测试(在循环中使用一些 Sleep())并观察内存使用量慢慢增加。当您的程序开始消耗大量可用物理内存时,其内存使用率将突然下降到接近于零。
有几个函数(如 GC.Collect()
)会强制执行多个级别的垃圾回收,但 强烈建议不要使用它们 除非你知道你在做什么正在做,因为这往往会使您的软件变慢并阻止垃圾收集器以最佳方式工作。
CLR 不会运行 每次释放内存时都使用垃圾收集器,因为它会消耗系统资源。因此垃圾收集器会根据不断增长的内存大小定期调用。它将清除所有未引用的内存泄漏。
也可以使用方法GC.Collect()显式调用垃圾收集器,但不建议显式使用。
即使它确实在内部取消分配内存,也没有义务将其 return 分配给操作系统。它将假定将来会请求更多内存并回收页面。操作系统的编号对程序如何选择使用它所声明的内存一无所知。
如果您确实想要显式声明和释放内存,则必须通过 Pinvoke 不安全代码调用 VirtualAlloc()。
垃圾收集很昂贵。您只希望它 运行 尽可能少。理想情况下永远不会。因此,系统会尽可能地延迟垃圾回收,基本上直到 运行 内存不足。
分配内存很昂贵。一旦 运行time 分配了一些内存,它通常不会再次释放它,即使它当前不需要它,因为如果它在 运行time 的某个时间内需要那么多内存程序,它很可能在将来的某个时候需要类似数量的内存,并且希望避免再次分配内存。
因此,即使 if 垃圾回收在您的测试期间发生,您也不会在任务管理器或进程资源管理器中看到它,因为 CLR 不会无论如何释放它。
您描述的是 reference-counting garbage collector。但是,所有当前存在的 CLI VES 实现都使用 tracing GC。跟踪 GC 不计算引用;他们追踪他们,只有当他们运行宁。跟踪 GC 不会注意到对象是否仍然可达 直到 它实际跟踪对象图,并且它只会在需要 运行 时跟踪对象图集合,即当你 运行 内存不足时。
部分信息已包含在您 link 的文章中。有几个迹象表明您观察到的行为是正确的:
... the garbage collector may (but is not required to) treat the object as no longer in use.
... at some unspecified later time ...
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)
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%
...
报告为 <unknown>
的是从 Windows 角度来看的虚拟内存。在本例中,使用了 150 MB。
0:003>!dumpheap -stat
...
00672208 32 8572000 Free
...
因此您可以看到,从 .NET 的角度来看,8.5 MB 是免费的,但尚未归还给 Windows(目前),并且仍将报告为在那里使用。
测量工作集
如果你没有修改任务管理器的默认列设置,那就更糟了,因为它会显示Working Set,它只是RAM中的内存。但是,一些内存可能已交换到磁盘,因此任务管理器可能不会报告。
我尝试了一个简单的实验来验证垃圾收集器的功能。引用 3.9 Automatic memory management (MSDN) about automatic memory management in .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 后垃圾收集器不释放内存?
.NET 垃圾收集器是一种高度优化的复杂软件。它经过优化,可以使您的程序 运行 尽可能快,并且 不会占用太多内存 。
因为释放内存的过程需要一些时间,所以垃圾收集器通常会等待 运行 直到您的程序使用 大量 内存。然后它会立即完成所有工作,这会导致您的程序在相对较长的时间后出现小的延迟(而不是更早的许多小延迟,这会减慢您的程序)。
这意味着,垃圾收集器的时间 运行s 不可预测。
您可以多次调用您的测试(在循环中使用一些 Sleep())并观察内存使用量慢慢增加。当您的程序开始消耗大量可用物理内存时,其内存使用率将突然下降到接近于零。
有几个函数(如 GC.Collect()
)会强制执行多个级别的垃圾回收,但 强烈建议不要使用它们 除非你知道你在做什么正在做,因为这往往会使您的软件变慢并阻止垃圾收集器以最佳方式工作。
CLR 不会运行 每次释放内存时都使用垃圾收集器,因为它会消耗系统资源。因此垃圾收集器会根据不断增长的内存大小定期调用。它将清除所有未引用的内存泄漏。
也可以使用方法GC.Collect()显式调用垃圾收集器,但不建议显式使用。
即使它确实在内部取消分配内存,也没有义务将其 return 分配给操作系统。它将假定将来会请求更多内存并回收页面。操作系统的编号对程序如何选择使用它所声明的内存一无所知。
如果您确实想要显式声明和释放内存,则必须通过 Pinvoke 不安全代码调用 VirtualAlloc()。
垃圾收集很昂贵。您只希望它 运行 尽可能少。理想情况下永远不会。因此,系统会尽可能地延迟垃圾回收,基本上直到 运行 内存不足。
分配内存很昂贵。一旦 运行time 分配了一些内存,它通常不会再次释放它,即使它当前不需要它,因为如果它在 运行time 的某个时间内需要那么多内存程序,它很可能在将来的某个时候需要类似数量的内存,并且希望避免再次分配内存。
因此,即使 if 垃圾回收在您的测试期间发生,您也不会在任务管理器或进程资源管理器中看到它,因为 CLR 不会无论如何释放它。
您描述的是 reference-counting garbage collector。但是,所有当前存在的 CLI VES 实现都使用 tracing GC。跟踪 GC 不计算引用;他们追踪他们,只有当他们运行宁。跟踪 GC 不会注意到对象是否仍然可达 直到 它实际跟踪对象图,并且它只会在需要 运行 时跟踪对象图集合,即当你 运行 内存不足时。
部分信息已包含在您 link 的文章中。有几个迹象表明您观察到的行为是正确的:
... the garbage collector may (but is not required to) treat the object as no longer in use.
... at some unspecified later time ...
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)
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%
...
报告为 <unknown>
的是从 Windows 角度来看的虚拟内存。在本例中,使用了 150 MB。
0:003>!dumpheap -stat
...
00672208 32 8572000 Free
...
因此您可以看到,从 .NET 的角度来看,8.5 MB 是免费的,但尚未归还给 Windows(目前),并且仍将报告为在那里使用。
测量工作集
如果你没有修改任务管理器的默认列设置,那就更糟了,因为它会显示Working Set,它只是RAM中的内存。但是,一些内存可能已交换到磁盘,因此任务管理器可能不会报告。