-
Notifications
You must be signed in to change notification settings - Fork 0
MS_DotNetMemoryLeak
.NETのメモリ・リークについて、
ついて説明する。
- マネージ・リソースであれば、GCで適切に解放される。
- このため、マネージ・オブジェクトでリークするものは、
関数スタックに積まれるローカル変数以外に保持された
マネージ・オブジェクトである可能性が高い。
※ 典型例は、
static フィールド、イベント ハンドラの購読解除漏れ、
キャッシュ/コレクションへの追加しっぱなし、タイマの保持。
いずれも「誰かが参照を握り続けている」ため GC 対象にならない。
- アンマネージ・リソースを使用するマネージコードはリソースを明示的に解放する。
- これを怠ると、マネージコード上でGCが動作していてもメモリリークが発生する。
Marshal クラスを使用してアンマネージ・メモリ確保をする場合は
メモリの使用後に明示的な開放が必要になるので注意する。
(アンマネージ・メモリは .NET オブジェクトと異なり GC 対象とならないため。)
移行メモ(誤記): 原文の「Marshallクラス」は
Marshalクラス(l は1つ)。 名前空間はSystem.Runtime.InteropServices。
メンバにアンマネージ・リソースを持つ .NET オブジェクトには、
IDisposable インターフェイスと対応する実装を施す必要がある。
-
IDisposableが適切に実装されていれば、例外時は GC 任せで
(未 Dispose のオブジェクトは Finalize で Dispose される)、
正常時に Close 呼び出しをするだけで良い
(Close 内で Dispose を呼び出すのが慣例の実装のため)。 - 例外的に Close やファイナライザがあっても Dispose を明示的に呼び出す必要があるものもある
(例えば旧 ODP.NET、今は修正されている)が、これはIDisposableの実装方針に反している。 - オブジェクトの世代によっては解放タイミングが遅れるため、ファイナライザの実行タイミングも遅れる。
これが問題となるような場合は、ファイナライザ任せで処理を実装しないこと(Dispose を明示的に呼び出す)。
補足: 現在の推奨は、
usingステートメント(またはawait using)で 確実にDisposeすること。加えて、
- ファイナライザは原則書かない(
SafeHandleに任せる)。- ファイナライザを持つオブジェクトは Gen0 で回収されず必ず1世代生き延びるため、 むやみに定義するとかえってメモリを圧迫する。
LOH(Large Object Heap、85,000 バイト以上のオブジェクトが置かれる領域)の断片化により、
Gen2 の断片化に起因するメモリリーク(実際には OutOfMemory)が発生することもある
(.NET 3.5 までの問題)。
補足(最新化): .NET Framework 4.5.1 以降は
GCSettings.LargeObjectHeapCompactionModeで LOH の明示的なコンパクションが可能。 .NET Core / .NET 5 以降ではさらに GC が改善されており、 LOH の断片化に起因する OutOfMemory は起きにくくなっている。 ただし「大きな配列を頻繁に作り直さない」(ArrayPool<T>を使う)という 設計上の配慮は依然として有効。
久しぶりに使ったので手順などをメモ(幾つか補足も記載)。
- 添付資料:Book1.xlsx
- VSのイミディエイト・ウィンドウからもSOSデバッガ拡張コマンドを実行できる。
- WinDbgから利用することもできるが、VSの環境変数を使う場合は、CMDから
vsvars32.batを実行後にWinDbg.exeを起動して対象プロセスにアタッチする。
※ 同様の操作をマネージ・ダンプに対して行う事もできる。
以下はSOSデバッガ拡張コマンドの実行例。
-
.load sosコマンドを実行。
.load sos
マネージのみのデバッグ中は、SOS は使用できません。
SOS を読み込むには、プロジェクト プロパティのアンマネージ デバッグを有効にしてください。
- と表示されたら、プロジェクト プロパティのアンマネージ デバッグを有効にする。
拡張 C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\sos.dll は読み込まれました
- と表示されたら、
!DumpHeapコマンドを実行。
!DumpHeap [<-stat>] [-min <size>] [-max <size>] [-thinlock]
[-mt <Method Table address>] [-type <partial type name>] [start [end]]
| オプション | 意味 |
|---|---|
-stat |
統計情報のみ。→ メモリリーク確認ではこれを使用する。 |
-min |
指定サイズ未満のオブジェクトを無視。 |
-max |
指定サイズ超過のオブジェクトを無視。 |
-thinlock |
thinlock を報告する(ロックに利用)。 |
-mt |
指定した Method Table に対応するオブジェクトのみ一覧。 |
-type |
指定した型名(部分文字列可)に対応するオブジェクトのみ一覧。→ 特定のクラスの確認ではこれを使用する。 |
start / end
|
指定したアドレスで一覧を開始/終了。 |
移行メモ(正誤): 原文は
-minを「指定サイズ以下を無視」、-maxを「指定サイズ以上を無視」と説明していたが、意味が逆に読める。 実際は-minが「これ以上の大きさだけ表示(=小さいものを無視)」、-maxが「これ以下の大きさだけ表示(=大きいものを無視)」。上表のとおりに改めた。
!DumpHeap コマンドでは、
- ヒープのオブジェクト情報を一覧できる。
- このコマンドは、メモリリークなどの問題を確認するのに有効である。
- 本番環境で動作するメモリリークをデバッグする場合は、WinDbg などのデバッガを使用すると良い。
指定したクラスのオブジェクトのみ一覧する。
!DumpHeap -Type Class1
Address MT Size
013ac060 009e71fc 20
total 1 objects
Statistics:
MT Count TotalSize Class Name
009e71fc 1 20 WindowsFormsApplication1.Class1
上記のオブジェクトのアドレス情報を !DumpObj に渡すと、オブジェクト情報が得られる。
!DumpObj 013ac060
Name: WindowsFormsApplication1.Class1
MethodTable: 009e71fc
EEClass: 00f01fb8
Size: 20(0x14) bytes
Fields:
MT Field Offset Type VT Attr Value Name
79332d70 4000006 4 System.Int32 1 instance 9 a
79332d70 4000009 24 System.Int32 1 static 9 x
| 項目 | 意味 |
|---|---|
| Name | クラス名 |
| MethodTable | メソッド・テーブル |
| EEClass | CLRクラス定義へのポインタ |
| Size | インスタンスのサイズ |
| Fields | 各種フィールド情報 |
同様に EEClass のアドレスを !DumpClass に渡すとクラス情報が、
Module のアドレスを !DumpModule に渡すとモジュール情報(PEFile)が得られる。
!DumpMT -md <Method Table address> で Method Table 情報(JIT 状況を含む)を確認できる。
- JIT 列の意味
- PreJIT:プリコンパイル済み。
- JIT:JITコンパイル済み。
- NONE:JITコンパイルが行われていない。
クラスレイアウトについて
インスタンス
└メソッドテーブル ┬ EEClass ─ BaseClass
├ MT of BaseClass
└ Module ┬ Field Descriptor Map
└ TypeDef To Method Table Map
-
!DumpHeap -statで、マネージヒープの統計情報が出力される。
!DumpHeap -stat
total 10311 objects
Statistics:
MT Count TotalSize Class Name
009e6c90 1 20 WindowsFormsApplication1.Class1
009e5e50 1 332 WindowsFormsApplication1.Form1
-
!DumpHeap -mt <Method Table address>で、指定したクラスに対する各インスタンス情報が得られる。 -
!gcroot <Object address>で、指定したオブジェクトに対する参照(ルート)情報が得られる。
★ この参照を辿って行くことで参照を握っているルートオブジェクトを確認できる。
!gcroot 013bd8f4
ebx:Root:013be34c(System.Windows.Forms.Application+ThreadContext)->
013bd8f4(WindowsFormsApplication1.Form1)
...
013dc328(Microsoft.Win32.UserPreferenceChangedEventHandler)->
013bd8f4(WindowsFormsApplication1.Form1)
※ この例の最後の行のように、UserPreferenceChangedEventHandler(static イベント)が
Form を掴んでいるのは、イベント購読解除漏れによるリークの典型パターン。
- その他、以下のコマンドを利用できる。
| コマンド | 用途 |
|---|---|
!eeheap -gc |
GC ヒープ(世代・セグメント・LOH)の情報 |
!objsize <addr> |
参照先オブジェクトも含めたサイズ |
!finalizequeue |
ファイナライザキューにたまっているオブジェクトの一覧 |
~* e !clrstack |
全マネージスレッドのスタックトレース(ファイナライザスレッドのハング確認) |
!Threads -special |
スレッド一覧と特殊スレッド(Finalizer 等)の確認 |
- 時間をおいて
!DumpHeap -statを2回取得し、Count / TotalSize が増え続けている型を特定。 - その型の Method Table で
!DumpHeap -mt→ インスタンスのアドレスを取得。 -
!gcrootで誰が参照を握っているかを辿る。
補足(最新化): .NET Core 以降では、SOS は
dotnet-sos(dotnet tool install -g dotnet-sos→dotnet-sos install)で導入する。 さらに、WinDbg を使わずコマンドラインで完結するdotnet-dump(ダンプ取得+dotnet-dump analyzeでdumpheap/gcroot実行)、dotnet-counters(GC やヒープのカウンタ監視)、dotnet-gcdump(GC ヒープのみを軽量に採取、Visual Studio で開ける)が使える。 Linux コンテナ上の .NET でも同じ手順が使えるのが利点。
WinDbg と Silverlight をインストールしておき、ダンプファイルに対して
Silverlight 同梱の sos.dll を .load して同様の操作を行う。
.load C:\Program Files\Microsoft Silverlight\<version>\sos.dll
!dumpheap -stat
!dumpheap -stat -type XXXXXXX
※ Silverlight 自体はサポート終了済み(2021年10月)。
Visual Studio 2015 Enterprise Edition ではマネージ メモリの分析機能が
強化されており、!DumpHeap コマンドで実現できることに近い内容が確認できる。
.NET Framework アプリケーションのデバッグに有用
(ただし、サポートは提供されておらずドキュメントや公開情報も少ない状況)
- psscor4(
.load "C:\Psscor4\x86\x86\psscor4.dll"→!psscor4.help) - mex(
.load mex.dll→!mex.help)
Tags: 障害対応, 性能, デバッグ
このWikiは「Open棟梁Project」,「OSSコンソーシアム 開発基盤部会」によって運営されています。