本篇讨论以下内容:
- 网站中消耗内存的主要两类对象:托管资源和非托管资源。
- 托管资源的生命周期。
- 减少托管资源和非托管资源内存使用的方法。
- 减少session使用的内存。如果使用不当,session会占用很多内存。
托管资源
托管资源是在代码中使用new关键字在堆上创建的对象。
生命周期
当调用new创建对象时,会在托管堆上分配内存。这只会发在在分配区域的末尾,这样效率会很高。
当CLR在分配区域的末尾为一个对象分配内存时,如果发现没有足够的空间,就会启动垃圾收集。这会移除所有没有被引用的对象,并压缩存活的对象。这会在分配区域的末尾产生连续的空间。
当一个对象所有引用离开它们的作用域或它们被设为null,这个对象就可以被垃圾收集了。然而,它只有在下次垃圾收集时,才会被物理删除,这可能不会立即发生。这意味着一个对象会在不再使用它后,继续使用一段时间内存。有意思的是,使用时间越长的对象,越有可能在失去所有引用后继续保留在内存中,这完全取决于它的代。
代
垃圾收集器进行垃圾收集时,会访问大量的对象,检测它们是否可以被收集,这个操作是非常昂贵的。为了減少代价,CLR假设这个一个事实:使用时间越长的对象比最近创建的对象更不可能失去它们的引用。这意味着优先检查年经的对象是有意义的。
实现这种算法的方式是将所有的对象划分成三代:0代、1代和2代。每一个对象对从0代开始,如果在一次垃圾收集中存活下来,就升级为1代。如果1代对象在下一次垃圾收集中存活下来,就升级为2代。2代对象在下一次垃圾收集中存活下来,还是2代。
0代对象会被频繁收集,1代次之,2代再次之。所以一个对象存活的越久,它所有引用都失效后被移除花费的时间越长。
当1代对象被收集时,0代对象也会被收集。2代对象被收集时,1代和0代都会被收集。所以,高代对象收集代价更高。
大对象堆
除了以下提到的堆,还在一个超过85KB对象使用的推,称作大对象堆。如果大对象堆或者2代对象空间不够,就会触发大对象堆收集和2代对象收集。因为2代对象收集会收集所有代的对象,所以这个操作代价是很高的。
大对象堆上对象在收集时,不会进行压缩。所以,如果在大对象堆上分配了不同大小的大对象,会在对象间留下很多不能再分配的空间。
计数器
在perfmon中使用.NET CLR Memory分类中的以下计数器:
- Percent Time in GC: 从上次收集完后生,花费在GC上的时间百分比。
- Gen 0 heap size: 在下次0代垃圾收集前可以分配给0代对象的最大字节数。这不是分配给0代对象的当前字节数。
- Gen 1 heap size: 1代对象占用的字节数。这不是1代对象可能使用的最大字数。
- Gen 2 heap size: 2代对象占用的字节数。这不是2代对象可能使用的最大字数。
- Large Object Heap size: 大对象堆的大小。
- # Bytes in all heaps: Gen 0 heap size、Gen 1 heap size、Gen 2 heap size、Large Object Heap size的总和。它表示所有托管对象占用的内存。
- # Gen 0 Collections: 自从程序启动后,0代对象被收集的次数。
- # Gen 1 Collections: 自从程序启动后,1代对象被收集的次数。
- # Gen 2 Collections: 自从程序启动后,2代对象被收集的次数。
CLR profiler
CLR Profiler是微软的一个显示程序内存占用的免费工具。从以下地址下载CLR Profiler:
- 适用.NET Framework 2.0的CLR Profiler:
CLR Profiler使用方法参考它的说明文档。
Garbage collector
根据垃圾收集器的工作方式,可以通过以下方法优化性能:
- 迟获取
- 早释放
- 使用StringBuilder连接字符串
- 使用Compare进行不区分大小写比较
- 使用Response.Write缓冲区
- 池化超过85KB的对象
迟获取
尽可能迟得创建对象。这会节省内存,并减短它们的存活时长,这样它们就少可能升级到高代对象。特别地:
- 不要在长操作前创建对象。
不要LargeObject largeObject = new LargeObject(); // Long running database call ... largeobject.MyMethod();
而是// Long running database call ... LargeObject largeObject = new LargeObject(); largeobject.MyMethod();
除了超过85KB的对象,不要预分配内存。预分配内存在那些不基于.NET没有垃圾收集的系统,例如实时系统,可能会很有效。然而,在.NET下这样做没有任何优势。
如果使用.NET 4,考虑使用Lazy<T>。
Lazy<ExpensiveObject> expensiveObject = new Lazy<ExpensiveObject>();
与通常情况下的类实例化不同,这不会创建对象。这个对象只会在第一次引用它的时候被创建。
早释放
如果只需要使用一个对象很短的时间,就要确保它没有一个长时引用。否则,垃圾收集器将不知道它可以被收集,甚至将它升级到高代对象。
如果需要在一个长时对象中引用一个短时对象,当你不再需要这个短时对象时,将它的引用设为null:
LargeObject largeObject = new LargeObject();
// Create referece from long lived object to new large object
longLivedObject.largeObject = largeObject;
// Reference no longer needed
longLivedObject.largeobject = null;
在一个类中,如果一个属性引用了一个短时对象,然后要执行一个长时操作前,如果你不再需要这个短时对象,将引用设为null。
private LargeObject largeObject { get;set; }
public void MyMethod() {
largeObject = new LargeObject();
// some processing ...
largeObject = null;
// more lengthy processing ...
}
对于本地变量(local variables, 方法中定义的变量)没有必要这么做,因为编译器可以判定变量什么时候不会再使用.
使用StringBuilder连接字符串
什么时候不使用StringBuilder
- 当字符串连接少于7次。
- 当在一条语句中连接字符串时,只会产生一个最终字符串。
s = s1 + s2 + s3 + s4;
StringBuilder的容量
StringBuilder的构造器可以接受一个容量参数,默认值是16。每次StringBuilder超过容量,就会分配一起原来容量两倍大的缓冲区,并将老的缓冲区中的内容复制到新人缓冲区中,并将老的缓冲区留给垃圾收集器。
所以,如果知道结果字符串的大小,在StringBuilder构造器中设置容量参数,会提高性能。
使用Compare进行不区分大小写比较
不使用:
// ToLower allocates two new strings
if (s.ToLower() == s2.ToLower()) {
}
使用:
if(string.Compare(s1, s2, true) == 0) {
}
以上代码会使用current culture。使用字节依次比较会更快一点:
if(string.Compare(s1, s2, StringComparison.OrdinalIgnorCase) == 0){
}
使用Response.Write缓冲区
不使用:
// Concatenation creates intermediate string
Response.Write(s1 + s2);
使用
Response.Write(s1);
Response.Write(s2);
在使用自定义控件时,对HtmlTextWrite对象使用相同的技术。
池化超过85KB的对象
频繁地分配和收集超过85KB的对象是很昂贵的操作,这也会导致内存碎片。
如果网站需要实例化超过85KB的类,考虑为这些对象在程序启动时创建一个池,而不要每次都创建。
在决定一个对象是否超过85KB时,确定它是自己占用了这些空间,还是仅仅引用了占用这么多空间的对象。例如,一个有85 * 1024 = 87040个元素的字节数组会占用85KB:
byte[] takes85KB = new byte[87040]; // 85 * 1024 = 87040
但是,一个引用85个对象,每个对象占用1024字节的数组是不会占用那么多空间的,因为这个数组仅仅包括了对象的引用而已。
非托管资源
一些经常使用的对象是基于非托管资源的,例如,文件句柄和数据库连接。这些资源都是稀缺资源。垃圾收集器也在会这些对象失去所有引用后,删除它们,并释放它们使用的稀缺资源。但最好在不使用它们后,立即释放,以供其它线程使用。
IDisposable
实现IDisposable的对象提供了Dispose()方法,这个方法会释放对象。它们也经常实现与Dispose方法功能相同的Close()方法,虽然Close()方法并不是IDisposable的成员。
如果一个对象实现了IDisposable接口,就要尽快调用Dispose()方法,而不要缓存这些对象。
为了保证即使抛出了异常,非托管资源也能尽快释放,一般在final代码块中调用Dispose():
SqlConnection connection = new SqlConnection(connectionString);
try {
// use connection...
}
finally {
connection.Dispose();
}
在C#中可以使用using语句:
using (SqlConnection connection = new SqlConnection(connectionString)) {
// use connection ...
} // connection.Dispose called implicitly
没有必要等到using语句自动释放连接。如果对已释放的对象调用Dispose(),不会产生错误。
using (SqlConnection connection = new SqlConnection(connectionString))
{
// use connection ...
connection.Dispose();
// long running code that doesn't use the connection ...
} // connection.Dispose is called again implicitly
如果创建了一个包含实现IDisposable的对象的类,或者从一个实现IDisposable的类继承,或者包括操作系统提供的非托管对象,就需要实现IDisposable。更多的信息,查看:
- IDisposable接口:http://msdn.microsoft.com/en-us/library/system.idisposable.aspx?ppud=4
- 实现IDisposable接口:http://msdn.microsoft.com/en-us/library/fs2xkftw.aspx?ppud=4
计数器
分类:.NET CLR Memory
# of Pinned Objects:上一次垃圾收集中遇到的pinned对象。非托管代码使用pinned对象。垃圾收集器不能在中移动pinned对象,所有有可能造成堆碎片。
分类:.NET CLR LocksAndThread
# of current logical Threads:应用程序中使用的.NET线程对象的数量。如果这个数字持续上升,那就是在不断地创建新线程,而没有移除老线程和它们的堆栈。
分类:Process
Private Bytes:当前进程分配的不能与其它进程共享的内存大小。非托管对象分配的内存等于Private Bytes – #Bytes in all Heaps(.NET CLR Memory分类)。
Sessions
Session state允许在多个请求间为访问者存储信息。在默认的InProc模式下,这些信息是存储在内存中的。这样速度最快,但如果有多个服务器,这种方式是不起作用的,因为下一个来自同一个访问者的请求可能由另一台服务器处理。StateServer和Sql Server模式满足多服务器的需要,它们将信息存储在中央服务器或数据库中。
ASP.NET为了区别不同用户的Session,将session ID存储在浏览器cookie中。另一种方式是存储中URL中。
ASP.NET没有一个方法判定访问者是否不再使用程序,这样session state就可以删除了。为了解决这个问题,ASP.NET假定如如果20分钟后没有收到访问者的请求,这个访问者的session state就可以删除了。20分钟这个数值是可配置的。这意味着,session state至少占用20分钟内存。
计数器
分类:ASP.NET Applications
Sessions Active:当前活动的session数量。
如果你确认session state占用了过多的,有一些解决方案:
- 减少session state存活时间。
- 减少session state占用的空间。
- 使用另一种session模式。
- 不使用session state。
如果sessions很快过期,它们会占用更少的内存:
- 在web.config中配置sessionState元素:
<configuration> <system.web> <sessionState mode="InProc" timeout="20" /> </system.web> </configuration>
也可以使用代码配置:Session.Timeout = 20;
当访问者注销时,可以显式移除session:
Session.Abandon();
减少session state占用的空间
不要存储有额外开销的对象。例如,不要存储用户界面元素或数据表。只存储需要的数据。
在缓存中存储session无关的数据,这样所有用户都可以共享这些数据。例如,不要在session中存储国家列表。
不要存储来自数据库中的数据。这样,就会加重数据库的负载,换取内存使用的减少。这样做在取数据速度快,并且不经常发生,而且数据本身占用很多内存的情况下是可行的。
使用其它的session模式
如果内存不够,可以使用SqlServer模式。这种方式带来的一个好处是当web服务器或应用程序池重启,session不会丢失。但是这会加重数据库的负载。使用这种模式,在不更新session state的页面中指定只读模式:
<%@ Page EnableSessionState="ReadOnly" %>
不使用session state
在很多情况下,根本不需要使用session state。有一些替代方案:
- 使用AJAX减少页面刷新。这使得可以在页面中保存session数据。
- 在ViewState中保存session state。session state不是很大,并且都是页面相关的时候,可以这么做。但这样会占用带宽,也有安全问题。
一个不使用session的原因是这会在cookie或URL中存储session ID,而这些会损害缓存功能。