メモリリークを見つける
サンプルアプリケーション | |
このチュートリアルでは、dotMemory を使用してアプリケーションでメモリリークを見つけて修正する方法を説明します。 しかし、前に進む前に、メモリリークが何であるかに同意しましょう。
メモリリークとは何ですか?
最も一般的な定義によると、メモリリークは「オブジェクトがメモリに格納されているが、実行中のコードからアクセスできない」ような不適切なメモリ管理の結果とされています。さらに「メモリリークは時間の経過とともに蓄積し、解消されなければ最終的にはシステムがメモリ不足に陥ります」。
実際には、上記の定義に厳密に従えば、.NET アプリケーションでは「古典的な」メモリリークは不可能です。 ガベージコレクタ(GC)は、メモリの解放を完全に制御し、コードによってアクセスできないすべてのオブジェクトを削除します。 さらに、アプリケーションが終了すると、GC はアプリが占めるメモリを完全に解放します。 それにもかかわらず、ポイント #2(リークのためにメモリが枯渇)はかなり現実的です。 もちろん、これはシステムをクラッシュさせませんが、遅かれ早かれ、アプリケーションは OutOfMemory 例外を発生させます。
なぜこのようなことが起こるのでしょうか? 実は、GC が回収するのは 参照されていないオブジェクトだけです。 知らない参照がオブジェクトに存在する場合、GC はそのオブジェクトを回収しません。 したがって、メモリリークを修正する主な方法は、時間の経過とともに増加するオブジェクト(リークの原因)と、それらをメモリ内に保持しているオブジェクトを特定することです。
サンプルアプリケーションでリークを修正するためのこの方法を試してみましょう。
サンプルアプリケーション
繰り返しになりますが、チュートリアルで使用するアプリは、コンウェイのライフゲームです。 先に進む前に、 github(英語) からアプリケーションをダウンロードしてください。 人生ゲームの開発に費やしたお金を返還し、ユーザーにさまざまな広告を表示するウィンドウを追加することにしたとしましょう。 最悪の慣行に従い、ユーザーがゲームオブライフを開始する(開始 ボタンをクリックする)たびに広告ウィンドウが表示されます。 ユーザーがバナーをクリックすると、ある Web サイトにリダイレクトされ、広告ウィンドウが閉じられます(ユーザーは、標準の閉じるボタンを使用してウィンドウを閉じることもできますが、本当に望んでいることではありません)。 広告を変更するために、広告ウィンドウはタイマー(DispatcherTimer クラスに基づく)を使用します。 AdWindow.cs ファイルで AdWindow クラスの実装を確認できます。

この機能が追加され、今テストに最適な時期です。 dotMemory を実行して、広告ウィンドウがアプリケーションのメモリ使用に影響を与えないようにしてください(言い換えると、正しく割り当てられて収集されます)。
ステップ 1. dotMemory を実行
Visual Studio で Game of Life ソリューションを開きます。
メニュー を使用して dotMemory を実行します。

これにより、 プロファイラ設定 ウィンドウが開きます。
プロファイラ設定 ウィンドウで、 開始からメモリ割り当てとトラフィックデータを収集する を選択します。 これにより、dotMemory は、アプリの起動直後にプロファイリングデータの収集を開始します。 オプションを指定した後のウィンドウの外観は次のとおりです。

プロファイリングセッションを開始するには、 実行 をクリックします。 これは私たちのアプリを実行し、dotMemory のメイン分析ページを開きます。
ステップ 2. スナップショットを取得する
アプリが起動すると、メモリスナップショットを取得できます。 新しい広告ウィンドウをテストし、その使用箇所がメモリ使用量にどのように影響するかを確認するには、ウィンドウが表示された直後(このスナップショットを比較の基準として使用します)と別のスナップショット広告ウィンドウは閉じられます。 2 番目のスナップショットは、GC がウィンドウをメモリから削除するために必要です。
アプリで 開始 ボタンをクリックしてゲームを開始します。 広告ウィンドウが表示されます。

dotMemory の スナップショットを取得 ボタンをクリックします。

これによりデータがキャプチャーされ、スナップショット領域にスナップショットが追加されます。 スナップショットを取得してもプロファイリングプロセスが中断されないため、別のスナップショットを取得できます。
アプリケーションの広告ウィンドウを閉じます。
dotMemory の スナップショットを取得 ボタンをクリックすると、もう一度スナップショットを取得できます。
Game of Life アプリケーションを終了してプロファイリングセッションを終了します。 メインページには 2 つのスナップショットが含まれています。

ステップ 3. スナップショットを比較する
さて、収集したスナップショットを比較します。 何を見たいのでしょうか? すべてが正常に動作していれば、広告ウィンドウは最初のスナップショットに存在し、2 番目のスナップショットには存在しないはずです。 見てみましょう。
各スナップショットの 比較に追加 をクリックして、比較エリアに追加します。 スナップショットを追加する順番は重要ではありません。dotMemory が常に古いスナップショットを比較の基準にするためです。

比較領域で 比較 をクリックします。 これにより、 スナップショットの比較 ビューが開きます。

