一 基礎知識

在分析之前,先上一張圖:

從上面可以看到,這個w3wp程序佔用了376M記憶體,啟動了54個執行緒。

在使用windbg檢視之前,看到的程序含有 *32 字樣,意思是在64位機器上已32位方式執行w3wp程序。這個可以通過檢視IIS Application Pool 的高階選項進行設定:

好了,接下開啟Windbg看看這個w3wp程序佔用了376M記憶體,啟動的54個執行緒。

1. 載入 WinDbg SOS 擴充套件命令

.load C:\Windows\Microsoft.NET\Framework\v2.0.50727\sos.dll

2. !dumpheap -stat

!DumpHeap 將遍歷 GC 堆對物件進行分析。

MT            Count    TotalSize       Class Name
78eb9834        1           12       System.ServiceModel.ServiceHostingEnvironment+HostingManager+ExtensionHelper

0118c800      101        14824      Free
...
63ce0004    19841      1111096 System.Reflection.RuntimeMethodInfo
63ce2ee4    11080      2061036 System.Int32[]
63ce0d48    34628      2242596 System.String
63ce37b8    20012      3264884 System.Byte[]
63cb4518   157645      4940676 System.Object[]
Total 524310 objects

可以看到,w3wp上總共有524310個物件, 共佔用了這些記憶體。

我們可以將上述上述列出的這些物件歸為2類:

1). 有根物件(在應用程式中對這些物件存在引用)

2). 自從上次垃圾回收之後新建立或無跟物件

要注意的是Free這項:

0118c800      101        14824      Free

這項一般都是GC not yet Compacted的空間或一些堆上分配的禁止GC compacted釘釦物件.

第一欄 : 型別的方法列表 MT(method type for the type)

第二欄:堆上的物件數量

第三欄:所有同類物件的總大小

3. !dumpheap -mt 63ce0d48

檢視 63ce0d48  單元的有哪些物件。

4. !do 103b3360

看看103b3360地址的string包含哪些內容

可見,103b3360地址的字串value="System.Web.UI.PageRequestManager:AsyncPostBackError", 佔120bytes. 這個字串物件包含3個欄位,它們的偏移量分別是4,8,12。

5. dd 103b3360

看看103b3360的值

從左往右第一列是地址,而第二列開始則是地址上的資料。

6. !dumpheap -type String -min 100

看看堆上所有大於100位元組的字串。 注意:假如 -min 85000(大於85000位元組的字串或物件將儲存在大物件堆上).

二. NET記憶體洩露分析案例

1 基礎認識

.net世界裡,GC是負責垃圾回收的,但GC僅僅是回收哪些不可及的物件(無根物件),對於有應用的有根物件,GC對此無能為力。

.net一些記憶體洩漏的根本原因:

  • 使用靜態引用
  • 未退訂的事件-作者認為這是最常見的記憶體洩漏原因
  • 未退訂的靜態事件
  • 未呼叫Dispose方法
  • 使用不徹底的Dispose方法
  • 在Windows Forms中對BindingSource的誤用
  • 未在WorkItem/CAB上呼叫Remove

一些避免記憶體洩漏的建議:

  • 物件的建立者或擁有者負責銷燬物件,而不是使用者
  • 當不再需要一個事件訂閱者時退訂此事件,為確保安全可以在Dispose方法中退訂
  • 當物件不再觸發事件時,應該將物件設為null來移除所有的事件訂閱者
  • 當模型和檢視引用同一個物件時,推薦給檢視傳遞一個此物件的克隆,以防止無法追蹤誰在使用哪個物件
  • 對系統資源的訪問應該包裝在using塊中,這將在程式碼執行後強制執行Dispose

對這些做基本瞭解後,我們將步入正題。

2. 案例分析

