通俗易懂话GC-C#的内存管理
昨天和一个朋友聊到图像处理软件内存占用多的问题,然后很自然聊到了GC,回想起以往很初学者都问到类似的问题:
1、C#自己就会垃圾回收,为什么我还要关心垃圾回收?
2、GC可以回收垃圾,但回收的时候又会让线程卡住,我到底该不该GC.Collect()?
为了回答这些问题,我们先从一个小实验讲起
我们先做这样一个程序,不断从电脑摄像头读取图像,然后显示出来,代码很简单:
using OpenCvSharp;
using var capture = new VideoCapture(0, VideoCaptureAPIs.DSHOW);
if (!capture.IsOpened())
return;
capture.FrameWidth = 1920;
capture.FrameHeight = 1280;
capture.AutoFocus = true;
const int sleepTime = 10;
using var window = new Window("capture");
while (true)
{
var image = new Mat();
capture.Read(image);
if (image.Empty())
break;
window.ShowImage(image);
int c = Cv2.WaitKey(sleepTime);
if (c >= 0)
{
break;
}
}
你将看到,内存持续增长,直到发生GC,如此往复:
现在我们加入手动GC:
using OpenCvSharp;
using var capture = new VideoCapture(0, VideoCaptureAPIs.DSHOW);
if (!capture.IsOpened())
return;
capture.FrameWidth = 1920;
capture.FrameHeight = 1280;
capture.AutoFocus = true;
const int sleepTime = 10;
using var window = new Window("capture");
while (true)
{
var image = new Mat();
capture.Read(image);
if (image.Empty())
break;
window.ShowImage(image);
int c = Cv2.WaitKey(sleepTime);
//手动触发GC
GC.Collect();
if (c >= 0)
{
break;
}
}
可以看到,随着密集的GC,内存占用平稳了:
但是,问题解决了吗?有经验的小伙伴会知道,GC会让程序变卡,所以手动GC在大多数时间并不是一个万能灵药,相反是毒药,那么真正的解决方案是什么呢?
我们把代码再改一下:
把GC.Collect(); 改成 image.Dispose();
using OpenCvSharp;
using var capture = new VideoCapture(0, VideoCaptureAPIs.DSHOW);
if (!capture.IsOpened())
return;
capture.FrameWidth = 1920;
capture.FrameHeight = 1280;
capture.AutoFocus = true;
const int sleepTime = 10;
using var window = new Window("capture");
while (true)
{
var image = new Mat();
capture.Read(image);
if (image.Empty())
break;
window.ShowImage(image);
int c = Cv2.WaitKey(sleepTime);
// GC.Collect(); //手动触发GC
image.Dispose(); //手动释放对象
if (c >= 0)
{
break;
}
}
可以看到,即没有发生内存暴涨,也没有发生GC。
为什么呢?GC.Collect(); 和 image.Dispose(); 分别发生了什么?
GC.Collect(); 时,程序内部发生了大迁徙:
第一步 GC线程会把其它线程从合作模式转换到抢占模式,合作模式线程可以访问托管堆和非托管堆,抢占模式只能访问非托管堆,可以简单的认为,GC线程会暂停其它线程。
第二步 把托管堆的对象都标记为垃圾(我不是针对谁)。
第三步 标记出存活的对象。
第四步 清理失活对象,把标记存活的对像向前移动,覆盖空闲内存区,在后部空出整片的空闲区。
第五步 恢复其它线程。
image.Dispose();时,程序只是把imgae对象所占的区域标定为空闲区。成本是极低的。
GC的成本很高,我应该想办法避免GC,尤其是在程序需要高实时响应的场合,那么又有了新的问题:
能避免自动GC吗?
首先,不需要过分担心自动GC,因为它和手动GC的“成本”大多数情况下并不相同。
为了不动辄进行大迁徙,设计者有设计分代回收的优化策略,一般对象初始化时会放在0代,每经历一次GC还能存活,就会上升一代,最终来到2代。大对象(大于85,000 byte) 比如图像初始化时就在2代。0代和1代的空间比较小,这样也是为了加速GC,自动GC一般不会像手动GC那样,把所有的代都整一遍,而是会优先回收低代内存,如果不够才会回收高代。
但避免自动GC还是我们的终极目标,想避免自动GC,首先要明白,自动GC什么时候会发生?
1、给新对象分配空间时,发现不够了。
2、收到系统物理内存不够的通知。
所以,总结一下:
1、大对象要手动销毁。别人实现的.Dispose()的要记得调用,自己写的大对象类要实现.Dispose()。
2、力大砖飞,物理内存要够大。
3、在合适的时候手动GC。
4、避免频繁创建大对象,比如上面的代码最优应该是下面这样:
using OpenCvSharp;
using var capture = new VideoCapture(0, VideoCaptureAPIs.DSHOW);
if (!capture.IsOpened())
return;
capture.FrameWidth = 1920;
capture.FrameHeight = 1280;
capture.AutoFocus = true;
const int sleepTime = 10;
using var window = new Window("capture");
//在循环外创建大对象
using var image = new Mat();
while (true)
{
capture.Read(image);
if (image.Empty())
break;
window.ShowImage(image);
int c = Cv2.WaitKey(sleepTime);
if (c >= 0)
{
break;
}
}