TechEd2008- .NET 應用程式除錯秘技系列番外篇 - Memory leak
Memory leak, 中文翻譯成"記憶體泄漏"(怪怪的), 用來說明程式因為疏忽或錯誤造成記憶體未能如期的進行釋放。從另一個角度看,就是記憶體的使用不斷的增長(因為沒釋放不再使用的記憶體或釋放的速度不如使用的速度)。有關於系統及應用程式的記憶體相關名詞,可能需要一篇專文來說明。 曾經在網路上聽到一些似是而非的言論~~"聽說呼叫GC回收可以立即釋放沒用到的記憶體~", "可是聽說呼叫GC回收很耗系統資源耶~~".....
我的建議是~在還沒有搞懂.NET 如何管理記憶體以及GC的運作原理前,請先忽略以上的"聽說"...
在我們team所處理過memory leak 的經驗當中(以.NET而言),有一半以上的問題都是記憶體的使用由一個或數個很大的物件所佔據,而這個物件短時間內又釋放不掉(或是需要長期使用),例如session, global 變數....此次的範例就是模擬程式執行時,記憶體不斷的增長,並且透過windbg來分析到底是誰佔用了記憶體。
範例及相關文件分成3個壓縮檔(08_Memoryleak.part01.rar , 08_Memoryleak.part02.rar , 08_Memoryleak.part03.rar共93MB)。之所以這麼大是因為為了方便大家學習,我將dump檔及分析的log都放在裏頭。因此解壓縮之後,您會發現有一個memoryleak.dmp 佔了七百多MB。
開啟專案後,請使用Ctrl + F5 執行程式,並在工作管理員觀察WebDev.WebServer.exe這個process。在工作管理員中,請按檢視=>選擇欄位並勾選"記憶體使用量"及"虛擬記憶體大小"2個選項。 執行後按下網頁上的"Eat Memory"按鈕,您將發現記憶體的使用會不斷成長。
當記憶體長到300MB以上時,您可以用WinDbg 附加到這個process以進行Memory leak的troubleshooting. 以下使用範例中的dump檔進行分析:
1. 輸入 ".loadby sos mscorwks" 指令來載入.NET 的extension
2. 由於我們使用WinDbg附加到process,因此一開始所在的thread是被我們斷下來的session。此時我們可以透過 "~*e !clrstack" 指令來顯示所有thread的call stacks.
!clrstack 是用來顯示目前所在thread的call stacks. 在前面加上"~*e" 可以針對所有thread來執行後面的指令。其顯示的結果很長,看一下結果可以看到,第11條thread應該是目前在運作的thread (其他都在waiting)
OS Thread Id: 0x960 (11)
ESP EIP
0437d6d4 79ef1750 [HelperMethodFrame_1OBJ: 0437d6d4] System.Number.FormatDecimal(System.Decimal, System.String, System.Globalization.NumberFormatInfo)
0437d7c8 793ef69f System.Decimal.ToString()
0437d7cc 66187ba7 System.Web.UI.WebControls.BoundField.FormatDataValue(System.Object, Boolean)
0437d7e8 66188181 System.Web.UI.WebControls.BoundField.OnDataBindField(System.Object, System.EventArgs)
0437d804 66188ac7 System.Web.UI.WebControls.AutoGeneratedField.OnDataBindField(System.Object, System.EventArgs)
0437d83c 6613bb54 System.Web.UI.Control.OnDataBinding(System.EventArgs)
0437d84c 6613bc4f System.Web.UI.Control.DataBind(Boolean)
0437d888 6613bb6d System.Web.UI.Control.DataBind()
0437d88c 6613bd39 System.Web.UI.Control.DataBindChildren()
0437d8bc 6613bc59 System.Web.UI.Control.DataBind(Boolean)
0437d8f8 6613bb6d System.Web.UI.Control.DataBind()
0437d8fc 661e38e6 System.Web.UI.WebControls.GridView.CreateRow(Int32, Int32, System.Web.UI.WebControls.DataControlRowType, System.Web.UI.WebControls.DataControlRowState, Boolean, System.Object, System.Web.UI.WebControls.DataControlField[], System.Web.UI.WebControls.TableRowCollection, System.Web.UI.WebControls.PagedDataSource)
0437d930 661e196e System.Web.UI.WebControls.GridView.CreateChildControls(System.Collections.IEnumerable, Boolean)
0437da10 661a728c System.Web.UI.WebControls.CompositeDataBoundControl.PerformDataBinding(System.Collections.IEnumerable)
0437da20 661e7138 System.Web.UI.WebControls.GridView.PerformDataBinding(System.Collections.IEnumerable)
0437da30 66183880 System.Web.UI.WebControls.DataBoundControl.OnDataSourceViewSelectCallback(System.Collections.IEnumerable)
0437da3c 66144dca System.Web.UI.DataSourceView.Select(System.Web.UI.DataSourceSelectArguments, System.Web.UI.DataSourceViewSelectCallback)
0437da48 66183a86 System.Web.UI.WebControls.DataBoundControl.PerformSelect()
0437da5c 661831f7 System.Web.UI.WebControls.BaseDataBoundControl.DataBind()
0437da64 661e3a35 System.Web.UI.WebControls.GridView.DataBind()
0437da68 05970661 Memoryleak.Memoryleak.Button1_Click(System.Object, System.EventArgs)
0437da94 6619004e System.Web.UI.WebControls.Button.OnClick(System.EventArgs)
0437daa8 6619023c System.Web.UI.WebControls.Button.RaisePostBackEvent(System.String)
0437dabc 661901b8 System.Web.UI.WebControls.Button.System.Web.UI.IPostBackEventHandler.RaisePostBackEvent(System.String)0437dd60 05970185 ASP.memoryleak_aspx.ProcessRequest(System.Web.HttpContext).csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
從上述的call stacks 來看,的確符合我們操作的流程,而且我們觀察到有DataGridView的操作,但這並不足以看出Memory Leak的原因。
3. 輸入 "!eeheap -gc" 指令,查看GC的數量以及大小,得到的結果如下:
Number of GC Heaps: 1
generation 0 starts at 0x16342658
generation 1 starts at 0x16023958
generation 2 starts at 0x012d1000
ephemeral segment allocation context: (0x16614fd0, 0x16616ff4)
segment begin allocated size
0022f558 04c67294 04c6d410 0x0000617c(24956)
.............
Large object heap starts at 0x022d1000
segment begin allocated size
022d0000 022d1000 032c5350 0x00ff4350(16728912)
.............
1c870000 1c871000 1ccdd420 0x0046c420(4637728)
Total Size 0x145a1edc(341450460)
------------------------------
GC Heap Size 0x145a1edc(341450460)
我們發現GC的大小就已經3百多MB,因此記憶體的確是.NET的應用程式所佔用。
4. 接下來我們使用"~11s" 指令將thread切換到第11條,並輸入 "!dumpheap -stat" 指令,該指令會將heap中的所有物件的個數及大小依序列舉出來,顯示結果如下(僅列舉最後幾列):
total 4469762 objects
Statistics:
MT Count TotalSize Class Name
654359c8 366 34579680 System.Data.RBTree`1+Node[[System.Int32, mscorlib]][]
65412bb4 366 34579680 System.Data.RBTree`1+Node[[System.Data.DataRow, System.Data]][]
65408b8c 1077500 68960000 System.Data.DataRow
我們可以發現有一百多萬個DataRow物件。這個非常可疑,而且光這些DataRow就6x MB。當然這些DataRow不會單獨存在,勢必是包含在一個或數個DataTable或DataSet當中。 上述資料的第一個欄位MT 表示 "Method Table" .NET會將相同型別的物件使用Method Table集中管理。
5. 輸入 "!dumpheap -mt 65408b8c" 指令 將method table 中的物件列舉出來:
Address MT Size
012d8414 65408b8c 64
.......
01471ab4 65408b8c 64
01471af4 65408b8c 64
01471b34 65408b8c 64
total 4017 objects
6. 輸入 "!do 01471b34" 指令, 將物件的資訊列出:
Name: System.Data.DataRow
MethodTable: 65408b8c
EEClass: 65408b1c
Size: 64(0x40) bytes
(C:\WINNT\assembly\GAC_32\System.Data\2.0.0.0__b77a5c561934e089\System.Data.dll)
Fields:
MT Field Offset Type VT Attr Value Name
65407d48 4000714 4 ...em.Data.DataTable 0 instance 014141f8 _table
654094f4 4000715 8 ...aColumnCollection 0 instance 0141447c _columns
79102290 4000716 18 System.Int32 1 instance 3967 oldRecord
79102290 4000717 1c System.Int32 1 instance 3967 newRecord
79102290 4000718 20 System.Int32 1 instance -1 tempRecord
79102290 4000719 24 System.Int32 1 instance 3968 _rowID
6541d178 400071a 28 System.Int32 1 instance 0 _action
7910be50 400071b 38 System.Boolean 1 instance 0 inChangingEvent
7910be50 400071c 39 System.Boolean 1 instance 0 inDeletingEvent
7910be50 400071d 3a System.Boolean 1 instance 0 inCascade
654088b4 400071e c ...m.Data.DataColumn 0 instance 00000000 _lastChangedColumn
79102290 400071f 2c System.Int32 1 instance 0 _countColumnChange
6541c3f4 4000720 10 ...em.Data.DataError 0 instance 00000000 error
790fd0f0 4000721 14 System.Object 0 instance 00000000 _element
79102290 4000722 30 System.Int32 1 instance 1245184 _rbTreeNodeId
79102290 4000724 34 System.Int32 1 instance 3968 ObjectID
79102290 4000723 484 System.Int32 1 shared static _objectTypeCount
>> Domain:Value 001654f0:NotInit 00207008:NotInit <<
7. DataRow物件有一個_table的欄位,輸入 "!do 014141f8" 指令 將物件資訊列出:
Name: System.Data.DataTable
MethodTable: 65407d48
EEClass: 654078f8
Size: 296(0x128) bytes
(C:\WINNT\assembly\GAC_32\System.Data\2.0.0.0__b77a5c561934e089\System.Data.dll)
Fields:
MT Field Offset Type VT Attr Value Name
7a7567d8 40009a3 4 ...ponentModel.ISite 0 instance 00000000 site
7a755968 40009a4 8 ....EventHandlerList 0 instance 00000000 events
790fd0f0 40009a2 1e4 System.Object 0 shared static EventDisposed
>> Domain:Value 001654f0:NotInit 00207008:NotInit <<
65406ecc 4000799 c System.Data.DataSet 0 instance 0140ac60 dataSet
65409004 400079a 10 System.Data.DataView 0 instance 00000000 defaultView
79102290 400079b d0 System.Int32 1 instance 1077501 nextRowID
....
由於資訊太多,因此僅列出分析需要的部份。我們可以看到DataTable的NextRowID 屬性值是1077501, 表示這個DataTable下一筆新的DataRowID是這個數字,同時可以理解為目前在這個DataTable已經有這麼多筆資料。
8. 接下來我們有幾個思考方向,
a) 我們從!dumpheap -stat 指令所找到的DataRow數量與DataTable裏DataRow的數量一致,因此這些DataRow都屬於同一個DataTable。
b) 是否有其他使用記憶體較多的物件類別?
c) 這些使用佔用記憶體的物件是否是必要的(一百多萬筆資料?)
9. 針對這個dump來看,其實我們已經很了解造成memory leak的原因是由於在程式中keep 一個很大的DataTable, 但並非所有的memory leak 都是如此,也有在session中存放幾MB的資訊, 但同時太多user 在線上,造成記憶體無法即時釋放。 以下再介紹2個相關的指令:
"!gcroot 01471b34" : 這個01471b34 的位址之先前我們所使用的DataRow記憶體位址(在這個例子中,使用任何一個DataRow的位址都可以),我們得到以下結果:
Note: Roots found on stacks may be false positives. Run "!help gcroot" for
more info.
Scan Thread 0 OSTHread 444
Scan Thread 2 OSTHread 29c
Scan Thread 6 OSTHread 1e4
Scan Thread 11 OSTHread 960
ESP:437d690:Root:0ae4facc(System.Web.UI.WebControls.AutoGeneratedField)->
0ae4f3d8(System.Data.DataColumnPropertyDescriptor)->
01414db8(System.Data.DataColumn)->
014141f8(System.Data.DataTable)->
01414438(System.Data.RecordManager)->
14871000(System.Object[])->
01471b34(System.Data.DataRow)
ESP:437d694:Root:16614be8(System.Web.UI.WebControls.DataControlFieldCell)->
0ae4facc(System.Web.UI.WebControls.AutoGeneratedField)->
013fce54(System.Web.UI.WebControls.GridView)->
0ae4efd8(System.Web.UI.WebControls.ReadOnlyDataSourceView)->
0dabcd4c(System.Data.DataView)->
0ae4efa4(System.Collections.Generic.Dictionary`2[[System.Data.DataRow, System.Data],[System.Data.DataRowView, System.Data]])->
1b071000(System.Collections.Generic.Dictionary`2+Entry[[System.Data.DataRow, System.Data],[System.Data.DataRowView, System.Data]][])
Scan Thread 12 OSTHread 888
Scan Thread 13 OSTHread a5c
Scan Thread 14 OSTHread 608
Scan Thread 16 OSTHread a74
這個指令會去callstack, heap裏找與物件相關聯的上層物件(root) 或handle.
!objsize 014141f8 : 這個指令固名思義是會去計算物件的大小,014141f8 是之前用過的DataTable物件。得到的結果如下:
sizeof(014141f8) = 211043384 ( 0xc944438) bytes (System.Data.DataTable)
請注意: 這個指令需要非常久的時間(筆者光算上面這個物件就花了將近20分鐘) 。
在撰寫這篇文章時,我發現先前存下來的dump有問題,因此重現產生了一個dump上傳到下載路徑裏。若您實際在debugging時,可能記憶體位址會與文章裏面的不同,大家可以參考我放在壓縮檔裏面的log。