[.NET] WindowsMobile上のC#アプリのメモリリークを解決せよ!

先週、思いっきりハマッた話です。
そもそも、自分が直接かかわっていないプロジェクトでいきなり日曜に呼び出されて、うんぬんかんぬん・・・ま、そいういった愚痴は置いといてw
このプロジェクトではあるWindowsMobile端末を使用しており、その上で動作する.NET(言語はC#)のアプリケーションにてメモリリークが発生しており、運用上使い物にならないレベルということだった。問題はあるフォームを「ShowDialog()」で開いて閉じてを繰り返すだけで、50KBずつリークするということはわかっていた。
何も確認せずにとりあえず、試してみてもらったのは以下の2つ。

  • メンバ変数でDataSetを使用しているようであれば、必ずDispos()すること。
  • フォームを閉じる際に、フォーム上のコントロールを全て明示的にDispose()すること。

しかし、上記の対応では効果が無かった(結果からすると、いい線をいっていたんだけど足りなかった・・・)。

で、こっから何が問題か?を特定するわけなんですが、どうにもこうにもWindows MobileアプリにはDevPartnerdotTrace Profilerといったツールが適用できない。
(実際にはどうにか適用できるのかも?知れないのですが、その方法は今でもわかりません。。。)
また、通常のWindowsアプリであればProfilerなんかも使って何が原因か?(ほんとにメモリリークなのか?)も調査するのですが、これまたWindows Mobileアプリだと何もわからん(スナップショットは取れるんだけど・・・それじゃぁねぇ)。で、Platform Builderとかいうのを利用すれば何かできるかもしれない?という情報もありましたが、そんなもん調べて適用している暇もなかったのでスルー(その後、何が出来るか?は、調べてもらっている最中でしてまだわかってません・・・)。
じゃぁ、どうすんの?という話ですけど。。
怪しそうなところに「GC.GetTotalMemory(true)」を入れてログにはかせるという地道な作業!
このGetTotalMemoryはその名前のとおり、マネージ メモリに現在割り当てられているメモリのバイト数を取得できます。尚、引数で「true」を指定すると、GC.Collectが動作した後の値を返してくれるので実施リークしているかどうかを確認するための目安になります。ほんとにこのぐらいしか方法がないのか?お馬鹿なワシにはわからんのです。。誰かもっといい方法を知っていたら、教えてください。
で、この方法でひたすら地道に調査した結果、リーク量を「50KB」から「1KB以下」に押さえ込むことに成功しました。えぇ、完全にゼロまではいけませんでしたが、業務上問題ないレベルであるということでOKはもらいましたけど(ライフワークとしてゼロにする調査を続けろとは言われましたが・・・)。
以下、原因と対策について。
[原因1]
.NETのメモリリークといえばDataSetを疑えというぐらい、DataSetがいつも悪さをしてくれます。
上記のログを埋め込んでリークする50KBのほとんどがDataSetであることがわかったのですが、Dispose()&nullセットをしても一向に解放されない。ということは、どっかで参照が残っているとことになるので、そいつを探したら・・・いましたっ!
問題のFormではメンバ変数でDataTableを使用しており、それとは別にDataRowのメンバ変数も存在。
で、あるメソッド内でDataRowにDataTableの参照をセットしていて、DataRowにnullをセットしていなかったためにDataTableの参照が残ったままになりいくらGC.Collectを実行しても解放されなかったわけです。こんなことツール使えればすぐにわかっただろうにとは思いますけど。。。
こいつで40KBほど減って、残り10KB近くリークが残った。
[原因2]
ログを埋め込んでいくと、Formのコンストラクタで実行される「InitializeComponent()」で確保されたメモリが解放されていないことが見て取れた。FormのDispose()処理に以下のコードを追記して、Form上のコントロールのDispose()はやっているのに何で?と。

protected override void Dispose(bool disposing)
{
foreach (System.Windows.Forms.Control ctrl in this.Controls)
{
ctrl.Dispose();
}
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}

試しに、Dispose()だけでなく、それぞれのコントロールにnullもセットするようにしたら・・・10KBも解放されるようになるではないですか!

protected override void Dispose(bool disposing)
{
foreach (System.Windows.Forms.Control ctrl in this.Controls)
{
ctrl.Dispose();
}
// コントロールは個々にnullをセット
txtControl1 = null;
txtControl2 = null;
:
:
:
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}

え~そうなの~!?という感じですよ。。。
後から教えてもらった以下のサイトにも同様のことが書いてありました。

[教訓]
必要の無くなったDataSet、Form上のコントロールは明示的にDispose()&nullセットしといたほうが無難というところでしょうか。何とも腑に落ちない話ではありますが。

コメント

  1. まんちんぽん より:

    > ライフワークとしてゼロにする調査を続けろ・・・
    www

  2. まりお より:

    (;´Д`)

Wordpress Social Share Plugin powered by Ultimatelysocial