先上測試程式碼:

 public class LeakTest
{
private static string leakString; public LeakTest()
{
for (int i = ; i < ; i++)
{
leakString += "LEAK";
}
} public string GetRamdonString()
{
System.Random random = new System.Random(); string str = string.Empty;
for (int i = ; i < ; i++)
{
str += str + random.Next();
}
return str;
} public void NoDispose()
{
string str = GetRamdonString(); ZipFile zip = new ZipFile();
zip.AddEntry("a.txt", str);
zip.AddEntry("b.txt", str);
zip.Save("test.rar");
//zip.Dispose();
}
} class Program
{
static void Main(string[] args)
{
LeakTest leakTest = new LeakTest();
leakTest.NoDispose();
Console.ReadLine();
}
}

需要說明的是:

這裡程式裡面定義了一個Static 字串,及使用了Ionic.Zip 這個Zip壓縮包,僅僅是為了模擬記憶體堆積現象,沒有呼叫zip.Dispose()方法,事實上Ionic.Zip並不會造成記憶體洩露。

正式開始了:

啊哈,好極了。 執行程式,好傢伙,果然很耗費記憶體! 這麼個小程式,吃了287M,並啟動了12個執行緒.

0:005> .load C:\Windows\Microsoft.NET\Framework64\v2.0.50727\sos.dll 
0:005> .load C:\Symbols\sosex_64\sosex.dll

0:005> !dumpheap -stat

 :> !dumpheap -stat
PDB symbol for mscorwks.dll not loaded
total objects
Statistics:
MT Count TotalSize Class Name
000007ff001d2248 System.Collections.Generic.Dictionary`+ValueCollection[[System.String, mscorlib],[Ionic.Zip.ZipEntry, Ionic.Zip.Reduced]]
000007ff000534f0 ZipTest.LeakTest
000007fee951e8e8 System.IO.TextReader+NullTextReader
000007fee94f8198 System.Security.Cryptography.RNGCryptoServiceProvider

...
000007ff001d9130 Ionic.Zlib.DeflateManager+CompressFunc
000007fee94d2d40 System.Threading.ExecutionContext
000007fee951e038 System.UInt32[]
000007fee951ca10 System.Int16[]
Free
000007fee94d7d90 System.String
000007fee94dfdd0 System.Byte[]
Total objects

果然,我們看到了裡面有2類大物件,分別佔用了134M和138M . 好傢伙!

0:005> !dumpheap -mt

 :> !dumpheap -mt 000007fee94dfdd0
Address MT Size
3...
00000000026f11f0 000007fee94dfdd0
000007fee94dfdd0
00000000027112a0 000007fee94dfdd0
0000000002722b50 000007fee94dfdd0
0000000002752b98 000007fee94dfdd0
...
000000000290ab98 000007fee94dfdd0
000000000293abe0 000007fee94dfdd0
...
0000000002ac1378 000007fee94dfdd0
0000000002ad1410 000007fee94dfdd0
66...
00000000165a71e0 000007fee94dfdd0
0000000027c11000 000007fee94dfdd0
total objects
Statistics:
MT Count TotalSize Class Name
000007fee94dfdd0 System.Byte[]
Total objects

果然,有那麼多65592和65560啊 啊

隨便找一個看一下:

0:005> !do 0000000002ba4790

 :> !do 0000000002ba4790
Name: System.Byte[]
MethodTable: 000007fee94dfdd0
EEClass: 000007fee90e26b0
Size: (0x10036) bytes
Array: Rank , Number of elements , Type Byte
Element Type: System.Byte
Fields:
None

哦。這是個一維的陣列,有65566位元組,推測應該好像是short(int16)長度。

繼續,

!gcroot 0000000002b42dd0

:> !gcroot 0000000002b42dd0
Note: Roots found on stacks may be false positives. Run "!help gcroot" for
more info.
Scan Thread OSTHread 1d3c
RSP:18ef58:Root:00000000025c5b88(Ionic.Zip.ZipFile)->
00000000025d2578(Ionic.Zlib.ParallelDeflateOutputStream)->
00000000025dc528(System.Collections.Generic.List`[[Ionic.Zlib.WorkItem, Ionic.Zip.Reduced]])->
000000000294ac38(System.Object[])->
0000000002b32d78(Ionic.Zlib.WorkItem)->
0000000002b42dd0(System.Byte[])
...
Scan Thread OSTHread

