C#占用内存的问题
用C#写程序的时候,每每一开程序就占用20MB+的内存,写个稍微复杂一点的WPF程序就占到100+MB。相比之下,用MFC写的程序占用的内存则相当少,20+MB的内存占用就比较多了。这点曾让我相当头疼,毕竟内存占用也是衡量程序质量的一个比较重要的指标。
在读了"C# 3.0 in a nutshell"后,终于明白了是怎么回事。Garbage Collector并不是每时每刻都在回收垃圾的,尤其是对于类似C/C++中的栈型变量,即使在程序控制已经超出这些变量的作用域时,Garbage Collector也并不立即回收空间。这有两方面的考虑:一方面,既然系统有空余的物理内存,为什么要牺牲一定性能来花精力来保持一个较小的内存占用呢?让那些空闲的物理内存得到充分利用,为性能的提升做自己的一份贡献岂不很好?从另一个角度--Lazy Calculation--的角度来看,选择性的回收当前不再需要的内存需要较大的代价,而程序结束后一次性释放整个进程的内存则很简单。若仅仅当系统真正需要内存的时候才进行内存回收,就能讨一个巧:如果系统内存一直很充裕,就一直不需要进行垃圾回收,这部分时间就省下来了,程序的性能也就变相上去了。而最坏的情况不过是在运行中系统需要这些内存,那时再回收也不会有什么损失,不过是不能讨到巧罢了。(Lazy Calculation的思想在"More Effective C++"中也有介绍,在很多程序或系统,包括linux中有重要应用)。
我们可以改动一些C#的程序来验证CLR的确是这样做的。下面的语句可以得到当前程序真实的内存占用量:
1 string procName = Process.GetCurrentProcess().ProcessName;
2 using (PerformanceCounter pc = new PerformanceCounter
3 ("Process", "Private Bytes", procName))
4 statusTextBlock.Text = (pc.NextValue() / 1000).ToString();
其中statusTextBlock.Text是状态栏的文本,当然也可以用MessageBox等形式输出。
对于一个现有的WPF程序做实验,可以知道在某时刻其真实内存占用量为52MB,而此时用Windows XP的Task Manager观察则得到它的内存占用为100+MB。其中多出的这部分余量就是已释放但尚未回收的垃圾空间。此时系统的空余物理内存还有60%+,所以Garbage Collector没有进行回收。而若保持这个程序继续运行,打开一个占用大量内存的程序(如Visual Studio),则可以发现Task Manager中显示该程序的内存占用变成了52MB+,这是一个比较理想的结果,说明这个程序真实占用的内存不会超过53MB,在某种程度上验证了上文的理论。也说明C#的大内存占用并不是一个弊病,而是一种合理利用系统资源的手段。这和某些linux爱好者的观点是一致的:linux内存占用比windows xp要大,是合理利用系统资源这一设计意图的体现而并非bug。(当然,Garbage Collector对于回收垃圾的时机的选择和很多因素相关,并不仅仅受系统空闲内存的影响)
如果看Task Manager里面大内存占用实在不爽也可以用强制垃圾回收的办法解决。对于上面的程序,如果在统计前调用GC.Collect();强制回收内存,在Task Manager中显示的内存占用就变成了37MB。这个比较诡异,竟然小于程序内部统计得到的数值。根据google的结果,这也许是Windows XP下的Task Manager对CLR程序的统计有问题所致,据说在Vista下问题有所改善,不过以我的小破本Pentium M 1.2G + 768MB RAM的条件怕是没有条件实验证实了。
当然,即使做了这样的优化,这个C#占用的内存也有50MB左右,和C++程序还是有一定差距,这也许是CLR程序运行的原理决定的,其中有.NET Framework占用的内存。这篇文只能指出C#程序的内存占用也许没有Windows XP下Task Manager说的那样夸张,并提出了一种验证的方法。至于如何进一步减小C#程序的内存占用,也许还需要更精确可靠的内存统计手段,更新版本的.NET Framework和更详细的C#内存分配原理说明。