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。