このビューには、作成された特定のクラスのオブジェクト(新規オブジェクト 列)と、スナップショット間で削除されたオブジェクト(デッドオブジェクト 列)の数が表示されます。 生き残ったオブジェクトは、ガベージコレクションで生き残ったオブジェクトの数、つまり両方のスナップショットに存在するオブジェクトの数を示します。 現在、
AdWindowクラスに興味があります。AdWindowクラスの発見を容易にするために、すべてのオブジェクトをそれらが属する名前空間でソートしましょう。 これを行うには、テーブルの上部にある グループ化 リストの 名前空間 をクリックします。GameOfLife名前空間を開きます。
それは何でしょうか?
GameOfLife.AdWindowオブジェクトは 生存オブジェクト 列にあり、これは広告ウィンドウがまだ生存していることを意味しています。 ウィンドウを閉じた後、対応するオブジェクトがヒープから削除されているはずです。 それにもかかわらず、何かが収集されないようにしています。
調査を開始し、広告ウィンドウが削除されていない理由を確認してください。
ステップ 4. スナップショットを分析する
dotMemory を始めるにはチュートリアルで記述されていたように、dotMemory でのあなたの作業は犯罪調査の際に考えなければなりません。 容疑者(オブジェクト)の膨大なリストを分析して調査を開始し、問題の原因となるものが見つかるまで継続的にリストを絞り込みます。 推論のあなたのチェーンは、dotMemory ウィンドウの左側のいわゆる Analysis Path に表示されます。
このアプローチを実際に試してみましょう:
生存している
GameOfLife.AdWindowインスタンスを開きます。 これを行うには、GameOfLife.AdWindowクラスの横にある 生存オブジェクト 列の番号 1 をクリックします。
両方のスナップショットにオブジェクトが存在するため、dotMemory はオブジェクトを表示するスナップショットを指定するように指示します。 もちろん、ウィンドウが収集されるべき最後のスナップショットに興味があります。
新しいスナップショットの中の "Survived Objects" を開く を選択し、 OK をクリックします。

これにより、「スナップショット #1 および #2 の両方に存在する
AdWindowクラスのインスタンス」というインスタンスが表示されます。 インスタンスの可能なビューのリストは、オブジェクトセットのビューのリストとは異なることに注意してください。 例: オブジェクトインスタンスの既定のビューは、他のオブジェクトへのインスタンスの参照のツリーを表示する 出力方向の参照 です。 ただし、関心があるのは、AdWindowによって参照されるオブジェクトではなく、それを参照するオブジェクト、つまり、広告ウィンドウをメモリに保持するオブジェクトのみです。 これを把握するには、 キー保持パス ビューに切り替えることができます。 このビューには、保持パスのグラフが表示されます。 ビューには、 すべての可能なパスが表示されるのではなく、互いに最も大きく異なるパスのみが表示されることに注意してください。 これにより、非常に類似した保持パスが大量に除外され、分析が簡素化されます。ビューのリストで キー保持パス をクリックします。

ご覧のとおり、広告ウィンドウはイベントハンドラー
EventHandlerによってメモリ内に保持されており、そのイベントハンドラーはDispatcherTimerクラスのインスタンスによって参照されています。
DispatcherTimerインスタンス上部の説明文から、インスタンスがTickイベントハンドラー経由で参照されていることが分かります。 では、どのメソッドが自分のインスタンスをTickイベントハンドラーに登録しているか調べて、コードを詳しく見てみましょう。グラフの
EventHandlerインスタンスをクリックします。
これにより、デフォルトの 出力方向の参照 ビューで
EventHandlerインスタンス * が開きます。 必要とするのは、インスタンスを作成するメソッドを決定することだけです。必要な方法をすばやく見つけるには、 作成スタックトレース ビューに切り替えます。

こちらで確認できます ! タイマーを実際に作成するスタック内の最新の呼び出しは、
AdWindowコンストラクターです。 コード内で見つけよう。GameOfLife ソリューションで Visual Studio に切り替えて、
AdWindowコンストラクターを探します。public AdWindow(Window owner) { ... _adTimer = new DispatcherTimer {Interval = TimeSpan.FromSeconds(3)}; _adTimer.Tick += ChangeAds; _adTimer.Start(); }ご覧のとおり、広告ウィンドウは
ChangeAdsメソッドでイベントを処理しています。 でも、なぜ広告ウィンドウは閉じた後もメモリに残るのでしょうか? それは、ウィンドウをタイマーのイベントに登録したまま、解除し忘れていたからです。 したがって、このリークの修正は簡単です。広告ウィンドウを閉じるときに呼ばれるUnsubscribe()メソッドを追加する必要があります。 実際、そのようなメソッドはすでにコードに含まれており、必要なのはウィンドウのOnClosedイベントでUnsubscribe();行のコメント解除だけです。 最終的にコードは次のようになります:protected override void OnClosed(EventArgs e) { Unsubscribe(); base.OnClosed(e); } public void Unsubscribe() { _adTimer.Tick -= ChangeAds; }リークが修正されたかを確認するため、ソリューションをビルドして再度プロファイリングを実行しましょう。 ステップ 2 を繰り返します。 スナップショットを取得と ステップ 3。 スナップショットを比較。