這裡有點看頭了! 看其跟物件 Ionic.Zip.ZipFile 這個物件佔著沒銷燬的記憶體呢!

RSP:18ef58:Root:00000000025c5b88(Ionic.Zip.ZipFile)->
00000000025d2578(Ionic.Zlib.ParallelDeflateOutputStream)->
00000000025dc528(System.Collections.Generic.List`1[[Ionic.Zlib.WorkItem, Ionic.Zip.Reduced]])->
000000000294ac38(System.Object[])->
0000000002b32d78(Ionic.Zlib.WorkItem)->
0000000002b42dd0(System.Byte[])

換一個看看:

:> !gcroot 00000000029bc730
Note: Roots found on stacks may be false positives. Run "!help gcroot" for
more info.
Scan Thread OSTHread 1d3c
RSP:18ef58:Root:00000000025c5b88(Ionic.Zip.ZipFile)->
00000000025d2578(Ionic.Zlib.ParallelDeflateOutputStream)->
00000000025dc528(System.Collections.Generic.List`[[Ionic.Zlib.WorkItem, Ionic.Zip.Reduced]])->
000000000294ac38(System.Object[])->
00000000029ac6d8(Ionic.Zlib.WorkItem)->
00000000029bc730(System.Byte[])
...
Scan Thread OSTHread

檢視下其代齡:

0:012> !gcgen 00000000029bc730
GEN 1

看到了,這個byte[]在1代。

到此為止,還記得有個靜態字串吧

private static string leakString;

我們回頭再去看看,

 !dumpheap -type String -min 1000

:> !dumpheap -type String -min
Address MT Size
00000000025c26e0 000007fee94d7d90
00000000025cca30 000007fee94d7d90
00000000025cd308 000007fee94d7d90
000000001ae81000 000007fee94d7d90
total objects
Statistics:
MT Count TotalSize Class Name
000007fee94d7d90 System.String
Total objects

Next,

0:012> !do 00000000025c26e0

:> !do 00000000025c26e0
Name: System.String
MethodTable: 000007fee94d7d90
EEClass: 000007fee90de560
Size: (0x1f5a) bytes
(C:\Windows\assembly\GAC_64\mscorlib\2.0..0__b77a5c561934e089\mscorlib.dll)
String: LEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKL....
EAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAK
Fields:
MT Field Offset Type VT Attr Value Name
000007fee94df000 System.Int32 instance m_arrayLength
000007fee94df000 c System.Int32 instance m_stringLength
000007fee94d97d8 System.Char instance 4c m_firstChar
000007fee94d7d90 System.String shared static Empty
>> Domain:Value 000000000062b1d0:00000000025c1308 <<
000007fee94d9688 400009a System.Char[] shared static WhitespaceChars
>> Domain:Value 000000000062b1d0:00000000025c1a90 <<

再看下這個物件:

!dumpobj 00000000025c26e0

