|
|
抓到一个mini dump,执行!clrstack,会提示如下错误:
*** WARNING: Unable to verify timestamp for mscorwks.dll
Failed to load data access DLL, 0x80004005
Verify that 1) you have a recent build of the debugger (6.2.14 or newer)
2) the file mscordacwks.dll that matches your version of mscorwks.dll is
in the version directory
3) or, if you are debugging a dump file, verify that the file
mscordacwks_<arch>_<arch>_<version>.dll is on your symbol path.
4) you are debugging on the same architecture as the dump file.
For example, an IA64 dump file must be debugged on an IA64
machine.
You can also run the debugger command .cordll to control the debugger's
load of mscordacwks.dll. .cordll -ve -u -l will do a verbose reload.
If that succeeds, the SOS command should work on retry.
If you are debugging a minidump, you need to make sure that your executable
path is pointing to mscorwks.dll as well.
解决办法很简单,把抓到的dump那个机器上的mscorwks拷过来,如在: c:\dumps\rules\debug目录下,那么执行:
0:037> .exepath+ c:\dumps\rules\debug
Executable image search path is: C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727;c:\dumps\rules\debug
然后重新加载符号表即可:.reload
对于XmlSerializer带来的内存占用过高,最终导致Out Of Memory的问题,参见以前这个链接: http://www.cnblogs.com/juqiang/archive/2008/01/15/1039936.html
(但是那篇文章中对于XmlSerializer构造方法的说明,是错误的。那段代码没有问题,有问题的是下面的)
首先看System.Xml.Serialization.XmlSerializer的构造方法,一共分为三大类:
public XmlSerializer(Type type) : this(type, (string) null)
public XmlSerializer(Type type, string defaultNamespace)
这两个方法,采用了上文引用的那篇文章的处理方式,应用了cache,这是正确的,不会造成内存占用过高。(上文引用那篇文章里面这个地方解释错了)
另一大类的方法是:
public XmlSerializer(XmlTypeMapping xmlTypeMapping),这个方法里面会产生一个tempAssembly,但是没有用缓存方式来处理。
有问题的一类方法是:
public XmlSerializer(Type type, Type[] extraTypes) : this(type, null, extraTypes, null, null, null, null)
public XmlSerializer(Type type, XmlAttributeOverrides overrides) : this(type, overrides, new Type[0], null, null, null, null)
public XmlSerializer(Type type, XmlRootAttribute root) : this(type, null, new Type[0], root, null, null, null)
public XmlSerializer(Type type, XmlAttributeOverrides overrides, Type[] extraTypes, XmlRootAttribute root, string defaultNamespace) : this(type, overrides, extraTypes, root, defaultNamespace, null, null)
public XmlSerializer(Type type, XmlAttributeOverrides overrides, Type[] extraTypes, XmlRootAttribute root, string defaultNamespace, string location, Evidence evidence)
前4个都最终调用了最后一个构造方法,里面产生了一个tempAssembly,也没有用缓存方式来护理。
我上次提到的那个问题,问题代码如下:
public static TReturn Convert<TReturn, TInput>(TInput input) where TReturn: class, new() where TInput: IProvisioningObject
{
using (MemoryStream stream = new MemoryStream())
{
new XmlSerializer(typeof(TInput)).Serialize((Stream) stream, input);
stream.Position = 0L;
XmlSerializer serializer = new XmlSerializer(typeof(TReturn), new XmlRootAttribute(input.GetType().Name));
return (TReturn) serializer.Deserialize(stream);
}
}
注意红色的代码,这里产生了一个tempAssembly,没有做缓存。这里的核心问题在于,tempAssembly不会被自动释放掉,除非appdomain被unload。
修正的方式类似于XmlSerializer的处理方式,采用一个二元Hashtable来做。代码修改如下:
1 private static TempXmlSerializerCache cache = new TempXmlSerializerCache();
2
3 public static TReturn Convert<TReturn, TInput>(TInput input)
4 where TReturn : class, new()
5 where TInput : IProvisioningObject
6 {
7 using (MemoryStream stream = new MemoryStream())
8 {
9 new XmlSerializer(typeof(TInput)).Serialize((Stream)stream, input);
10 stream.Position = 0L;
11
12 XmlSerializer serializer = cache[typeof(TReturn).ToString(), input.GetType().Name];
13 if (serializer == null)
14 {
15 lock (cache)
16 {
17 serializer = cache[typeof(TReturn).ToString(), input.GetType().Name];
18 if (serializer == null)
19 {
20 serializer = new XmlSerializer(typeof(TReturn), new XmlRootAttribute(input.GetType().Name));
21 cache.Add(typeof(TReturn).ToString(), input.GetType().Name, serializer);
22 }
23 }
24 }
25 return (TReturn)serializer.Deserialize(stream);
26 }
27 }
这里的二元Hashtable,是从XmlSerializer里面扒出来的,稍微修改了一下而已:
public class TempXmlSerializerCache
 {
private Hashtable cache = new Hashtable();

public void Add(string ns, object o, XmlSerializer serializer)
 {
XmlSerializerCacheKey key = new XmlSerializerCacheKey(ns, o);
lock (this)
 {
if (this.cache[key] != serializer)
 {
Hashtable hashtable = new Hashtable();
foreach (object obj2 in this.cache.Keys)
 {
hashtable.Add(obj2, this.cache[obj2]);
}
this.cache = hashtable;
this.cache[key] = serializer;
}
}
}

public XmlSerializer this[string ns, object o]
 {
get
 {
return (XmlSerializer)this.cache[new XmlSerializerCacheKey(ns, o)];
}
}
}