これで完了です!
AdWindowインスタンスは現在 デッドオブジェクト 列にあります。これは、2 番目のスナップショット取得時までに正常に回収されたことを意味します。 リークは修正されました!
正直なところ、この種のリークは非常に頻繁に発生します。 実際、dotMemory は、このタイプのリークがないかアプリを 自動的にチェックすることがよくあります。
リークを含む 2 番目のスナップショットを開いて インスペクション ビューを見ると、 イベントハンドラーのリーク チェックにすでに AdWindow オブジェクトが含まれていることがわかります。

ステップ 5. 他の漏れをチェックする
イベントハンドラーのリークを修正し、広告ウィンドウは無事ガベージコレクターによって回収されました。 しかし、このリークの原因となったタイマーはどうでしょうか? すべてが正常に動作していれば、タイマーも回収され、2 番目のスナップショットには存在しないはずです。 見てみましょう。
dotMemory で 2 番目のスナップショットを開きます。 これを行うには、分析パスで GameOfLife.exe のプロファイリング ステップ(調査の開始)をクリックしてから、2 番目のスナップショットの Snapshot #2 リンクをクリックします。

タイプ をクリックして、スナップショットの タイプ ビューを開きます。
開いた タイプ ビューで、フィルターフィールドに dispatchertimerと入力します。 これによりリストが絞り込まれ、クラス名にこのパターンを含むオブジェクトのみが残ります。 ご覧のとおり、ヒープには 7 つの
System.Windows.Threading.DispatcherTimerオブジェクトがあります。
このオブジェクトセットをダブルクリックして開きます。

これにより、 タイプ ビューのセットが開きます。 今度は、このセットに広告ウィンドウで作成されたタイマーが含まれていないことを確認する必要があります。 タイマーが
AdWindowコンストラクターで作成されたため、これを行う最も簡単な方法は、 バックトレース ビューを使用してセットを見ることです。ビューのリストで バックトレース をクリックします。 ビューは、オブジェクトを直接作成したものから始まり、スタックの最初の呼び出しに降りる呼び出しを表示します。

残念ながら、
AdWindow.ctor(Window owner)呼び出しはまだ存在し、この呼び出しで作成されたタイマーが回収されていないことを意味します。 広告ウィンドウが閉じられてメモリから削除されたにも関わらず、このタイマーはスナップショットに存在しています。 これはもう一つのメモリリークを解析する必要がありそうです。AdWindow.ctor(Window owner)コールをダブルクリックします。 dotMemory は、この呼び出しによって作成されたDispatcherTimerクラスのインスタンスを表示します。 デフォルトでは、 出力方向の参照 ビューが使用されます。 次に、このインスタンスがどのようにメモリ内に保持されているかを調べる必要があります。 キー保持パス ビューを使ってみましょう。キー保持パス をクリックします。 ご覧のとおり、2 つの主要な保存経路があります。

タイマーの最初の保持パスは、アプリケーション内のすべてのタイマーを格納するグローバルな
DispatcherTimerリストに繋がっています。 2 番目の方法では、タイマーがDispatcherOperationCallbackオブジェクトからも保持されていることが示されています。 このオブジェクトは、タイマーを実行すると作成されるデリゲートです。 これはタイマーがまだ実行中であることを意味します。DispatcherTimerクラスの特徴の一つは、タイマーが停止した場合のみグローバルタイマーリストからそのインスタンスが削除されることです。 したがって、リークを修正するには、広告ウィンドウが閉じられる前にタイマーを停止する必要があります。 これをコードで実行しましょう!AdWindowクラスの実装を含む AdWindow.cs ファイルを開きます。 実際には、修正は非常に簡単になります。adTimer.Stop();行をUnsubscribe()メソッドに追加するだけです。 修正後、メソッドは次のようになります。public void Unsubscribe() { _adTimer.Tick -= ChangeAds; _adTimer.Stop(); }解決策を再構築します。
タイプ ビューで 2 番目のスナップショットを開き、
System.Windows.Threading.DispatcherTimerタイプのすべてのオブジェクトを検索します。
ご覧のとおり、7 ではなく 6 つの
DispatcherTimerオブジェクトしかありません。 ガベージコレクタが広告ウィンドウで使用されているタイマーを確実に収集するように、 バックトレース ビューを使用してこれらのタイマーを見てみましょう。DispatcherTimer オブジェクトをダブルクリックし、ビューのリストで バックトレース をクリックします。

素晴らしい! リストに
AdWindowコンストラクターが存在しないため、リークが正常に修正されたことを意味します。
もちろん、このタイプのリークは特にアプリケーションにとって重要ではないようです。 dotMemory を使用しなかった場合、この問題に気付かなかったかもしれません。 それにもかかわらず、他のアプリでは(たとえば、サーバーサイドのものが 24 時間 365 日働いている)、 OutOfMemory 例外を引き起こしてこの漏れが現れることがあります。