:> !dumpobj 00000000025c26e0
Name: System.String
MethodTable: 000007fee94d7d90
EEClass: 000007fee90de560
Size: (0x1f5a) bytes
(C:\Windows\assembly\GAC_64\mscorlib\2.0..0__b77a5c561934e089\mscorlib.dll)
(C:\Windows\assembly\GAC_64\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
String: LEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKL....
EAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAK
Fields:
MT Field Offset Type VT Attr Value Name 000007fee94df000   System.Int32  instance  m_arrayLength 000007fee94df000  c System.Int32  instance  m_stringLength 000007fee94d97d8   System.Char  instance 4c m_firstChar 000007fee94d7d90   System.String  shared static Empty >> Domain:Value 000000000062b1d0:00000000025c1308 << 000007fee94d9688 400009a  System.Char[]  shared static WhitespaceChars >> Domain:Value 000000000062b1d0:00000000025c1a90 <<

顯示結果一樣,String:LEAKLEAKLEAKLEAKLEAK......,字串長度4000,和我們的測試程式碼吻合:

  public LeakTest()
{
for (int i = ; i < ; i++)
{
leakString += "LEAK";
}
}

到此,記憶體檢視分析演示的差不多了!

這裡我們演示的是個小得不能再小的程式,且存在前提預期。 假如在實際專案環境中,因為引用的DLL多,生成的物件繁雜,實際診斷問題根源就複雜得多,這就需要比較紮實的基本功。

. 死鎖排查

 1. 基礎

還是用上面的Console App例子,執行這個程式,啟動了13個執行緒。我們先看一下這13個執行緒:

!runaway

:> !runaway
User Mode Time
Thread Time
: days ::05.085
: days ::01.903
:4ddc days ::01.825
:5af4 days ::01.809
: days ::01.747
:6c38 days ::01.731
:6a94 days ::01.700
:43ec days ::01.622
:8fdc days ::01.606
:1e64 days ::00.000
:6a4 days ::00.000
:64b4 days ::00.000
:69e4 days ::00.000

恩。13個執行緒,沒錯。 這裡還可以看到每個執行緒的執行時間。 其中 0 執行緒佔用時間最多。我們去看下堆疊呼叫:

~0s

!ClrStack -a

:> ~0s
ntdll!ZwRequestWaitReplyPort+0xa:
`77b714da c3 ret
:> !ClrStack -a
OS Thread Id: 0x5588 ()
*** WARNING: Unable to verify checksum for C:\Windows\assembly\NativeImages_v2..50727_64\mscorlib\c3beeeb6432f004b419859ea007087f1\mscorlib.ni.dll
Child-SP RetAddr Call Site
00000000001de670 000007fee9b02c79 DomainNeutralILStubClass.IL_STUB(Microsoft.Win32.SafeHandles.SafeFileHandle, Byte*, Int32, Int32 ByRef, IntPtr)
PARAMETERS:
<no data>
<no data>
<no data>
<no data>
<no data> 00000000001de790 000007fee9b02d92 System.IO.__ConsoleStream.ReadFileNative(Microsoft.Win32.SafeHandles.SafeFileHandle, Byte[], Int32, Int32, Int32, Int32 ByRef)
PARAMETERS:
hFile = <no data>
bytes = <no data>
offset = <no data>
count = <no data>
mustBeZero = <no data>
errorCode = 0x00000000001de820
LOCALS:
<no data>
0x00000000001de7c0 = 0x0000000000000000
<no data>
<no data> 00000000001de7f0 000007fee93f08da System.IO.__ConsoleStream.Read(Byte[], Int32, Int32)
PARAMETERS:
this = <no data>
buffer = <no data>
offset = <no data>
count = <no data>
LOCALS:
0x00000000001de820 = 0x0000000000000000
<no data> 00000000001de850 000007fee9412a8a System.IO.StreamReader.ReadBuffer()
PARAMETERS:
this = <no data>
LOCALS:
<no data> 00000000001de8a0 000007fee9b0622f System.IO.StreamReader.ReadLine()
PARAMETERS:
this = <no data>
LOCALS:
<no data>
<no data>
<no data>
<no data> 00000000001de8f0 000007ff00190188 System.IO.TextReader+SyncTextReader.ReadLine()
PARAMETERS:
this = 0x00000000030387b0 00000000001de950 000007feea23c6a2 ZipTest.Program.Main(System.String[])
PARAMETERS:
args = 0x00000000027e2680
LOCALS:
0x00000000001de970 = 0x00000000027e26a0

瞧準了,這是個主執行緒,他在等待Console.ReadLine(). 所以佔用了這麼長時間。

再在看一下這13個執行緒裡,哪些是託管堆執行緒:

!threads

:> !threads
ThreadCount:
UnstartedThread:
BackgroundThread:
PendingThread:
DeadThread:
Hosted Runtime: no
PreEmptive Lock
ID OSID ThreadOBJ State GC GC Alloc Context Domain Count APT Exception
00000000009d4510 a020 Enabled 00000000030387d0:000000000303a510 00000000009cb1d0 MTA
64b4 00000000009dc4d0 b220 Enabled : 00000000009cb1d0 MTA (Finalizer)
4ddc 0000000000a1a010 180b220 Enabled 0000000002fe1e28:0000000002fe2450 00000000009cb1d0 MTA (Threadpool Worker)
6a94 0000000000a1d590 180b220 Enabled 0000000002fe73c8:0000000002fe8450 00000000009cb1d0 MTA (Threadpool Worker)
43ec 0000000000a7bbd0 180b220 Enabled 0000000002fec968:0000000002fee450 00000000009cb1d0 MTA (Threadpool Worker)
8fdc 0000000000a892b0 180b220 Enabled 0000000002ff0968:0000000002ff2450 00000000009cb1d0 MTA (Threadpool Worker)
0000000000aa3270 180b220 Enabled 0000000002fee968:0000000002ff0450 00000000009cb1d0 MTA (Threadpool Worker)
5af4 0000000000a97eb0 180b220 Enabled 0000000002fe8968:0000000002fea450 00000000009cb1d0 MTA (Threadpool Worker)
0000000000a99400 180b220 Enabled 0000000002fe0358:0000000002fe0450 00000000009cb1d0 MTA (Threadpool Worker)
a 6c38 0000000000a9f3a0 180b220 Enabled 0000000002fe3e28:0000000002fe4450 00000000009cb1d0 MTA (Threadpool Worker)

在託管堆上啟動的執行緒有10個。這10個執行緒分別是什麼,繼續看:

0號MTA: 程式主執行緒

MTA (Finalizer):這個是Finalizer執行緒,該執行緒負責垃圾物件回收。

MTA (Threadpool Worker):這些是ThreadPool建立的執行緒,這裡是Ionic.Zlib.WorkItem產生的工作執行緒。

另外,CLR根據需要還會開啟其他一些執行緒,如:

併發的GC執行緒 ,伺服器GC執行緒 ,偵錯程式幫助執行緒 ,AppDomain解除安裝執行緒 等.

看一下同步塊情況,有麼有死鎖?

!syncblk

!dlk

:> !dlk
Examining SyncBlocks...
Scanning for ReaderWriterLock instances...
Scanning for holders of ReaderWriterLock locks...
Scanning for ReaderWriterLockSlim instances...
Scanning for holders of ReaderWriterLockSlim locks...
Examining CriticalSections...
No deadlocks detected.

顯示該程式沒有鎖相關資源,實際確實沒有。

2 死鎖

Lock:lock 關鍵字將語句塊標記為臨界區,方法是獲取給定物件的互斥鎖,執行語句,然後釋放該鎖。 下面的示例包含一個 lock 語句。

lock 關鍵字可確保當一個執行緒位於程式碼的臨界區時,另一個執行緒不會進入該臨界區。 如果其他執行緒嘗試進入鎖定的程式碼,則它將一直等待(即被阻止),直到該物件被釋放。

通常,應避免鎖定 public 型別,否則例項將超出程式碼的控制範圍。 常見的結構 lock (this)、lock (typeof (MyType)) 和 lock ("myLock") 違反此準則:

  • 如果例項可以被公共訪問,將出現 lock (this) 問題。

  • 如果 MyType 可以被公共訪問,將出現 lock (typeof (MyType)) 問題。

  • 由於程序中使用同一字串的任何其他程式碼都將共享同一個鎖,所以出現 lock("myLock") 問題。

最佳做法是定義 private 物件來鎖定, 或 private static 物件變數來保護所有例項所共有的資料。

3 案例分析