以及:
public class XmlSerializerCacheKey
 {
private string ns;
private object type;

public XmlSerializerCacheKey(string ns, object type)
 {
this.type = type;
this.ns = ns;
}

public override bool Equals(object o)
 {
XmlSerializerCacheKey key = o as XmlSerializerCacheKey;
if (key == null)
 {
return false;
}
return ((key.type == this.type) && (key.ns == this.ns));
}

public override int GetHashCode()
 {
return (((this.ns != null) ? this.ns.GetHashCode() : 0) ^ ((this.type != null) ? this.type.GetHashCode() : 0));
}
}
网站升级后,说有High CPU的问题。mstsc上去后,看了一下,果然如此,w3wp.exe的cpu几乎一直是满的,我的远程桌面操作也很慢。下载下来windbg,装好,抓了两个dump。首先!runaway一下,
0:022> !runaway
User Mode Time
Thread Time
22:8cc 0 days 0:17:15.238
23:4d8 0 days 0:15:20.936
15:898 0 days 0:09:15.316
24:c64 0 days 0:05:07.587
这是前4个线程,一共占用了47分钟左右,我们再看一下time的时间:
0:022> .time
Debug session time: Sun Mar 9 15:07:52.000 2008 (GMT+8)
System Uptime: 1 days 2:04:59.903
Process Uptime: 0 days 0:55:38.000
Kernel time: 0 days 0:00:07.000
User time: 0 days 0:48:41.000
cool,.time的时间和!runaway的几乎一致,再看看!threadpool的数字:
0:022> !threadpool
CPU utilization 100%
Worker Thread: Total: 5 Running: 5 Idle: 0 MaxLimit: 200 MinLimit: 2
Work Request in Queue: 15
Unknown Function: 6a2aa6fb Context: 0190c5b8
AsyncTimerCallbackCompletion TimerInfo@01969b30
AsyncTimerCallbackCompletion TimerInfo@01969428
Unknown Function: 6a2aa6fb Context: 0190ccb8
AsyncTimerCallbackCompletion TimerInfo@019698a8
AsyncTimerCallbackCompletion TimerInfo@01969c98
AsyncTimerCallbackCompletion TimerInfo@019f5778
AsyncTimerCallbackCompletion TimerInfo@01918658
AsyncTimerCallbackCompletion TimerInfo@01969788
Unknown Function: 6a2aa6fb Context: 0190cb38
AsyncTimerCallbackCompletion TimerInfo@019695d8
Unknown Function: 6a2aa6fb Context: 0190caf8
AsyncTimerCallbackCompletion TimerInfo@01969740
Unknown Function: 6a2aa6fb Context: 0190c178
Unknown Function: 6a2aa6fb Context: 0190cbf8
--------------------------------------
Number of Timers: 9
--------------------------------------
Completion Port Thread:Total: 2 Free: 2 MaxFree: 4 CurrentLimit: 2 MaxLimit: 200 MinLimit: 2
这些信息基本够了,罪魁祸首就是!runaway跑的那几个线程,我们随便抓两个来看看:
0:022> kb
ChildEBP RetAddr Args to Child
00005205 7a4c72c0 00000000 00000000 00000000 System_ni!System.Text.RegularExpressions.RegexCharClass.CharInClassRecursive(Char, System.String, Int32)+0x20
00005205 7a4b16e5 00000000 00000000 00000000 System_ni!System.Text.RegularExpressions.RegexInterpreter.Go()+0xc40
00005205 7a4b15c3 00000000 00000000 00000000 System_ni!System.Text.RegularExpressions.RegexRunner.Scan(System.Text.RegularExpressions.Regex, System.String, Int32, Int32, Int32, Int32, Boolean)+0xa5
00005205 7a4b14af 00000000 00000000 00000000 System_ni!System.Text.RegularExpressions.Regex.Run(Boolean, Int32, System.String, Int32, Int32, Int32)+0x103
ffffffff 7a4dc313 00000000 00000000 00000000 System_ni!System.Text.RegularExpressions.Regex.Match(System.String, Int32)+0x1f
ffffffff 7a4d1c39 00000000 00000000 00000000 System_ni!System.Text.RegularExpressions.RegexReplacement.Replace(System.Text.RegularExpressions.Regex, System.String, Int32, Int32)+0x47
0606f8ac 7a4d1b53 00000000 00000000 00000000 System_ni!System.Text.RegularExpressions.Regex.Replace(System.String, System.String, Int32, Int32)+0x99
05d8ed04 0ff965c7 00000000 00000000 00000000 System_ni!System.Text.RegularExpressions.Regex.Replace(System.String, System.String)+0x2f
05d8ed04 0ff96530 00000000 00000000 00000000 3rdparty_Framework!3rdparty.Framework.Text.HtmlHelper.RemoveHtmlComments(System.String)+0x67
00000000 0ff96477 00000000 00000000 00000000 3rdparty_Framework!3rdparty.Framework.Text.HtmlHelper.RemoveHtmlAndComments(System.String)+0x38
10c3e8a8 0ff96142 00000000 00000000 00000000 3rdparty_Framework!3rdparty.Framework.Components.AggregatedPost.set_Description(System.String)+0x2f
10c3e8a8 0ff95d8e 00000000 00000000 00000000 3rdparty_Framework!3rdparty.Framework.Data.DataHelper.LoadAggreatedPost(System.Data.IDataReader)+0x1e2
10c3e910 0ff95c02 00000000 00000000 00000000 3rdparty_Framework!3rdparty.Framework.Data.DatabaseObjectProvider.GetAggregatedPosts(Int32, Int32, Int32)+0x126
10c3e958 0ff9e4eb 00000000 00000000 00000000 3rdparty_Framework!3rdparty.Framework.Data.Cacher.GetPagedAggregatedPosts(Int32, Int32, Int32, 3rdparty.Framework.CacheDuration, Boolean)+0x14a
10c3e9b0 0ff9e364 00000000 00000000 00000000 3rdparty_Blog_Web!3rdparty.Blog.Web.WebForm1.BindData()+0x16b
10c3ec04 66f12980 00000000 00000000 00000000 3rdparty_Blog_Web!3rdparty.Blog.Web.WebForm1.Page_Load(System.Object, System.EventArgs)+0x44
10c3ec04 6628eff2 00000000 00000000 00000000 System_Web_RegularExpressions_ni!System.Web.Util.CalliHelper.EventArgFunctionCaller(IntPtr, System.Object, System.Object, System.EventArgs)+0x10
10c3ec04 6613cb24 00000000 00000000 00000000 System_Web_ni!System.Web.Util.CalliEventHandlerDelegateProxy.Callback(System.Object, System.EventArgs)+0x22
10c3ec04 6613cb70 00000000 00000000 00000000 System_Web_ni!System.Web.UI.Control.OnLoad(System.EventArgs)+0x64
10c3ec04 6614e14d 00000000 00000000 00000000 System_Web_ni!System.Web.UI.Control.LoadRecursive()+0x30
再看看其他线程的:
0:015> kb
ChildEBP RetAddr Args to Child
0000515e 7a4b16e5 00000000 00000000 00000000 System_ni!System.Text.RegularExpressions.RegexInterpreter.Go()+0x152b
0000515e 7a4b15c3 00000000 00000000 00000000 System_ni!System.Text.RegularExpressions.RegexRunner.Scan(System.Text.RegularExpressions.Regex, System.String, Int32, Int32, Int32, Int32, Boolean)+0xa5
0000515e 7a4b14af 00000000 00000000 00000000 System_ni!System.Text.RegularExpressions.Regex.Run(Boolean, Int32, System.String, Int32, Int32, Int32)+0x103
ffffffff 7a4dc313 00000000 00000000 00000000 System_ni!System.Text.RegularExpressions.Regex.Match(System.String, Int32)+0x1f
ffffffff 7a4d1c39 00000000 00000000 00000000 System_ni!System.Text.RegularExpressions.RegexReplacement.Replace(System.Text.RegularExpressions.Regex, System.String, Int32, Int32)+0x47
05f79678 7a4d1b53 00000000 00000000 00000000 System_ni!System.Text.RegularExpressions.Regex.Replace(System.String, System.String, Int32, Int32)+0x99
05db3568 0ff965c7 00000000 00000000 00000000 System_ni!System.Text.RegularExpressions.Regex.Replace(System.String, System.String)+0x2f
05db3568 0ff96530 00000000 00000000 00000000 3rdparty_Framework!3rdparty.Framework.Text.HtmlHelper.RemoveHtmlComments(System.String)+0x67
00000000 0ff96477 00000000 00000000 00000000 3rdparty_Framework!3rdparty.Framework.Text.HtmlHelper.RemoveHtmlAndComments(System.String)+0x38
0140f0d8 0ff96142 00000000 00000000 00000000 3rdparty_Framework!3rdparty.Framework.Components.AggregatedPost.set_Description(System.String)+0x2f
0140f0d8 0ff95d8e 00000000 00000000 00000000 3rdparty_Framework!3rdparty.Framework.Data.DataHelper.LoadAggreatedPost(System.Data.IDataReader)+0x1e2
0140f140 0ff95c02 00000000 00000000 00000000 3rdparty_Framework!3rdparty.Framework.Data.DatabaseObjectProvider.GetAggregatedPosts(Int32, Int32, Int32)+0x126
0140f188 0ff9e4eb 00000000 00000000 00000000 3rdparty_Framework!3rdparty.Framework.Data.Cacher.GetPagedAggregatedPosts(Int32, Int32, Int32, 3rdparty.Framework.CacheDuration, Boolean)+0x14a
0140f1e0 0ff9e364 00000000 00000000 00000000 3rdparty_Blog_Web!3rdparty.Blog.Web.WebForm1.BindData()+0x16b
0140f434 66f12980 00000000 00000000 00000000 3rdparty_Blog_Web!3rdparty.Blog.Web.WebForm1.Page_Load(System.Object, System.EventArgs)+0x44
0140f434 6628eff2 00000000 00000000 00000000 System_Web_RegularExpressions_ni!System.Web.Util.CalliHelper.EventArgFunctionCaller(IntPtr, System.Object, System.Object, System.EventArgs)+0x10
0140f434 6613cb24 00000000 00000000 00000000 System_Web_ni!System.Web.Util.CalliEventHandlerDelegateProxy.Callback(System.Object, System.EventArgs)+0x22
0140f434 6613cb70 00000000 00000000 00000000 System_Web_ni!System.Web.UI.Control.OnLoad(System.EventArgs)+0x64
0140f434 6614e14d 00000000 00000000 00000000 System_Web_ni!System.Web.UI.Control.LoadRecursive()+0x30
0140f434 6614d8e3 00000000 00000000 00000000 System_Web_ni!System.Web.UI.Page.ProcessRequestMain(Boolean, Boolean)+0x59d
其他两个线程和这两个类似。
偶感觉是在太幸运了,居然碰到这么一个simple的high cpu,哈哈!那看一下,为什么Regex.Replace会慢?运行一下!clrstack -a命令:
0:015> !clrstack -a
OS Thread Id: 0x898 (15)
ESP EIP
====================其他太长,省略……=============================
0140f034 7a4d1b53 System.Text.RegularExpressions.Regex.Replace(System.String, System.String)
PARAMETERS:
this = <no data>
input = <no data>
replacement = <no data>
0140f044 0ff965c7 Joycode.Framework.Text.HtmlHelper.RemoveHtmlComments(System.String)
PARAMETERS:
text = 0x05da9298
LOCALS:
<CLR reg> = 0x05db3568
<CLR reg> = 0x00000000
0x0140f044 = 0x00000000
====================其他太长,省略……=============================
哦,那个字符串的地址是0x05da9298,看看大小,是41K
0:015> !objsize 0x05da9298
sizeof(05da9298) = 41,680 ( 0xa2d0) bytes (System.String)
如果do这个字符串的话,我们能看到它是一个html文本。因为runaway和.time和threadpool的数据太明显,so,断定问题就在于replace的字符串太大或者频度太高造成的。
后来经过确认,RemoveHtmlComments代码中的Regex.Replace没有用,so,直接comment掉了,问题解决。
额外看一下别的东西:
| Address |
MT |
Priority |
Create Time |
Expires |
Last Updated |
Key Class Name
|
| 01b1c6cc |
6639d580 |
Normal |
03/09/2008 06:12:14 |
12/31/9999 23:59:59 0 |
0 |
dmachine/webroot/2/app_code System.Web.CachedPathData |
| 01b1f470 |
6639d580 |
Normal |
03/09/2008 06:12:14 |
12/31/9999 23:59:59 |
0 |
dmachine/webroot/2/global.asax System.Web.CachedPathData
|
| 01b3b3b0 |
6639d580 |
Normal |
03/09/2008 06:12:15 |
12/31/9999 23:59:59 |
03/09/2008 07:07:35 |
dmachine/webroot/2/systemmessages System.Web.CachedPathData |
这是aspnetcache的很少一部分的内容,值得注意的是,有很多cache的内容,过期时间是9999年。实际上,我们有很大理由可以判断,“天长日久”,w3wp会因为Out Of Memory而导致recycled问题,或者是访问量骤增并持续一段时间,会导致网站短期内崩掉。
再看一下heap:
0:015> !heapstat
Heap Gen0 Gen1 Gen2 LOH
Heap0 7291624 255092 5071228 13381296
Heap1 7178448 440428 4637748 14825768
Total 14470072 695520 9708976 28207064
Free space: Percentage
Heap0 84 12 6568 2011792 SOH: 0% LOH: 15%
Heap1 2588 12 392 7699728 SOH: 0% LOH: 51%
Total 2672 24 6960 9711520
大对象是最大的,,几乎等于gen0+gen1+gen2的总和。我们看看整体情况先:
0:015> .foreach(myobj {!dumpheap -short -min 85000}) {!objsize myobj}
sizeof(09c07bf0) = 689,900 ( 0xa86ec) bytes (System.Byte[])
sizeof(09cb02e0) = 616,976 ( 0x96a10) bytes (System.Byte[])
sizeof(09e2f2a8) = 616,976 ( 0x96a10) bytes (System.Byte[])
====================其他太长,省略……=============================
sizeof(0c89cf50) = 616,976 ( 0x96a10) bytes (System.Byte[])
sizeof(09d7c320) = 131,084 ( 0x2000c) bytes (System.Int32[])
====================其他太长,省略……=============================
sizeof(0bec8f60) = 1,114,124 ( 0x11000c) bytes (System.Int32[])
sizeof(0c092a68) = 1,114,124 ( 0x11000c) bytes (System.Int32[])
sizeof(09b8cb40) = 105,512 ( 0x19c28) bytes ()
sizeof(09bc6780) = 263,280 ( 0x40470) bytes ()
====================其他太长,省略……=============================
sizeof(0c42f080) = 4,644,560 ( 0x46ded0) bytes ()
sizeof(09b26468) = 262,164 ( 0x40014) bytes (System.String)
====================其他太长,省略……=============================
sizeof(0bda0f00) = 262,164 ( 0x40014) bytes (System.String)
sizeof(0bfd8f70) = 262,164 ( 0x40014) bytes (System.String)
byte数组:
0:015> !gcroot 0c398670
Note: Roots found on stacks may be false positives. Run "!help gcroot" for
more info.
Scan Thread 7 OSThread c60
Scan Thread 13 OSThread aec
Scan Thread 14 OSThread c68
Scan Thread 15 OSThread 898
ESP:140f088:Root: 05da5d54(System.Data.SqlClient.SqlDataReader)->
05da5a18(System.Data.SqlClient.SqlConnection)->
01d3a928(System.Data.SqlClient.SqlInternalConnectionTds)->
05b84ccc(System.Data.ProviderBase.DbConnectionPool)->
05b84f9c(System.Collections.Generic.List`1[[System.Data.ProviderBase.DbConnectionInternal, System.Data]])->
05b84fb4(System.Object[])->
05e03e00(System.Data.SqlClient.SqlInternalConnectionTds)->
05e03e9c(System.Data.SqlClient.TdsParser)->
05e03f3c(System.Data.SqlClient.TdsParserStateObject)->
0c398670(System.Byte[])
ESP:140f0e8:Root: 05da5d54(System.Data.SqlClient.SqlDataReader)->
05da5a18(System.Data.SqlClient.SqlConnection)->
01d3a928(System.Data.SqlClient.SqlInternalConnectionTds)->
05b84ccc(System.Data.ProviderBase.DbConnectionPool)->
05b84f9c(System.Collections.Generic.List`1[[System.Data.ProviderBase.DbConnectionInternal, System.Data]])->
05b84fb4(System.Object[])->
05e03e00(System.Data.SqlClient.SqlInternalConnectionTds)->
05e03e9c(System.Data.SqlClient.TdsParser)->
05e03f3c(System.Data.SqlClient.TdsParserStateObject)->
0c398670(System.Byte[])
其他的bye数组类似,都是在执行SqlDataReader的操作。(为啥这样,这个callstack看不到)
看int32数组:
0:015> !gcroot 09fd4e60
Note: Roots found on stacks may be false positives. Run "!help gcroot" for
more info.
Scan Thread 7 OSThread c60
====================其他太长,省略……=============================
Scan Thread 24 OSThread c64
ecx:Root: 01e51c1c(System.Text.RegularExpressions.RegexInterpreter)->
09fd4e60(System.Int32[])
esi:Root: 01e51c1c(System.Text.RegularExpressions.RegexInterpreter)->
09fd4e60(System.Int32[])
ESP:114ae598:Root: 01e51c1c(System.Text.RegularExpressions.RegexInterpreter)->
09fd4e60(System.Int32[])
ESP:114ae5ac:Root: 01e51c1c(System.Text.RegularExpressions.RegexInterpreter)->
09fd4e60(System.Int32[])
Scan Thread 26 OSThread 9ac
Scan Thread 27 OSThread 80c
这个是上面分析的正则表达式。
字符串的信息,就是上面看到的,都是html reply。针对这几种类型,我们做一个总结(在所有大对象中)
byte数组:4M,这个是sqldatareader读出来的数据
int32数组:5.8M,这个是因为regex和下面的string导致的
string:8.5M
另外:
上述所有module都是debug模式的,这里需要我们修改一下web.config,设置debug=false以及batch compilation为true,这样也能一定程度上提高performance。
没啥结论,就这么结束了。
0:015> !finddebugmodules
Loading all modules.
Searching for modules built in debug mode...
App_GlobalResources.-mywaxaw.dll not built release
App_Code.fqhaeca6.dll not built release
3rdparty.Framework.DLL not built release
App_global.asax.0y1c1i6p.dll not built release
3rdparty.Blog.Web.DLL not built release
freetextbox.DLL not built release
3rdparty.BlogML.DLL not built release
3rdparty.Extensibility.DLL not built release
3rdparty.Installation.DLL not built release
3rdparty.Plugins.Core.DLL not built release
3rdparty.Plugins.Examples.DLL not built release
3rdparty.Scripting.DLL not built release
3rdparty.Web.Controls.DLL not built release
App_Web_lab8my1c.dll not built release
App_Theme_Home.Clever.__mjadjg.dll not built release
App_Web_xppxn64l.dll not built release
App_Web_yxlgarwj.dll not built release
App_Web_a8wjdvdv.dll not built release
App_Web_ijqvv7ky.dll not built release
App_Web_xxkqxoen.dll not built release
App_Web_b25tsu-i.dll not built release
App_Web_kcb_q9mh.dll not built release
App_Theme_theme.elitecircle.jmea1orn.dll not built release
App_Web_site.master.9fb3aa03.6svjtsyu.dll not built release
App_GlobalResources.-mywaxaw.resources.dll not built release
App_Theme_theme.default.rggbez76.dll not built release
App_Web_post.aspx.9fb3aa03.ocnogniv.dll not built release
App_Web_k_ivm-xy.dll not built release
App_Web_category.aspx.9fb3aa03.lq_3jsov.dll not built release
App_Web_r8dwq35a.dll not built release
App_Web_archivemonth.aspx.9fb3aa03.jkc9sbgz.dll not built release
App_Web_gallery.aspx.9fb3aa03.vc-q4rsf.dll not built release
App_Web_contact.aspx.9fb3aa03.ayhi_kzk.dll not built release
App_Web_vocni5-f.dll not built release
App_Web_zgfpt_vo.dll not built release
Done Seaching
Paint事件被触发了多少次?比较简单的方式,我们自己做一个perfmon能用的counter。看代码:
1 private void button1_Click(object sender, EventArgs e) 2 { 3 4 if (!PerformanceCounterCategory.Exists("GDI+ Monitor")) 5 { 6 CounterCreationDataCollection ccdc = new CounterCreationDataCollection(); 7 8 CounterCreationData totalPaint = new CounterCreationData(); 9 totalPaint.CounterName = "# operator executed"; 10 totalPaint.CounterHelp = "Counts of OnPaint events called"; 11 totalPaint.CounterType = PerformanceCounterType.NumberOfItems32; 12 13 ccdc.Add(totalPaint); 14 15 PerformanceCounterCategory.Create("GDI+ Monitor", "Some counters for GDI+ objects", PerformanceCounterCategoryType.MultiInstance, ccdc); 16 } 17 } 好,点一下button1之后,我们就可以在perfmon中看到GDI+ Monitor这个category了,然后只有一个counter,就是# operator executed 然后在winform中增加这么一段:
paintcall = new PerformanceCounter();
paintcall.CategoryName = "GDI+ Monitor";
paintcall.CounterName = "# operator executed";
paintcall.MachineName = ".";
paintcall.ReadOnly = false;
 好了,代码里面可以用paintcall这个变量了。我们现在的需求是监视Paint被触发了多少次,那么可以在代码中这么写:
1 Graphics g2 = e.Graphics; 2 3 Bitmap bmp = new Bitmap(this.Width, this.Height); 4 Graphics g = Graphics.FromImage(bmp); 5 6 paintcall.Increment(); 7 8 Rectangle r = new Rectangle(0, 0, this.Width, this.Height); 9 g.FillRectangle(new LinearGradientBrush(r, Color.Red, Color.Blue, LinearGradientMode.BackwardDiagonal), r); 10 11 g2.DrawImage(bmp, new Point(0, 0)); 12 13 g.Dispose(); 14 g = null; 15 16 bmp.Dispose(); 17 bmp = null; 看上面第6行,这句会把paint call增加一。当然,其他方法有很多,这里不写了。 好,跑一下perfmon,然后把我们新增加的counter add上,嗯,可以看到当窗口无效的时候,计数器就增加了。 有一个小的细节,当窗口稍微改动大小的时候,你会发现Paint被调用了多次,这个实在很郁闷。so,稍微做点手脚:
1 private bool resize = false; 2 private void Form1_ResizeEnd(object sender, EventArgs e) 3 { 4 resize = false; 5 this.Invalidate(); 6 } 7 8 private void Form1_ResizeBegin(object sender, EventArgs e) 9 { 10 resize = true; 11 } 然后我们修改一下Paint事件的代码如下:
1 private void Form1_Paint(object sender, PaintEventArgs e) 2 { 3 if (true == resize) return; 4 5 Graphics g2 = e.Graphics; 6 7 Bitmap bmp = new Bitmap(this.Width, this.Height); 8 Graphics g = Graphics.FromImage(bmp); 9 10 paintcall.Increment(); 11 12 Rectangle r = new Rectangle(0, 0, this.Width, this.Height); 13 g.FillRectangle(new LinearGradientBrush(r, Color.Red, Color.Blue, LinearGradientMode.BackwardDiagonal), r); 14 15 g2.DrawImage(bmp, new Point(0, 0)); 16 17 g.Dispose(); 18 g = null; 19 20 bmp.Dispose(); 21 bmp = null; 22 } 注意第三行,我们判断,如果在resize过程中,那么就直接返回。这样,虽然界面像白板一样,但是却少了很多Paint操作,从性能上会好不少。
GDI+自身是否有leak,我们不去管,现在说的是.NET代码中的处理。 首先看我这个简单的helper
using System;
using System.Diagnostics;
using System.Text;
using System.Runtime.InteropServices;

 public class MemoryReport {
[DllImport("user32.dll", CharSet=CharSet.Auto)]
public static extern long GetGuiResources(IntPtr hProcess, long flag);

 public static string Write() {
Process p = Process.GetCurrentProcess();
ing hcount = p.HandleCount;
long psize = p.PrivateMemorySize64;
long vsize = p.VirtualMemorySize64;
long workset = p.WorkingSet64;
long gcsize = GC.GetTotalMemory(false);
int gdiobjs = (int)(GetGdiResources(p.Handle,0));
int userobjs = (int)(GetGdiResources(p.Handle,1));

return String.Format("Handle count:{0:N0},Private Bytes:{1:N0}K, Virtual Bytes:{2:N0}K, Working Set:{3:N0}K, GC Heap Size:{4:N0}K, GDI Objects:{5:N0}, User Objects:{6:N0}", hcount, psize>>10, vsize>>10, workset>>10, gcsize>>10, gdiobjs, userobjs);
}
}现在我们做一个winform程序,放一个button,在click里面写如下测试代码:
 for(int i=0;i<1000;i++) {
Bitmap b = new Bitmap("c:\\1.bif");
IntPtr ip = b.GetHbitmap();
Bitmap b2 = Bitmap.FromHbitmap(ip);
}

