调试.NET Web应用程序High Memory - Part 1
最近遇到.NET Web应用程序内存使用的各种问题,总结一些具体的现象和调试方法。常见的不正确的内存使用造成高内存使用量主要原因有以下这么几种,
问题分类
大数据量DataTable
大多数web应用程序都会用到DataTable,DataTable中会有很多的Cell来存储表格中的数据,一旦表格中有过多的列便会导致内存使用量的急剧上升,如果不能够得到及时释放,内存就会被大量的表格数据占用。从而导致高内存使用量的问题。同时也增加内存中对数据的查找过滤的性能损耗,更糟糕的是很多情况下这些数据是要从数据库服务器通过网络传输过来,费力不讨好的典型。
解决这个问题还是要从数据量入手,尽量增加查询条件减少返回的数据量,或者分页查询,另外要及时的释放占用的内存。
Session或者Application State中存储太多数据
程序员经常会发明各种各样的提高性能方式,于是有些时候Session或者Application State中也变成着眼于性能的程序员的一块可利用资源,把很多数据塞在其中作为缓存,看上去是个很不错的想法,但是这很有可能让系统的内存使用量失去控制,从而再次造成高内存使用的问题。
那我总归要缓存一些必要的数据,我应该放在哪里?ASP.NET Cache是个不错选择,因为他有内建的机制来决定何时cache的内存被回收,例如在内存有压力的时候,减少cache至少比程序崩溃要好。
Debug模式下的应用程序和库文件(DLL)
经常发现生产环境中存在debug模式下的库文件DLL,或者干脆这个应用程序跑在debug模式下。
Web.config中如此设置
<compilationdebug=”true”/>
注意,debug模式是为了debug用的,对于生产环境不适用。
- 他会让变量生命周期变长,长到直到出了scope才考虑回收。所以在debug模式下发现内存使用就是比release模式下高一些,这就是原因了。
- 另外debug模式下webresource.axd,scriptresource.axd处理模块中不会让客户端缓存经其处理的脚本,客户端每次都要重新下载这些脚本!
- Page编译时间会变长,因为一些batch optimization被禁用了
- 代码执行时间变长,debug模式下会插入一些供debug的代码
所以如果你负责一个应用程序的部署,一定要注意稍微花一点时间把dll在release下模式下编译,同时debug设成false。或者干脆在机器级别配置文件中更改如下配置,该机器web应用程无一例外都要运行在release模式下。
Machine.config
<configuration>
<system.web>
<deployment retail=”true”/>
</system.web>
</configuration>
大量异常抛出
不要忽视异常,很多时候觉得异常没关系,catch住系统别崩溃就行了,但是别忘了异常也是一个消耗内存的object,而且有的时候消耗内存并不小,他不仅需要内存放他的调用栈信息,错误消息,很多时候他还包含了inner exception以及相应的错误相关的object。大量这样的异常放在内存中也会对内存造成相当大的损耗。
所以还是老老实实发现一个异常就看看是不是逻辑错误造成的,把问题消灭在萌芽状态。如果你想知道你的应用程序中有多少异常正在发生,performance counter可以帮你。
内存碎片
内存碎片是连续内存分配的杀手,一个很简单的例子,比如我有100M内存,但是第50M的点上存在一个4k的碎片,那如果64M连续空间的分配请求就会失败,因为没有连续的64M空间,OOM就发生了。
造成内存碎片的原因主要有哪些?
Debug = true
页面编译出来的dll被加载到内存中,如果在非batchcompile的应用程序中,页面都是在第一次被请求之后编译成相应的dll然后加载到内存中,那么这些dll就没有一个同一个位置来存放,于是散落在内存各个角落。成为内存碎片。
解决方式就是要将<compilation debug="false"/>从而打开batch compile,batch compile会将同一个文件夹的页面编译到一个dll中,这样就大大减少了内存碎片来避免该问题的发生。
Dynamic assemblies
XSLTtransformation
XSLT的加载过程中会产生动态assmebly,例如如下代码就会动态产生1000个dll。
For(int i=0;i<1000;i++)
{
xslt.Load(stylesheet);
//Do other stuff
xslt.Transform(doc, null, writer);
}
如果我们确定XSLT是一样的那就不需要加载多次。可以改为以下方式。
xslt.Load(stylesheet);
For(int i=0;i<1000;i++)
{
//Do other stuff
xslt.Transform(doc, null, writer);
}
XmlSerializer
msdn文档中对XmlSerializer (.NET framework 2.0)描述如下
Dynamically Generated Assemblies
To increaseperformance, the XML serialization infrastructure dynamically generatesassemblies to serialize and deserialize specified types. The infrastructurefinds and reuses those assemblies. This behavior occurs only when using thefollowing constructors:
System.Xml.Serialization.XmlSerializer(Type)
System.Xml.Serialization.XmlSerializer(Type,String)
If you use any of the other constructors, multiple versions of thesame assembly are generated and never unloaded, resulting in a memory leak andpoor performance. The simplest solution is to use one of the two constructorsabove. Otherwise, you must cache the assemblies in a Hashtable, as shown in the following example.
Hashtable serializers = new Hashtable();
// Use the constructor that takes a type and XmlRootAttribute.
XmlSerializer s = new XmlSerializer(typeof(MyClass), myRoot);
// Implement a method named GenerateKey that creates unique keys
// for each instance of the XmlSerializer. The code should take
// into account all parameters passed to the XmlSerializer
// constructor.
object key = GenerateKey(typeof(MyClass), myRoot);
// Check the local cache for a matching serializer.
XmlSerializer ser = (XmlSerializer)serializers[key];
if (ser == null)
{
ser = new XmlSerializer(typeof(MyClass), myRoot);
// Cache the serializer.
serializers[key] = ser;
}
else
{
// Use the serializer to serialize, or deserialize.
}
也就是说如果我们使用的不是上面列出的两个构造函数来构造XmlSerializer,那注意要通过缓存的方式将其缓存起来以备后续使用。因为其他的构造函数不会重用生成的dll,从而可能会造成内存碎片过多的问题。
我就是要加载很多dll去做某件事情,但是我不希望产生这样的副作用!
如果真的有这种要求其实也是有办法可以满足的。已经加载的dll只有到applicationdomain卸载的时候才会从内存中卸载掉。所以接下来我们要介绍的方式是通过创建和卸载一个单独的application domain来完成的。
class Program
{
static void Main(string[] args)
{
AppDomain tempDomain = null;
try
{
Console.WriteLine("Create temp domain");
tempDomain = AppDomain.CreateDomain("TempDomain");
TempDomain domain = (TempDomain)tempDomain.CreateInstanceFromAndUnwrap(
Assembly.GetAssembly(typeof(TempDomain)).Location, typeof(TempDomain).FullName);
domain.DoWork();
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
finally
{
Console.Read();
AppDomain.Unload(tempDomain);
Console.WriteLine("Unload temp domain");
}
Console.Read();
}
}
public class TempDomain : MarshalByRefObject
{
public void DoWork()
{
int i = 0;
try
{
for (i = 0; i < 100; i++)
{
Console.WriteLine("Creating {0} dynamic assembly", i);
AppDomain myCurrentDomain = AppDomain.CurrentDomain;
AssemblyName myAssemblyName = new AssemblyName();
myAssemblyName.Name = "TempAssembly";
AssemblyBuilder myAssemblyBuilder = myCurrentDomain.DefineDynamicAssembly(myAssemblyName, AssemblyBuilderAccess.Run);
ModuleBuilder myModuleBuilder = myAssemblyBuilder.DefineDynamicModule("TempModule");
}
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
}
}
Regular Expression正则表达式匹配大字符串
正则表达式是把典型的双刃剑,用的好可以帮你快速的完成极其复杂的字符串匹配或替换,但是一知半解的使用正则表达式很有可能掉进一些陷阱,造成内存或者CPU使用率的问题。
比如你在一个非常大的字符串(MB级别的字符串)中匹配字符串.
static void Main(string[] args)
{
Regex regex = new Regex("<body(.|\n)*</body>");
string html = File.ReadAllText("asp.net.txt");
MatchCollection matches = regex.Matches(html);
Console.WriteLine("Match count {0}", matches.Count);
Console.Read();
}
注意为什么堆中会存在如此大的int数组呢?这与正则表达式的实验有关,正则表达式需要数组来维护在匹配过程中的所有匹配位置信息,如果在这个过程中发现需要更大的数组的话,其实现是直接把数组变为原来的两倍长度。由于5Mhtml.txt中有很多的匹配,所以这些数组的大小便失去了控制。
注意这个问题不是.netframework实现的问题,而是对于所有的基于
非确定有限状态自动机的正则表达式实现均存在的问题。
调试方法
调试内存问题有各种各样的工具和方法,我们先从现成的工具开始,
Performance counter
性能计数器是系统性能最重要的观察工具,没有之一。无论是问题出现之前还是问题正在发生,打开性能计数器收集一些相关的的数据对调试一般都大有裨益。
观察托管代码的内存问题可以参考以下这些性能技术
•Process/PrivateBytes
•Process/VirtualBytes
•.NET CLRMemory/All counters
•.NET CLRLoading/All counters
通过.NET CLR Memory/%Time in GC这个性能计数,我们可以知道GC时间占用了多少百分比,如果发现这个百分比在50%以上,那么我们就要继续往下看看程序里再发生什么事情,如果这个时间只有10%或者更小,那就不用在这上面浪费时间了。
现在我们来说GC时间百分比较高的情形,接下来我们可以看看Allocated Bytes/sec,注意这个值经常会比太精确,如果性能计数的采样时间间隔大于内存回收的间隔,那你很有可能看到这个值是0,因为这个值只有在回收开始的时候开始刷新。Allocated Bytes/sec比较高一般都会伴随%Time in GC比例较高,接下来我们要着重看一下
#Gen 0 Collections, # Gen 1 Collections, and # Gen 2 Collections来分辨到底各个代的回收频率如何。第0代,第1代的回收相对都比较迅速,对应用程序的性能不会有大的影响,但是第2代的回收就要猛烈很多。一个比较健康的情况是10次1代回收发生1次2代回收。
有一种比较特殊的现象% Time in GC比例高但Allocated Bytes/sec却很小,这时候要注意第二代的回收情况,原因多数是因为很多object经常很容易的被提升至第二代,要查看这种问题可以通过CLR Profiler或者windbg来查看第二代的对象。
Debug Diagnostic Tool v1.2
Debug Diag是调试托管程序尤其是IIS Web Application的利器,1.2版本集成了性能分析脚本,使得很多复杂的问题变得一目了然,无所遁形。比如内存问题,当问题发生的时候,直接打开Debug Diag,切换到Processes选项卡,右键选择Create Full Userdump,然后在Advanced Analysis里面加载进来,选择性能分析脚本进行分析,各种问题以及相应的细节,解决方案都会以一个html格式的报告呈现给用户。
Windbg
Windbg目前是随Windows SDK一起安装的。通过windbg打开dump文件。加载相应的sos调试扩展库,这里要说一句,debugdiag的安装目录下包含了两个更加强大的调试扩展库,psscor2, psscor4分别对应调试.NET framework 2.0和4.0的程序。通过.load命令加载相应的调试扩展,之后开始我们的调试过程。
针对上面列举各种问题具体的调试方式在part 2会详细介绍。
参考文档
http://support.microsoft.com/default.aspx?scid=kb;EN-US;316775
http://support.microsoft.com/kb/872800
http://support.microsoft.com/default.aspx?scid=kb;EN-US;886385
http://msdn.microsoft.com/en-us/library/system.xml.serialization.xmlserializer(v=vs.80).aspx
上一篇: Java性能优化技巧汇总
下一篇: eclipse中的Java数组问题,求教