批量和分页
在典型的互联网web应用中,数据量较大且高并发的情况下,不分页,或者不进行批量处理,每次总是取出很多用户数据,很容易造成内存开销过大,系统内存吃紧。再比如我们有时候进行文件操作,读取文件内容的时候就要斟酌考虑文件有多大。
慎用静态
比起大数据查询造成的常发性的内存不足,使用静态太多的应用程序一时半会也不会内存泄露。可随着系统的运行,静态的东西越来越多,内存开销也就越大,由于GC的回收策略,无效的静态所占内存又不容易及时释放,久而久之就造成了内存不足。
二方库、三方库,非托管资源,优先使用Dispose模式
每次调用别人的资源都应该有个警觉性:用你的类库可以,占用我的(内存)不行。
如果你熟悉自动内存管理,熟悉GC,理解Dispose模式,那么一定会在调用别人的资源的时候想着还是using一下为妙,或者,强制赋个null也是举手之劳,要相信某些良好的编程习惯可以让自动内存管理更有效。
减少字符串临时对象
举例来说,对于String的+=,很多应用程序中这个操作层出不穷。我们都知道+=操作会造成很多临时字符串对象,这些对象由于CLR对字符串的驻留处理,容易占用内存空间。如果是高并发的web应用程序,而字符串操作随处可见,且字符串的长度又不确定地长,前端页面各种各样的拼接,久而久之,内存占用就会是一个重大问题。CLR对字符串的优化处理使得字符串不被优先回收,如果字符串操作频繁,临时字符串较长(比如大于等于85000字节)而出现大对象堆的分配,那么更容易出现内存泄露。
很多人可能都会想到如何优化程序去降低string的临时对象的生成概率。对的,StringBuilder的出现就顺理成章了。
警惕大对象
1、任何大于等于85000字节的对象都被自动视为大对象,大对象从特殊的大对象堆中分配。大对象堆和小对象堆一样进行终结和释放,但是GC回收算法从来不对大对象堆(Large Object Heap)进行内存压缩整理,因为在堆中下移85000字节或更大的内存块会浪费太多的CPU时间;
2、在.NET中,CLR采用基于代(generation)的垃圾回收,大对象总被认为是第2代(generation)的一部分,GC分析哪些对象不可达,优先分析第0代和第1代,第2代的对象通常被认为长时间存活。
正是由于1和2所述的两个原因(主要原因还是第1个),在垃圾回收过程中容易造成内存碎片。这里推荐一篇老外写的流传甚广的文章供参考:the dangers of the LOH。
随着应用程序的运行,如果LOH导致的内存碎片越来越多,内存有效使用率下降会非常严重,比如我们在web应用程序中+=拼接字符串(见第4条的分析),如果大于等于85000字节的字符串临时对象很多,那么用户量一上去,随着系统的运行,GC回收压力越来越大OOM的风险会变得更高。
虽然内存碎片化导致的OOM看上去似乎无解,但是如果写程序的人仔细分析解决问题,想方设法主动降低创建大对象的频率,那么内存泄露的可能就会降低,足够优秀健壮的程序不能彻底解决OOM,但是我们完全可以将风险发生的情况将至最低可能。
一个足够合格的coder肯定需要具备充足的分析和解决OOM问题的准备和经验,有很多分析和检查OOM的工具如ANTS Memory Profiler,还可以通过调试利器如windbg对内存dump文件进行分析。用好这些工具,让OOM无所遁形也不失为解决之道。