MessageBox.Show(MemoryReport.Write());观察每次的结果,Private Bytes/ Virtual Bytes/ Working Set基本是一个上涨的走向。但是我们感兴趣的是这几个地方: 1、Handle count:这个值一般会波动变化,在这里例子里面,你把程序运行起来后,用taskmgr来观察Handle Count一栏(默认的没有,需要你自己手工添加这个column),一般是100以下。然后点一下按钮,handle count会增长1000左右,再点几次,会在1000上下波动,不会继续增长。 2、GDI Objects:这个值每次会增加1000 3、你连续点10次这个button,嘣!程序crash了。。。如果看dump里面的异常,会是什么bitmap的一个构造方法的parameter不正确。 4、GC Heap Size很小很小,我这里是2M。但是virtual size很大。 对于1,为什么这样,我不清楚;对于2,原因在于GetHbitmap返回的是一个Unmanged resource,GC不会回收(即使你使用了GC.Collect()这个值也不会下降的);对于3,OS默认的每个process的GDI objects上限为10000个,我们代码中是循环了1000次,所以如果你点了10次button,程序就会完蛋。对于4,说明leak的资源是unmanged resource,so,gc heap看起来很乖。 那么,如何修复上面的问题2?既然是unmanged resource,我们就要从unmanged找起。
嗯,再运行一次,好了!GDI objects稳定了,再也没有变化过。 不过,我们修改一下循环计数器,到5000吧,然后观察Handle count,波动的比较厉害,内存相关的三组数值也稍有变化。好,我们再修改一次程序
重新run一次,嗯,这个世界终于清静了,handle count/gdi resource/ mem size都很平稳。 so,总结一下,对于类似上面的、可能被反复调用的type,如GDI+ obj,可以考虑使用完毕后立刻Dispose,这样可以被GC提早回收。对于返回一个IntPtr的方法,要仔细看,是不是需要再call win32里面对应的Delete方法。 对于绝大多数GDI+ obj,我们只需要DeleteObject即可,但是对于icon,我记着是另外一个函数,有兴趣的可以在msdn上查一下。
1、这本书对于初学者没有太大用处 2、这本书对于眼中只有架构、自己不写程序的、鄙视代码的人没有用处 3、这本书对于非微软的人用处不算太大,你不知道ms内部的数据结构,你没有private symbols。 4、这本书对于微软的人用处不算太大,搞debug的就那么几号人
5、这本书对于在客户现场被骂的狗血喷头的、自己即使架了.NET IDE也不知道如何找出问题的人很有用处
如果你是第五种人,疯狂购买吧!
此文是偶的偶像和哥们的,转过来,替他做一下宣传。偶会买10本,9本送人,有要的,现在报名。
(原来公司大量的COM+和.NET的case,都是熊老大做的)
Windows 高效排错 《Windows 高效排错》 可以在CSDN读书频道预览了
读书频道的排版有些问题,看起来不是很舒服。如果想看PDF的,可以在这里下载
纸板书籍估计在11月中下旬面世
现在在China-pub, dearbook等网上书店已经有介绍,地址分别
您的看法非常重要。如果你看过这本书的PDF,了解本书的内容和潜在读者,请您在上面的链接中留下你的观点,便于其它不了解这本书的人选择。
如果您通过这本书介绍的方法解决过实际的问题,非常希望您能够分享您的经验。您可以发邮件到 eparg@msn.com。如果您的分享能够帮助其它读者,我非常希望能够送书给您作为答谢。
如果您对书中所述内容有疑惑,也欢迎您写信来讨论。我会尽快回复。如果我们之间的讨论对其他读者有帮助,我也会放到网上,同时送书给您作为答谢。
===
上面是官腔了。我个人想说的话其实是:
1) 书前言里面说,"2007年年初我不再做技术支持。希望这本书能帮我记录下这一段美好的经历",其中最让我难忘的就是跟xiao,elan和leo站在水房门口讨论case的日子 2) 常来我blog踩的同学们,你们要书的话把地址留给我。等我拿到了我给大家发。(估计要等一段时间。出版社就给我6本。剩下的我争取去批发) 3) 跟我一起做过case的,如果你需要的话也把地址留给我 4) 多卖一本书,我可以多拿3块钱不到。整本书的稿费还不够在上海买半个车牌,或者买个厕所。我一点也不关心字面上的销量。我之所以花那么多时间来做这个亏本生意,目的只有一个,就是希望跟喜欢调试的人分享其中的过程,让真正需要这方面文章的人有所参考。所以如果你身边有这本书的潜在读者,劳烦你推荐一下。
背景:两个CustomControl,一个叫做MyPanel,是一个M*N的格子,一个叫做Ball,是一个球。球可以放在panel的格子中,可以鼠标进行drag & drop.
直接call很简单,没有任何问题。但是通过reflection,碰到了郁闷的事情。虽然最终搞定了,但是要看过InvokeMember的代码才能知道为什么。
代码:
Object panel = null; Type tpanel = null;
private void ReflectionForm_Load(object sender, EventArgs e) { Assembly asm = Assembly.LoadFile(@"D:\test\WindowsApplication5\MyLibrary\bin\Debug\mylibrary.dll"); tpanel = asm.GetType("MyLibrary.MyPanel"); panel = tpanel.InvokeMember("", BindingFlags.CreateInstance, null, this, new object[] { });
(panel as Control).Location = new Point(100, 100); this.Controls.Add(panel as Control); }
出错的地方:
tpanel.InvokeMember("AddBall", BindingFlags.Instance | BindingFlags.Public|BindingFlags.InvokeMethod, null, panel, new object[] { obj });
如果上面的panel是object类型,那么一切正常。如果是Control类型,即,我声明为:Control panel,然后 object obj = tpanel.InvokeMember("", BindingFlags.CreateInstance, null, this, new object[] { }); panel = object as Control; panel.Location = new Point(100,100); this.Controls.Add(panel);
那么上面的InvokeMember就会提示我:Method not found!如果我修改BindingFlags,去掉Method,那么没有MethodNotFound的exception,但是会有“请使用InvokeMethod,Set/Get Field之类的提示”。如果加上,就是MethodNotFound,真是faint!
google了一些,貌似有些人碰到过,一个post上面说,arguments的类型必须要和method的类型完全一致才可以。我想,如果Panel当作control来用,那么control -> Ball类型会无法转换的,而object -> Ball是允许的。
哪位高手帮忙解释一下?谢谢!
(不好意思,置顶几天)
微软招聘SQL专家,如果您认为在下述方面有专长,请积极报名:
1、工作地点:上海; 2、很强的微软技术背景和产品熟悉度; 3、很强的客户沟通能力; 4、熟悉 SQLServer,熟悉Reporting Service开发、维护 5、熟悉 BI理念、产品,如果有 SharePoint Portal Server 2003 或 Office SharePoint Server 2007 实战经验将优先考虑;
请把个人简历发给我:juqiang1975@msn.com
这个回复是针对我上一个post大家的热情回复而写的。安全问题容易被人忽视,倒不是说不重视,而是自己苦心研究出来的东西,很容易从其他人另一个角度被攻破。
尤其在.NET上,这种问题之所以出现,我认为是大家对于.NET的视角不同造成的。.net让快速开发确实达到了,但是也让程序员丧失了“警惕性”。
那位兄弟说的破解成本确实是问题。成本太高,破解没有意义了。但是致命的是,我接触到的这些国内ERP厂商,对于序列号控制这部分,实在是太脆弱了,脆弱到破解成本几乎就是0 - 虽然后面有大型应用、技术支持这个说法。
我印象中看卡巴斯基的书上说,最好的“提高破解难度”的方法是在native code中进行SEH处理,把核心代码放到catch里面。据说这样会让cracker晕倒………………
至于加壳与否,我认为只要想做,有足够的耐心,把内存都dump出来,象dos下面破解,一样的。
业界对于Java/.NET程序的一个批评就是其安全性。由于IL的特点,各种reflector很容易把代码搞出来。混淆器,貌似一个很常用的功能吧?今天看某个软件,与我们的应用有些类似,所以想借鉴一下。安装好之后,发现有一个License Manager,两个按钮,一个是生成申请信息,一个是导入序列号。(通用的做法,我们的应用也是这么做的) 用reflector打开后,ooh,大概80%的信息都被混淆掉了。field/method的名称,都成了稀奇古怪的文字。还有,我的File Disassembler plug-in居然不能用了,于是只能把所有的method/field信息都paste到notepad上面。(下面所有代码,出于众所周知的原因,我又做了第二次“混淆”,所有的变量/方法名称,都是随意写的)
映入眼帘的第一个method是.ctor,我们知道这个是缺省的构造方法。看代码,如下:
public SomeConstructor() { this.Abcdef123456(); }
上面的方法就是被混淆过了,虽然没意义,但是我们能够“猜测”出来! 进入上面的Abcdef123456方法,如下:
private void Abcdef123456() { this.fedcba654321 = new Container(); Button button1 = new Button(); Button button2 = new Button(); ... }
好熟悉吧!这就是InitializaComponent方法,查找所有的Abcdef123456,统一替换为这个新名字就可以了。同样的道理,通过看程序运行时候的界面、在查找代码,能更正90%以上的混淆名称,变成一个清晰的表达(虽然不一定和源代码相同)
继续看reflector的结果,有两个分别是Button1_Click和Button2_Click方法。 先看第一个,里面有段代码如下: byte[] buffer1 = MD5.Encrypt(this.B33498573V + '\v' + this.J39475SDf, false); stream1.Write(buffer1, 0, buffer1.Length); byte[] buffer2 = MD5.Encrypt(new UnicodeEncoding().GetBytes(text1)); stream1.Flush(); if (SomeCondition == true) { SaveCPUandDISKInfo(this.B33498573V.Text , this.J39475SDf.Text); } InfoShow("\u751f\u6210");
看起来似乎很麻烦?晕,居然还有MD5,还有一些复杂的逻辑判断。怎么搞?分析那个MD5怎么加密的吗?没意义! 首先我们看最后面的InfoShow,我们都知道,这是Unicode编码方式下的文字,简单的用Console.WriteLine出来即可,上面的两个字是“生成”,于是我们猜测,这个和序列号可能相关。很自然的,我们会想到,对于注册的话,会有“成功”或者“失败”。前者转换为unicode是:\u6210\u529f。好吧,我们搜索“\u6210\u529f”。幸运!我们在Button2_Click方法中找到了这个字符串。这个方法代码如下: if (MD5.CheckRegisterKey(this.FileWillImported.Text)){ File.Copy(); ReadRegistry(); GetCPUandDISKinfoFromDatabase(); if(ResultFromUpline() == true){ SetCPUandDISKinfoIntoDatabase(); } InfoShow("\u6210\u529f"); } else{ InfoShow("\u975e\u6cd5"); }
我们看到上面的代码,进行了一些复杂的操作,包括什么MD5/SHA/RSA等,最后成功才显示出来一个"\u6210\u529f"。作为游戏,我们继续寻找“失败”的unicode bytes,ooh,没有找到!但是从上面的代码我们可以看出,最后的else是有问题的。同样的Console.WriteLine一下,显示出来的是“非法”这两个汉字。
下面的工作就简单了,ildasm /out方式,把IL弄出来,直接调用SetCPUandDISKinfoIntoDatabase方法,屏蔽掉所有的判断即可。 再用reflector看,该文件并没有任何对于非托管代码的refrence,安装目录下面也没有任何非托管dll,所以基本确定就是这样子了。
总结一下,我们用到的方法,都是20年前的方法。找到关键字符串,查找上下文可疑代码,把相关的判断信息都屏蔽掉,工作就完成了。那些花哨的各种“非对称算法”,没有任何意义。因为在加密解密工作中,流程的转向是重要的,流程本身,是无意义的。这里没有我们10年前看到的那些让人目眩的反侦查手法。
前段时间帮一朋友看过一个.net的应用,用usb key来做的,调用了自己写的Win32 API。同样的弊病,写了非常复杂的加密、解密、反跟踪、反调试处理,可惜的是,这一切都是在Win32中作的,.NET代码只是简单的做了CALL。屏蔽掉了200多行的IL,直接return true,唔,这个世界清净了………………
(再次声明,本文纯属对于.NET简单混淆的一个讨论,文中对于该软件的任何可以识别的特征,都进行了“混淆”) (对于破解和反汇编,可以看卡巴斯基本人写的那本黑客反汇编揭秘,本人对于汇编纯属文盲)
反过头来,我们考虑本文标题的内容,.NET或者J2EE代码应该如何保护?混淆是必要的,但太脆弱。反跟踪、反调试,我记着有高手在作,但是我不认为这能解决根本问题。不管多复杂的加密手段,被一些低级的代码调用的时候,总能很快的被搞定,如上面分析的那些代码。
貌似PageParser的大量调用,会导致大量的小size的dll、众多memory hole的出现。得到eparg的指教,越来越感觉是这个样子。据joycode老大说,只有2.0这个样子,只有特定的这一个app这个样子。
准备写一个.net 2的测试程序,看这个hole是不是确实很明显?
(详情等下文)
Session,Session,Session!(请耐心阅读………………)
地球人都知道,asp.net中有三种方式存放我们的session objects。In Proc模式,在cache中存放对象。StateServer在State Service中存放,最后一种是存放在SQL Server中。对于In Proc模式,太多的session对象,意味着高内存占用;对于后两者,意味着序列化和反序列化的性能损失。 Session存放的东西太多,不一定意味着性能的问题,但这依赖于你往session里面存放的东西。 让我们假设一个场景,你开发了一个网上商店,有很少的人在用,所以你只用了一台web server,使用了In Proc模式,你的程序对每个用户的订单,都存放了一些dataset。 突然,你的程序出名了!一个大公司来找你,于是,你增加了N台web server,同时把Session State也修改成了SQL Server模式。
问题来了! asp.net进程,内存占用很好(800MB – 1GB),有时候会提示OutOfMemory异常,或者提示.net进程被recycled了。dump的大小为1,473,913 bytes,所以,大概就是1.4G。首先,我们看看托管堆上面都有些啥东西?
0:023> !eeheap -gc Number of GC Heaps: 2 ------------------------------ Heap 0 (0x000b7198) generation 0 starts at 0x022104d4 generation 1 starts at 0x022037c0 generation 2 starts at 0x02170030 ephemeral segment allocation context: none segment begin allocated size 0x2170000 0x2170030 0x224a4e0 0xda4b0(894,128) Large object heap starts at 0x0a170030 segment begin allocated size 0x0a170000 0x0a170030 0x0acf0b20 0x00b80af0(12,061,424) 0x0d490000 0x0d490030 0x0e3d2450 0x00f42420(16,000,032) 0x12010000 0x12010030 0x12f52460 0x00f42430(16,000,048) 0x13010000 0x13010030 0x13f52460 0x00f42430(16,000,048) 0x15010000 0x15010030 0x15f52460 0x00f42430(16,000,048) 0x1a010000 0x1a010030 0x1af52460 0x00f42430(16,000,048) … 0x71ca0000 0x71ca0030 0x72be2470 0x00f42440(16,000,064) 0x748b0000 0x748b0030 0x757f2470 0x00f42440(16,000,064) 0x7d0e0000 0x7d0e0030 0x7d881250 0x007a1220(8,000,032) Heap Size 0x2d5b4e10(760,958,480) ------------------------------ Heap 1 (0x000ede88) generation 0 starts at 0x06249b58 generation 1 starts at 0x0623e190 generation 2 starts at 0x06170030 ephemeral segment allocation context: none segment begin allocated size 0x6170000 0x6170030 0x6283b64 0x113b34(1,129,268) Large object heap starts at 0x0b170030 segment begin allocated size 0x0b170000 0x0b170030 0x0b9f1c90 0x00881c60(8,920,160) 0x0c3e0000 0x0c3e0030 0x0d322460 0x00f42430(16,000,048) 0x0e490000 0x0e490030 0x0f3d2460 0x00f42430(16,000,048) 0x11010000 0x11010030 0x11f52460 0x00f42430(16,000,048) 0x14010000 0x14010030 0x14f52450 0x00f42420(16,000,032) 0x16010000 0x16010030 0x16f52480 0x00f42450(16,000,080) 0x17010000 0x17010030 0x17b81b60 0x00b71b30(12,000,048) 0x18010000 0x18010030 0x18b81b60 0x00b71b30(12,000,048) 0x19010000 0x19010030 0x19f52460 0x00f42430(16,000,048) 0x1b010000 0x1b010030 0x1bf52460 0x00f42430(16,000,048) … 0x61010000 0x61010030 0x61f52470 0x00f42440(16,000,064) 0x62db0000 0x62db0030 0x63cf2470 0x00f42440(16,000,064) 0x657e0000 0x657e0030 0x66722470 0x00f42440(16,000,064) 0x685c0000 0x685c0030 0x69502470 0x00f42440(16,000,064) 0x6e110000 0x6e110030 0x6ec81b70 0x00b71b40(12,000,064) 0x72ca0000 0x72ca0030 0x73be2470 0x00f42440(16,000,064) 0x77ff0000 0x77ff0030 0x78f32470 0x00f42440(16,000,064) 0x7e0e0000 0x7e0e0030 0x7f022470 0x00f42440(16,000,064) Heap Size 0x286a4124(678,052,132) ------------------------------ GC Heap Size 0x55c58f34(1,439,010,612)
!eeheap –gc告诉我们两个事情: a) the GC Heap大小在1.4G左右,和上面的总内存占用非常接近。这说明大部分内存占用,都在托管堆上。 b) 大部分内存占用都在LargeObjects上面(>85000bytes的东西)
很自然的,我们要看看在heap里面的LO到底都是啥东西? 0:023> !dumpheap -min 85000 -stat Using our cache to search the heap. Statistics: MT Count TotalSize Class Name 0x000eda20 1 920,144 Free 0x01b2209c 33 132,000,528 System.Object[] 0x01b226b0 163 1,304,001,956 System.Int32[] Total 197 objects, Total size: 1,436,922,628 大部分内存被Int32数组占用了,还有132M被一个对象数组占用了。注意啊!这里的132M和那个1.3G,都只是object本身的大小,而不包括存储在Object数组里面的对象的大小。如果要看详细情况,我们需要跑一下!objsize才可以。 0:023> !dumpheap -min 85000 Using our cache to search the heap. Address MT Size Gen 0x0b170030 0x000eda20 920,144 -1 Free 0x0a920210 0x01b2209c 4,000,016 -1 System.Object[] 0x247b1250 0x01b2209c 4,000,016 -1 System.Object[] 0x277b1250 0x01b2209c 4,000,016 -1 System.Object[] 0x287b1250 0x01b2209c 4,000,016 -1 System.Object[] … 0x51af0030 0x01b226b0 8,000,012 -1 System.Int32[] 0x52291250 0x01b226b0 8,000,012 -1 System.Int32[] 0x53b40950 0x01b226b0 8,000,012 -1 System.Int32[] 0x56620030 0x01b226b0 8,000,012 -1 System.Int32[] 0x57620030 0x01b226b0 8,000,012 -1 System.Int32[] 0x5a440030 0x01b226b0 8,000,012 -1 System.Int32[] 0x5abe1250 0x01b226b0 8,000,012 -1 System.Int32[] 0x5c660030 0x01b226b0 8,000,012 -1 System.Int32[] 0x5ce01250 0x01b226b0 8,000,012 -1 System.Int32[] 0x61010030 0x01b226b0 8,000,012 -1 System.Int32[] 0x617b1250 0x01b226b0 8,000,012 -1 System.Int32[] 0x62db0030 0x01b226b0 8,000,012 -1 System.Int32[] 0x63551250 0x01b226b0 8,000,012 -1 System.Int32[] 0x657e0030 0x01b226b0 8,000,012 -1 System.Int32[] 0x65f81250 0x01b226b0 8,000,012 -1 System.Int32[] 注意Gen这一列,这些object都没有被GC,为啥?随便找一个,看看吧! 0:023> !gcroot 0x72ca0030 Scan Thread 16 (0xbd8) ESP:1a3f5e0:Root:0x6280d38(System.Web.HttpContext)-> 0x62809ec(System.Web.Hosting.ISAPIWorkerRequestInProcForIIS6)-> 0x61b7030(System.Web.HttpWorkerRequest/EndOfSendNotification)-> 0x61b47d4(System.Web.HttpRuntime)-> 0x61b4ca0(System.Web.Caching.CacheMultiple)->0x61b4cc4(System.Object[])-> 0x61b4cdc(System.Web.Caching.CacheSingle)-> 0x61b4dac(System.Web.Caching.CacheExpires)->0x61b4ff8(System.Object[])-> 0x61b5ab8(System.Web.Caching.ExpiresBucket)-> 0x222e63c(System.Web.Caching.ExpiresEntry[])-> 0x220292c(System.Web.Caching.CacheEntry)-> 0x22028fc(System.Web.SessionState.InProcSessionState)-> 0x2202690(System.Web.SessionState.SessionDictionary)-> 0x220273c(System.Collections.Hashtable)-> 0x2202770(System.Collections.Hashtable/bucket[])-> 0x2202810(System.Collections.Specialized.NameObjectCollectionBase/NameObjectEntry)-> 0x72ca0030(System.Int32[]) Scan Thread 20 (0x89c) Scan Thread 22 (0xa5c) Scan HandleTable 0xdc9d8 Scan HandleTable 0xea6e8 Scan HandleTable 0x1531e8 看这条链,我们发现Int32被Cache root了(看GC的原理就知道这句话啥意思了),看上面的InProcSessionState,我们知道现在是InProc模式。 继续看…… 0:023> !dumpaspnetcache -stat Going to dump the ASP.NET Cache. MT Count TotalSize Class Name 0x0211cc9c 1 20 System.Web.Security.FileSecurityDescriptorWrapper 0x020c242c 2 56 System.Web.UI.ParserCacheItem 0x0206c66c 5 260 System.Web.Configuration.HttpConfigurationRecord 0x0c2e7014 1 316 System.Web.Mobile.MobileCapabilities 0x79b94638 4 376 System.String 0x0c2eaeb4 151 7,248 System.Web.SessionState.InProcSessionState Total 164 objects, Total size: 8,276 好,我们找到了151个Session对象。 0:023> .foreach (obj {!dumpheap -mt 0x0c2eaeb4 -short}){!objsize ${obj}} sizeof(0x22028fc) = 8,000,812 (0x7a152c) bytes (System.Web.SessionState.InProcSessionState) sizeof(0x2202a10) = 8,000,812 (0x7a152c) bytes (System.Web.SessionState.InProcSessionState) sizeof(0x2202cfc) = 8,000,812 (0x7a152c) bytes (System.Web.SessionState.InProcSessionState) sizeof(0x2202fe8) = 8,000,812 (0x7a152c) bytes (System.Web.SessionState.InProcSessionState) sizeof(0x22032d4) = 8,000,812 (0x7a152c) bytes (System.Web.SessionState.InProcSessionState) sizeof(0x22035c0) = 8,000,812 (0x7a152c) bytes (System.Web.SessionState.InProcSessionState) sizeof(0x2203a38) = 8,000,812 (0x7a152c) bytes (System.Web.SessionState.InProcSessionState) sizeof(0x2203d24) = 8,000,812 (0x7a152c) bytes (System.Web.SessionState.InProcSessionState) sizeof(0x2204010) = 8,000,812 (0x7a152c) bytes (System.Web.SessionState.InProcSessionState) … sizeof(0x6248080) = 12,000,772 (0xb71e04) bytes (System.Web.SessionState.InProcSessionState) sizeof(0x6248334) = 12,000,772 (0xb71e04) bytes (System.Web.SessionState.InProcSessionState) sizeof(0x62485e8) = 12,000,772 (0xb71e04) bytes (System.Web.SessionState.InProcSessionState) sizeof(0x624a84c) = 8,000,812 (0x7a152c) bytes (System.Web.SessionState.InProcSessionState) sizeof(0x624d7b8) = 8,000,812 (0x7a152c) bytes (System.Web.SessionState.InProcSessionState) sizeof(0x6250724) = 8,000,812 (0x7a152c) bytes (System.Web.SessionState.InProcSessionState) sizeof(0x6253690) = 8,000,812 (0x7a152c) bytes (System.Web.SessionState.InProcSessionState) sizeof(0x62565fc) = 8,000,812 (0x7a152c) bytes (System.Web.SessionState.InProcSessionState) sizeof(0x6259568) = 8,000,812 (0x7a152c) bytes (System.Web.SessionState.InProcSessionState) sizeof(0x625c4d4) = 8,000,812 (0x7a152c) bytes (System.Web.SessionState.InProcSessionState) sizeof(0x625f440) = 8,000,812 (0x7a152c) bytes (System.Web.SessionState.InProcSessionState) sizeof(0x62623ac) = 8,000,812 (0x7a152c) bytes (System.Web.SessionState.InProcSessionState) sizeof(0x6265318) = 8,000,812 (0x7a152c) bytes (System.Web.SessionState.InProcSessionState) sizeof(0x6268284) = 8,000,812 (0x7a152c) bytes (System.Web.SessionState.InProcSessionState) sizeof(0x626b1b8) = 12,000,772 (0xb71e04) bytes (System.Web.SessionState.InProcSessionState) 看到了!151个session对象,每个拿到了8-12M左右的数据。就是这个鸟玩意搞的内存有问题啦! 随便找一个,看看什么在里面??? 0:023> !do 0x626b1b8 Name: System.Web.SessionState.InProcSessionState MethodTable 0x0c2eaeb4 EEClass 0x0c1c5660 Size 48(0x30) bytes GC Generation: 0 mdToken: 0x02000132 (c:\windows\assembly\gac\system.web\1.0.5000.0__b03f5f7f11d50a3a\system.web.dll) FieldDesc*: 0x0c2eae0c MT Field Offset Type Attr Value Name 0x0c2eaeb4 0x40009f2 0x4 CLASS instance 0x06269f74 dict 0x0c2eaeb4 0x40009f3 0x8 CLASS instance 0x00000000 staticObjects 0x0c2eaeb4 0x40009f4 0xc System.Int32 instance 20 timeout 0x0c2eaeb4 0x40009f5 0x18 System.Boolean instance 0 isCookieless 0x0c2eaeb4 0x40009f6 0x10 System.Int32 instance 0 streamLength 0x0c2eaeb4 0x40009f7 0x19 System.Boolean instance 0 locked 0x0c2eaeb4 0x40009f8 0x1c VALUETYPE instance start at 0x0626b1d4 utcLockDate 0x0c2eaeb4 0x40009f9 0x14 System.Int32 instance 1 lockCookie 0x0c2eaeb4 0x40009fa 0x24 VALUETYPE instance start at 0x0626b1dc spinLock 每个InProcSessionState对象都有一个叫做_entriesArray的dict成员,装有实际的session对象列表。就是上面偏移为0x04的那行。 0:023> !do 0x06269f74 Name: System.Web.SessionState.SessionDictionary MethodTable 0x0c2e0c54 EEClass 0x0c1c1308 Size 44(0x2c) bytes GC Generation: 0 mdToken: 0x0200013b (c:\windows\assembly\gac\system.web\1.0.5000.0__b03f5f7f11d50a3a\system.web.dll) FieldDesc*: 0x0c2e0b30 MT Field Offset Type Attr Value Name 0x0206b338 0x4000a8b 0x24 System.Boolean instance 0 _readOnly 0x0206b338 0x4000a8c 0x4 CLASS instance 0x06269fb8 _entriesArray 0x0206b338 0x4000a8d 0x8 CLASS instance 0x06269fa0 _hashProvider 0x0206b338 0x4000a8e 0xc CLASS instance 0x06269fac _comparer 0x0206b338 0x4000a8f 0x10 CLASS instance 0x0626a020 _entriesTable 0x0206b338 0x4000a90 0x14 CLASS instance 0x00000000 _nullKeyEntry 0x0206b338 0x4000a91 0x18 CLASS instance 0x00000000 _keys 0x0206b338 0x4000a92 0x1c CLASS instance 0x00000000 _serializationInfo 0x0206b338 0x4000a93 0x20 System.Int32 instance 4 _version 0x0c2e0c54 0x4000a0f 0x25 System.Boolean instance 1 _dirty 0x0c2e0c54 0x4000a0e 0 CLASS shared static s_immutableTypes >> Domain:Value 0x000dad08:NotInit 0x00104f30:0x021be0dc << 同样,还是看_entriesArray,偏移为0x04的那行 0:023> !do 0x06269fb8 Name: System.Collections.ArrayList MethodTable 0x79ba2ee4 EEClass 0x79ba3020 Size 24(0x18) bytes GC Generation: 0 mdToken: 0x02000100 (c:\windows\microsoft.net\framework\v1.1.4322\mscorlib.dll) FieldDesc*: 0x79ba3084 MT Field Offset Type Attr Value Name 0x79ba2ee4 0x4000362 0x4 CLASS instance 0x06269fd0 _items 0x79ba2ee4 0x4000363 0xc System.Int32 instance 3 _size 0x79ba2ee4 0x4000364 0x10 System.Int32 instance 3 _version 0x79ba2ee4 0x4000365 0x8 CLASS instance 0x00000000 _syncRoot 哦,Name是ArrayList,好,我们继续看0x04对应的那个_items吧! 0:023> !do -v 0x06269fd0 Name: System.Object[] MethodTable 0x01b2209c EEClass 0x01b22018 Size 80(0x50) bytes GC Generation: 0 Array: Rank 1, Type CLASS Element Type: System.Object Content: 16 items ------ Will only dump out valid managed objects ---- Address MT Class Name 0x0626a81c 0x0206b784 System.Collections.Specialized.NameObjectCollectionBase/NameObjectEntry 0x0626a82c 0x0206b784 System.Collections.Specialized.NameObjectCollectionBase/NameObjectEntry 0x0626a83c 0x0206b784 System.Collections.Specialized.NameObjectCollectionBase/NameObjectEntry ---------- 对于每个对象,我们打印出0x04位置的Name和0x08位置的Size。 0:023> .foreach (obj {!do -v 0x06269fd0 -short}){.echo ***;!do poi(${obj}+0x4);!do poi(${obj}+0x8);!objsize ${obj}} *** String: somestring String: this is a string i stored in session scope sizeof(0x626a81c) = 160 ( 0xa0) bytes (System.Collections.Specialized.NameObjectCollectionBase/NameObjectEntry) *** String: alargeintarray Name: System.Int32[] MethodTable 0x01b226b0 EEClass 0x01b22638 Size 8000012(0x7a120c) bytes GC Generation: 3 Array: Rank 1, Type System.Int32 Element Type: System.Int32 Content: 2,000,000 items sizeof(0x626a82c) = 8,000,076 (0x7a124c) bytes (System.Collections.Specialized.NameObjectCollectionBase/NameObjectEntry) *** String: sometimesbig Name: System.Object[] MethodTable 0x01b2209c EEClass 0x01b22018 Size 4000016(0x3d0910) bytes GC Generation: 3 Array: Rank 1, Type CLASS Element Type: System.Object Content: 1,000,000 items sizeof(0x626a83c) = 4,000,076 (0x3d094c) bytes (System.Collections.Specialized.NameObjectCollectionBase/NameObjectEntry) 到这里,我们看到,一个很小的字符串:this is a string i stored in session scope。在Session的 somestring里面。然后是在Session的alargeintarray有一个8M的Int32数组,最后,是一个4M的sometimesbig在session的sometimesbig。 下面如何解决?就看我们的业务逻辑和代码实现啦!和偶无关了,嗬嗬!
(序 & 跋)
此文及后面的系列,都是从tess老大那里翻译过来的。一直和GTEC的老牛们作CASE(此句有误,一直提CASE,等老牛们提供答案),算是粘到了一点仙气。偶一直比较懒,所以以前精心抄袭的文章,今天再次精心作序于此,希望对各位挣扎于现实与理想的各位,共享,共勉。tess老大的文章国内似乎有人翻译过,但偶个人观点,不看好,因为好东西都没了哦。
不是我不舍得,意思是,我的post里面基本上没有link,但是从google上都能搞到,如tess老大的系列debug文章。写程序的人,用好google应该是第一要素啊,哇哈哈哈!(百度除外,偶鄙视的公司)
每个post的题目都挺吓人,但是最终发现问题以及解决掉的方法,都异常简单。简单,产生丑陋。
问题描述:
程序慢的要死,CPU占用始终持续在70%-80%之间
解决步骤:
性能监视器。对于高CPU占用,一般的是这三个原因:
· 高的离谱的循环
· 太多的加载(比如,许多小的对象被频繁的处理)
· GC作了太多的事情
第一种情况,当你在恰当的时机抓到一个dump,就非常容易解决,一般而言,都是因为业务处理逻辑造成的。第二种情况,一般需要从硬件上考虑,scale up或者scale out,都行。
是否是GC的问题,我们需要看性能监视器里面的.NET CLR Memory计数器。这里面,最重要的是.net CLR Memory / % Time in GC. 这个值的阀值,可能是5%或者30%或者20%。实际上,没有一个准确的阀值存在的。当然,这个数字理论上应该接近于0%才对。
在GC里面,导致高CPU占用的原因,通常是因为过高的分配速率(对应到性能监视器里面的.net CLR Memory / allocated bytes/sec计数器。但实际上,如果所有的GC操作都在第0代上,则不会导致这个问题。真正的元凶,是大量的2代操作。如N多的对象在被移动到2代或者从2代中被释放。另一个原因就是我们熟知的大对象操作(LOH)
再强调一次,没有什么准确的指标,就告诉我们,超过了它就是出问题了,这是不可能的。包括微软给我们的大多数Practics Training,只有“尽量”、“尽可能”、“如果”等,而不是“一定”、“必须”。
如同我在那篇“浅谈GC中”讲到的一样,如果你搞了GC.Collect(2)或者GC.GetTotalMemory(true),那么也会导致大量的2代回收。
对于这个问题,我从性能监视器中抓到了这些数据:
% Time in GC ~40 % average allocated bytes / sec 400 MB average # Induced GC 0 # Gen 0 Collections 28.379 # Gen 1 Collections 28.378 # Gen 2 Collections 28.378
看第二行,真是让人晕倒!每秒分配400M字节!但实际上,我的代码中没有分配任何这么大的东西,有点太离谱了吧?!如果看最后面三行,也比较搞笑,0、1、2代的分配几乎完全相同。这实际上说明有LOH在压缩,或者有大量的对象冲进了第2代,然后又被立刻释放掉。
开始debug吧!
GC问题很难debug,因为:
1. 如果在GC中间过程中用adplus -hang模式抓了一个dump,基本上从dump里面看不到任何高CPU占用的原因。 2. 即时你通过性能监视器找到了GC的问题,也抓到了dump,但是很难分析。
实际上,通用的做法是每隔一小段时间,你就抓一次dump。如果看起来都差不多,那么有可能就能分析出哪里的问题了。
步骤1 - 我们在GC里面不?
如果我们的OS是server,那么每个CPU有一个GC线程(如果超线程的话,就是2个)。如果是非server的,那么只有一个GC线程。我们正在看到的这个dump,是在一个双核的、带有.NET2.0的机器上产生的。
如果我们不在GC过程中,那么我们会有两个线程(每个CPU一个):
18 Id: 134c.918 Suspend: 1 Teb: 7ffa8000 Unfrozen ChildEBP RetAddr Args to Child 01e1fe68 7c822124 77e6baa8 00000398 00000000 ntdll!KiFastSystemCallRet 01e1fe6c 77e6baa8 00000398 00000000 00000000 ntdll!NtWaitForSingleObject+0xc 01e1fedc 79e77fd1 00000398 ffffffff 00000000 kernel32!WaitForSingleObjectEx+0xac 01e1ff20 79e77f9a 00000398 ffffffff 00000000 mscorwks!PEImage::LoadImage+0x199 01e1ff70 79e77f50 ffffffff 00000000 00000000 mscorwks!CLREvent::WaitEx+0x117 01e1ff80 79f3549b ffffffff 00000000 00000000 mscorwks!CLREvent::Wait+0x17 01e1ffa8 79f6ece3 00000000 b28c067c 01e1ffec mscorwks!SVR::gc_heap::gc_thread_function+0x2e 01e1ffb8 77e66063 000e7660 00000000 00000000 mscorwks!SVR::gc_heap::gc_thread_stub+0x9b 01e1ffec 00000000 79f6ec79 000e7660 00000000 kernel32!BaseThreadStart+0x34
上面的代码表明,GC正在等着干活……在我抓到的dump中,GC看起来这个样子:
16 Id: f28.1150 Suspend: 1 Teb: fff82000 Unfrozen ChildEBP RetAddr Args to Child 0248fd28 7d4d8c46 000002dc 00000000 00000000 ntdll!ZwWaitForSingleObject+0x15 0248fd98 79e77fd1 000002dc ffffffff 00000000 kernel32!WaitForSingleObjectEx+0xac 0248fddc 79e77f9a 000002dc ffffffff 00000000 mscorwks!PEImage::LoadImage+0x199 0248fe2c 79e77f50 ffffffff 00000000 00000000 mscorwks!CLREvent::WaitEx+0x117 0248fe3c 79f35e1a ffffffff 00000000 00000000 mscorwks!CLREvent::Wait+0x17 0248fe54 7a0d5a3b 001afbf0 0000000d 00000002 mscorwks!SVR::t_join::join+0x61 0248ff54 79f391bf 00000002 001afbf0 00000000 mscorwks!SVR::gc_heap::plan_phase+0xd78 0248ff70 79f39954 00000002 ffffffff 001afbf0 mscorwks!SVR::gc_heap::gc1+0x57 0248ff88 79f35422 00000000 00000000 001afbf0 mscorwks!SVR::gc_heap::garbage_collect+0x37c 0248ffa8 79f6ece3 00000000 914b1904 0248ffec mscorwks!SVR::gc_heap::gc_thread_function+0x68 0248ffb8 7d4e0729 001afbf0 00000000 00000000 mscorwks!SVR::gc_heap::gc_thread_stub+0x9b 0248ffec 00000000 79f6ec79 001afbf0 00000000 kernel32!BaseThreadStart+0x34
看上面的粗体,哦,正在Collect。就是说,我们闯到了GC的肚子里面。
步骤2 - GC为什么开始工作了?
先看一下,CPU都被谁用掉了?
0:029> !runaway User Mode Time Thread Time 14:11b8 0 days 0:00:55.687 16:1150 0 days 0:00:45.500 17:7a8 0 days 0:00:43.875 21:1244 0 days 0:00:23.140 0:ea0 0 days 0:00:00.046 29:fc8 0 days 0:00:00.000 28:11f0 0 days 0:00:00.000
0:021> .time Debug session time: Tue Jun 20 09:46:04.000 2006 (GMT+2) System Uptime: 4 days 6:36:51.031 Process Uptime: 0 days 0:16:53.000 Kernel time: 0 days 0:00:45.000 User time: 0 days 0:02:48.000
上面来看,大约有17分钟被程序用掉了。我们看一下14号在作什么?
14 Id: f28.11b8 Suspend: 1 Teb: fff88000 Unfrozen ChildEBP RetAddr Args to Child 020afc8c 7d4d8c46 00000224 00000000 020afcd0 ntdll!ZwWaitForSingleObject+0x15 020afcfc 79e77fd1 00000224 00009c40 00000000 kernel32!WaitForSingleObjectEx+0xac 020afd40 79e77f9a 00000224 00009c40 00000000 mscorwks!PEImage::LoadImage+0x199 020afd90 79e77f50 00009c40 00000000 00000000 mscorwks!CLREvent::WaitEx+0x117 020afda0 79f5b69c 00009c40 00000000 00000000 mscorwks!CLREvent::Wait+0x17 020afe20 7a1121c3 001862f8 00009c40 00000000 mscorwks!ThreadpoolMgr::SafeWait+0x73 020afe94 79f71123 00000000 00000000 00000000 mscorwks!ThreadpoolMgr::WorkerThreadStart+0xf1 020affb8 7d4e0729 0019cab8 00000000 00000000 mscorwks!ThreadpoolMgr::intermediateThreadProc+0x49 020affec 00000000 79f710dd 0019cab8 00000000 kernel32!BaseThreadStart+0x34
哦,很清闲,什么都没干。但是,那17分钟中,有56秒在用着CPU,什么意思呢???我们继续看一下21号线程的托管堆。
0:021> !clrstack OS Thread Id: 0x1244 (21) ESP EIP 029ef1e8 7d61c824 [HelperMethodFrame: 029ef1e8] 029ef254 02920df3 LargeObjectHeap.GetLotsOfDatesXML(Int32) 029ef2a4 02920b20 LargeObjectHeap.Button1_Click(System.Object, System.EventArgs) 029ef2b4 6881bdc6 System.Web.UI.WebControls.Button.OnClick(System.EventArgs) 029ef2c8 6881bfbc System.Web.UI.WebControls.Button.RaisePostBackEvent(System.String) 029ef2dc 6881bf38 System.Web.UI.WebControls.Button.System.Web.UI.IPostBackEventHandler.RaisePostBackEvent(System.String) 029ef2e0 687d91e0 System.Web.UI.Page.RaisePostBackEvent(System.Web.UI.IPostBackEventHandler, System.String) 029ef2e8 687d912a System.Web.UI.Page.RaisePostBackEvent(System.Collections.Specialized.NameValueCollection) 029ef2f8 687dcbbf System.Web.UI.Page.ProcessRequestMain(Boolean, Boolean) 029ef4b0 687db521 System.Web.UI.Page.ProcessRequest(Boolean, Boolean) 029ef4e0 687db487 System.Web.UI.Page.ProcessRequest() 029ef518 687db3a7 System.Web.UI.Page.ProcessRequestWithNoAssert(System.Web.HttpContext) 029ef520 687db33a System.Web.UI.Page.ProcessRequest(System.Web.HttpContext) 029ef534 02920795 ASP.largeobjectheap_aspx.ProcessRequest(System.Web.HttpContext) 029ef538 686888df System.Web.HttpApplication+CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute() 029ef56c 6865a071 System.Web.HttpApplication.ExecuteStep(IExecutionStep, Boolean ByRef) 029ef5ac 6865a39b System.Web.HttpApplication.ResumeSteps(System.Exception) 029ef5f4 6865912d System.Web.HttpApplication.System.Web.IHttpAsyncHandler.BeginProcessRequest(System.Web.HttpContext, System.AsyncCallback, System.Object) 029ef610 6865e0bd System.Web.HttpRuntime.ProcessRequestInternal(System.Web.HttpWorkerRequest) 029ef644 6865dd72 System.Web.HttpRuntime.ProcessRequestNoDemand(System.Web.HttpWorkerRequest) 029ef650 6865c447 System.Web.Hosting.ISAPIRuntime.ProcessRequest(IntPtr, Int32) 029ef800 79f1ef33 [ContextTransitionFrame: 029ef800] 029ef850 79f1ef33 [GCFrame: 029ef850] 029ef9a8 79f1ef33 [ComMethodFrame: 029ef9a8]
看上面的call stack,我们看到了LargeObjectHeap.GetLotsOfDatesXML(),这个咚咚触发了LOH的回收,然后按次序触发了2代、1代、0代。
但是只有一个大对象,并不会导致高CPU的占用啊!那我们看一下实际的代码吧!
String GetLotsOfDatesXML(int i)
{
String dates = "<Dates>";
for (int j = 0; j < i; j++)
{
DateTime dt = DateTime.Now.Add(new TimeSpan(j, 0, 0, 0));
dates += "<Date><Month>" + dt.Month + "</Month><Day>" + dt.Day + "</Day><DayOfWeek>" + dt.DayOfWeek + "</DayOfWeek><Date>";
dates += "<Date><Month>" + dt.Month + "</Month><Day>" + dt.Day + "</Day><DayOfWeek>" + dt.DayOfWeek + "</DayOfWeek><Date>";
}
dates += "</Dates>";
return dates;
}
如果外面传来的i小点还好,但是如果很大呢?对于确切地i是多少,我们需要一点一点地开始找。从callstack上面,我们看到是LargeObjectHeap.Button1_Click调用了GetLotsOfDatesXML方法。
protected void Button1_Click(object sender, EventArgs e)
{
String str = GetLotsOfDatesXML(Int32.Parse(txtNumIterations.Text));
}
在这里,txtNumIterations是一个textbox控件。下面,基本上都是!do和!dso的工作了。
0:021> !dso OS Thread Id: 0x1244 (21) ESP/REG Object Name 029ef0f0 0b1a2270 System.String <String is invalid or too large to print> 029ef1a8 0b1a2270 System.String <String is invalid or too large to print> 029ef1bc 0b1a2270 System.String <String is invalid or too large to print> 029ef228 06a759e4 System.String </DayOfWeek><Date> 029ef28c 06a76df0 ASP.largeobjectheap_aspx 029ef2b4 06a77b84 System.ComponentModel.EventHandlerList 029ef2bc 06a77a84 System.Web.UI.WebControls.Button 029ef2c8 06a76df0 ASP.largeobjectheap_aspx 029ef304 06a76880 System.Web.HttpContext ...
0:021> !do 06a76df0 Name: ASP.largeobjectheap_aspx MethodTable: 02746ccc EEClass: 028d24cc Size: 380(0x17c) bytes (C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\Temporary ASP.NET Files\debuggersamples\e7443224\5232f845\App_Web_h0ctkxwz.dll) Fields: MT Field Offset Type VT Attr Value Name 790fa3e0 4001fe0 4 System.String 0 instance 02a7cba4 _id 790fa3e0 4001fe1 8 System.String 0 instance 00000000 _cachedUniqueID 68a2af44 4001fe2 c ...em.Web.UI.Control 0 instance 00000000 _parent 68a91070 4001fe3 2c System.Int32 0 instance 5 _controlState 68a85ea0 4001fe4 10 ...m.Web.UI.StateBag 0 instance 00000000 _viewState 68a2af44 4001fe5 14 ...em.Web.UI.Control 0 instance 00000000 _namingContainer 68a273d0 4001fe6 18 System.Web.UI.Page 0 instance 06a76df0 _page ... 68a7d910 4000004 16c ...ebControls.Button 0 instance 06a77a84 Button1 68a95f40 4000005 170 ...bControls.TextBox 0 instance 06a77be4 txtNumIterations 68a2bc80 4000006 174 ...Controls.HtmlForm 0 instance 06a77688 form1
好,继续看一下这个textbox。
0:021> !do 06a77be4 Name: System.Web.UI.WebControls.TextBox MethodTable: 68a95f40 EEClass: 68a95ebc Size: 80(0x50) bytes (C:\WINDOWS\assembly\GAC_32\System.Web\2.0.0.0__b03f5f7f11d50a3a\System.Web.dll) Fields: MT Field Offset Type VT Attr Value Name 790fa3e0 4001fe0 4 System.String 0 instance 02a85a04 _id 790fa3e0 4001fe1 8 System.String 0 instance 00000000 _cachedUniqueID 68a2af44 4001fe2 c ...em.Web.UI.Control 0 instance 06a77688 _parent 68a91070 4001fe3 2c System.Int32 0 instance 5 _controlState 68a85ea0 4001fe4 10 ...m.Web.UI.StateBag 0 instance 06a788f0 _viewState 68a2af44 4001fe5 14 ...em.Web.UI.Control 0 instance 06a76df0 _namingContainer 68a273d0 4001fe6 18 System.Web.UI.Page 0 instance 06a76df0 _page 68a92e2c 4001fe7 1c ...+OccasionalFields 0 instance 06a78974 _occasionalFields 68a2b378 4001fe8 20 ...I.TemplateControl 0 instance 00000000 _templateControl ...
0:021> !do 06a788f0 Name: System.Web.UI.StateBag MethodTable: 68a85ea0 EEClass: 68a85e30 Size: 16(0x10) bytes (C:\WINDOWS\assembly\GAC_32\System.Web\2.0.0.0__b03f5f7f11d50a3a\System.Web.dll) Fields: MT Field Offset Type VT Attr Value Name 79113dfc 400235f 4 ...tions.IDictionary 0 instance 06a78900 bag 79104f64 4002360 8 System.Boolean 0 instance 1 marked 79104f64 4002361 9 System.Boolean 0 instance 0 ignoreCase
0:021> !do 06a78900 Name: System.Collections.Specialized.HybridDictionary MethodTable: 7a747ad4 EEClass: 7a7aa890 Size: 20(0x14) bytes (C:\WINDOWS\assembly\GAC_MSIL\System\2.0.0.0__b77a5c561934e089\System.dll) Fields: MT Field Offset Type VT Attr Value Name 7a747bac 4001145 4 ...ed.ListDictionary 0 instance 06a78924 list 790fea70 4001146 8 ...ections.Hashtable 0 instance 00000000 hashtable 79104f64 4001147 c System.Boolean 0 instance 0 caseInsensitive
0:021> !do 06a78924 Name: System.Collections.Specialized.ListDictionary MethodTable: 7a747bac EEClass: 7a7aa918 Size: 28(0x1c) bytes (C:\WINDOWS\assembly\GAC_MSIL\System\2.0.0.0__b77a5c561934e089\System.dll) Fields: MT Field Offset Type VT Attr Value Name 7a747c78 4001148 4 ...ry+DictionaryNode 0 instance 06a78940 head 790fed1c 4001149 10 System.Int32 0 instance 1 version 790fed1c 400114a 14 System.Int32 0 instance 1 count 791117c8 400114b 8 ...ections.IComparer 0 instance 00000000 comparer 790f9c18 400114c c System.Object 0 instance 00000000 _syncRoot
0:021> !do 06a78940 Name: System.Collections.Specialized.ListDictionary+DictionaryNode MethodTable: 7a747c78 EEClass: 7a7aa9b8 Size: 20(0x14) bytes (C:\WINDOWS\assembly\GAC_MSIL\System\2.0.0.0__b77a5c561934e089\System.dll) Fields: MT Field Offset Type VT Attr Value Name 790f9c18 4001158 4 System.Object 0 instance 06a66bd8 key 790f9c18 4001159 8 System.Object 0 instance 06a78914 value 7a747c78 400115a c ...ry+DictionaryNode 0 instance 00000000 next
0:021> !do 06a66bd8 Name: System.String MethodTable: 790fa3e0 EEClass: 790fa340 Size: 26(0x1a) bytes (C:\WINDOWS\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll) String: Text ...
找到了Text属性,看看是多少?
0:021> !do 06a78914 Name: System.Web.UI.StateItem MethodTable: 68a131b4 EEClass: 68a13144 Size: 16(0x10) bytes (C:\WINDOWS\assembly\GAC_32\System.Web\2.0.0.0__b03f5f7f11d50a3a\System.Web.dll) Fields: MT Field Offset Type VT Attr Value Name 790f9c18 4002362 4 System.Object 0 instance 06a78184 value 79104f64 4002363 8 System.Boolean 0 instance 1 isDirty
0:021> !do 06a78184 Name: System.String MethodTable: 790fa3e0 EEClass: 790fa340 Size: 28(0x1c) bytes (C:\WINDOWS\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll) String: 40000 Fields: MT Field Offset Type VT Attr Value Name 790fed1c 4000096 4 System.Int32 0 instance 6 m_arrayLength 790fed1c 4000097 8 System.Int32 0 instance 5 m_stringLength 790fbefc 4000098 c System.Char 0 instance 34 m_firstChar 790fa3e0 4000099 10 System.String 0 shared static Empty >> Domain:Value 001a8868:790d6584 001ca990:790d6584 << 79124670 400009a 14 System.Char[] 0 shared static WhitespaceChars >> Domain:Value 001a8868:06a303f0 001ca990:06a34118 <<
靠,循环了4万次!
结论:
地球人都知道,用+=会产生3*n个对象,如果用StringBuilder,就不会有这个问题了。
string bytecode = "73 01 00 00 06 28 4E 00 00 0A 2A"; 上面这个string干什么的?加密后的代码?某种不可思议的数字?汇编语言?
如果按照CLR IL规范来解析,我们会得到一段常见的代码。 0000 : newobj instance void WindowsApplication3.MainForm::.ctor() 0005 : call System.Void System.Windows.Forms.Application::Run() 0010 : ret
简单吗?看起来是new一个MainForm,然后call Application的Run方法,最后return。 仔细想想,这就是我们常见的,每个winform程序中缺省方式下自动生成的代码。如下: static void Main() { Application.Run(new MainForm()); } 实际上等同于: static void Main() { MainForm mf = new MainForm(); Application.Run(mf); }
那么,上面那个bytecode,是如何被翻译成上述代码的?
哦,bytecode的第一个字符,就是73。有一个神奇的表格,其中一行: 73 newobj <Method> N arguments o
我们看到了,73代表一个东西叫做newobj,并且,它是一个method。我们应该能猜测出来,C#代码中应该就是一个new关键字。
第二个数字01,我们还是查找那个表格: 01 break - - -
哦,怎么是break呢?我们代码里面,不大可能new了一个对象之后,立刻就break啊?实际上,第一句newobj之后,执行的代码是从0x05,就是0x28开始(第二句) 。为什么跳跃了5个字节呢?因为IL中,并不只是包含operator,还包含了其他数据。在CLR解析过程中,它发现0x73是一个method之后,会继续取得一个所谓的metadataToken。伪代码如下: metadataToken = ReadInt32(il, ref position); 而ReadInt32代码,如下所示: private int ReadInt32(byte[] _il, ref int position) { return (((il[position++] | (il[position++] << 8)) | (il[position++] << 0x10)) | (il[position++] << 0x18)); }
为什么会这么做??? 暂且不表,我们继续。通过上面的ReadInt32,position增加了4,变成了5。所以要从bytecode第0个字节跳跃5个,直奔0x28。通过查表,我们知道,0x28代表着一个call: 28 call <Method> N arguments Ret.value。通过某种手段,我们可以得到当前0x28的call的方法名,是run。
那么首先,那个神奇的表格是什么呢?就是一个二维信息,从0x00到0xFE,每个字节分别对应着某个操作,如br,call,newobj等。这个表格,可以从《Inside Microsoft.NET IL Assembler eBook》中找到。 我们查表查找到operator之后,通过一系列的Read*方法,得到metadatatoken。针对这个token,分别进行resolve,得到最后的operand。如上所述,通过Read*系列方法,我们同时移动了bytecode中的offset,可以定位到正确的下一个operator。
这段解析代码,可以到codeproject上查找SDILReader,里面的Globals.cs、ILInstruction.cs、MethodBodyReader.cs,分别对应着全局operators、IL构造、方法转换等内容。
GC的几个误区,需要注意的地方(节选自偶从前写的《浅谈GC》)
说明:对于下面提到的“上一部分”、“第一部分”等,是整篇文章的前部分,我这里面只放了后面的编程注意事项。
1.1 万物皆可被回收 GC针对的仅仅是托管资源,对于非托管资源,GC不负责、也无法对其进行回收。对于File Handle、Mutex、Network Handle、Database Connection、Socket、GDI Object(如Brush、Font、Icon),调用者需要自己手动进行处理。究其根本原因在于,CLR无法识别这些非托管资源的正确类型,无法知道该如何对资源进行释放(虽然CLR可以跟踪到这些资源的生命周期)。而对于托管资源,Metadata(元数据),则给CLR提供了正确的对象引用信息以及释放资源的方法,这是GC算法的一个重要依据。 根据上面非托管对象回收的介绍,我们知道,只有提供了Finalize方法的对象,才可以被GC回收掉。否则,会造成内存泄漏。
1.2 手动垃圾回收可以提高性能 我们经常会看到手动垃圾回收在代码中的调用,如下面这段例子:
private void SomeCloseFunction( ){ // Some Close Function GC.Collect(); // 或者如下调用代码: GC.Collect(1);// 对generation 1进行回收 }
上述代码的目的是为了清理内存,提高程序的运行效率。但是事与愿违,绝大多数情况下,我们的代码会变慢,或者变得根本无法使用。原因如下:
- GC在Collect的时候,CLR会挂起当前正在运行的所有线程。
- GC会对generation 0/1/2所有的对象进行回收,这可能是一个非常耗时的工作。
- 如果GC.Collect在你的某个被调用频度很高的核心代码中,如数据库连接的关闭、某个字符串处理方法,频繁回收,将导致其他应用无法得到CPU的控制权,产生严重的性能问题。
这里面的根本原则就是,CLR知道何时进行GC,GC的最佳时机是CLR根据操作系统的运行状况动态决定的。当然,如果我们的代码中对于非托管资源有很多的占用,如一个GDI的应用,那么可以在应用程序关闭或者GDI处理完毕的时候,进行一次GC.Collect的调用。
1.3 Finalize会快速并准确地回收对象 Finalize方法在C#中无法被重写,取而代之的是一个非常容易让我们混淆的C++析构函数的写法,~SomeClassName()。在C++中,析构函数我们可以认为是一种确定性行为,对象的生命周期结束后,析构函数将立刻被调用。而在C#(或者其他的支持CLR的语言)中,由于GC的不确定性,上述析构方法被调用的时机我们无法确定。
第一部分我们讲过,只有对于非托管资源,我们才需要对其进行手动回收处理。常见的代码可能是这样:
private SomeUnmangedType sut; private void MyClass( ){ sut = CreateSomeUnmanagedType(); sut.DoSomeThing(); }
~MyClass(){ sut.CloseSomeUnmanagedType(); }
对于非托管资源,一般来讲,上述的代码是正确的。如果我们用ildasm看这段代码的IL code,我们会发现代码实际上是类似于下面的样子:
~MyClass(){ try{ sut.CloseSomeUnmanagedType(); } finally{ base.Finalize(); } }
try/ finally对,是C#编译器自动生成的代码。在finally中,自动增加了对于base的Finalize方法的调用。
而对于托管资源,我们有N多的理由来反对使用Finalize方法:
- Finalize的被调用,是我们不能控制的,是由CLR决定的。
- 所有重写了Finalize方法的对象以及相关引用对象,在垃圾回收的时候,代龄可能会提高,增加不必要的内存负担。
- 所有重写了Finalize方法的对象,CLR会额外的建立一个链表来对其进行维护,GC将进行至少两次才能真正的释放对象。
- 致命的一点是,对于托管资源对象,如Database Connection,如果我们在Finalize方法中对其进行了Close的调用,那么将会发生不可预知的错误,根本原因就在于对于FReachable Queue的特殊线程处理,CLR不保证Finalize方法的执行顺序。如A包含了一个对于B的引用,那么A或者B的Finalize调用顺序是不可预知的。对此问题,可以参考如下的MSDN链接:http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpref/html/frlrfsystemdatasqlclientsqlconnectionclassclosetopic.asp
为了解决此问题,我们可以采用Dispose模式,即需要实现IDisposable接口。在C#中,也可以方便的使用using语法。对于Dispose模式,可以参考如下的MSDN示例:
class X: IDisposable{ public X(?…){ … initialize resources … }
~X(){ ?… release resources ?… }
public void Dispose(){ // this is the same as calling ~X() Finalize(); // no need to finalize later System.GC.SuppressFinalize(this); } };
对于GC更多的资源,请参见MSDN相关文章:http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpguide/html/cpconProgrammingEssentialsForGarbageCollection.asp
1.4对于COM+之类的Wrapper会自动管理内存生命周期
首先需要澄清的是,COM+ Application或者COM+ Library,是长时间(如24*7)的run在客户的Server上的。只要24*7有组件不停的被调用,那么该Application或者Library就不会被自动释放或回收。
所以,虽然.NET提供了对于COM+的wrapper,我们仍需要手工的再作一些工作。
错误的代码示例如下:
MyComplusComponent mcc = new MyComplusComponent(); mcc.CallSomeFunction(); mcc = null;
这段代码之所以错误,是因为仅仅释放了mcc这个wrapper,而该对象本身的Reference却没有被release,导致该mcc所指向的Complus Compoent一直存在。修正代码如下:
MyComplusComponent mcc = new MyComplusComponent(); mcc.CallSomeFunction(); System.Runtime.InteropServices.Marshal.ReleaseComObject(mcc);//这句对于reference进行了-1的操作。 mcc = null;
总结一下就是说,OS自己能做的,CLR自己能做的,我们不要干预。尤其是对于Wrapper之类的东西,一定要小心谨慎。
看这个代码(VC6 + sp6)
void Test( void ){
unsigned int a = 1;
int b = -2;
printf("a+b>0 is ;%s", (a+b>0)?"true":"false");
}
结果是:true。理由很简单,int会向unsigned int靠拢,得到的是一个很大的unsigned int,所以是true。隐含在后面的事什么呢?看汇编:
10: printf("a+b>0 is %s", (a+b>0)?"true":"false"); 00401036 mov eax,dword ptr [ebp-4] 00401039 add eax,dword ptr [ebp-8] 0040103C test eax,eax 0040103E jbe main+39h (00401049) 00401040 mov dword ptr [ebp-0Ch],offset string "true" (00422044) 00401047 jmp main+40h (00401050) 00401049 mov dword ptr [ebp-0Ch],offset string "false" (0042203c) 00401050 mov ecx,dword ptr [ebp-0Ch] 00401053 push ecx 00401054 push offset string "a+b>0 is %s" (0042202c) 00401059 call printf (004010a0) 0040105E add esp,8
注意看上面红色的部分,此时EAX=0XFFFFFFFF,test的结果ZF=0
那么,同样的代码,我们放在.NET中来做,显示结果是相反的:false
uint a = 1; int b = -2; Console.WriteLine((a + b > 0) ? "True" : "False");
看IL代码:
IL_0001: ldc.i4.1 IL_0002: stloc.0 //set a = 1; IL_0003: ldc.i4.s -2 // push 一个-2(i4.s意味着当作一个int8,就是byte来处理)作为int32类型 IL_0005: stloc.1 // set b = -2 IL_0006: ldloc.0 IL_0007: conv.u8// convert变量a为一个unsigned int64 IL_0008: ldloc.1 IL_0009: conv.i8// convert变量b为一个int 64 IL_000a: add // 相加 IL_000b: ldc.i4.0 IL_000c: conv.i8// convert为一个int 64
native assembly我看不到,不知道怎么写的,单纯看上面的IL,看不出来,CLR为什么要这么处理。
|