dotMemory 2024.1 ヘルプ

メモリリークを見つける

サンプルアプリケーション

このチュートリアルでは、dotMemory を使用してアプリケーションでメモリリークを見つけて修正する方法を説明します。しかし、前に進む前に、メモリリークが何であるかに同意しましょう。

メモリリークとは何ですか?

最も一般的な定義によれば、メモリリークは、「オブジェクトがメモリに格納されているが、実行中のコードからアクセスできない」という誤ったメモリ管理の結果です。さらに、「メモリリークは時間とともに増加し、クリーンアップされなければシステムは最終的にメモリ不足になります」

実際には、上記の定義に厳密に従えば、.NET アプリケーションでは「古典的な」メモリリークは不可能です。ガベージコレクタ(GC)は、メモリの解放を完全に制御し、コードによってアクセスできないすべてのオブジェクトを削除します。さらに、アプリケーションが終了すると、GC はアプリが占めるメモリを完全に解放します。それにもかかわらず、ポイント #2(リークのためにメモリが枯渇)はかなり現実的です。もちろん、これはシステムをクラッシュさせませんが、遅かれ早かれ、アプリケーションは OutOfMemory 例外を発生させます。

なぜこれが起こるのですか? は、GC は参照されていないオブジェクトのみを収集します。不明なオブジェクトへの参照がある場合、GC はオブジェクトを収集しません。メモリリークを修正する際の主な戦術は、時間の経過とともに加算されるオブジェクト(リークの原因)と、以前のオブジェクトをメモリに保持しているオブジェクトを特定することです。

サンプルアプリケーションでリークを修正するためのこの方法を試してみましょう。

サンプルアプリケーション

繰り返しになりますが、チュートリアルで使用するアプリは、コンウェイのライフゲームです。先に進む前に、github(英語) からアプリケーションをダウンロードしてください。人生ゲームの開発に費やしたお金を返還し、ユーザーにさまざまな広告を表示するウィンドウを追加することにしたとしましょう。最悪の慣行に従い、ユーザーがゲームオブライフを開始する(開始ボタンをクリックする)たびに広告ウィンドウが表示されます。ユーザーがバナーをクリックすると、ある Web サイトにリダイレクトされ、広告ウィンドウが閉じられます(ユーザーは、標準の閉じるボタンを使用してウィンドウを閉じることもできますが、本当に望んでいることではありません)。広告を変更するために、広告ウィンドウはタイマー(DispatcherTimer クラスに基づく)を使用します。AdWindow.cs ファイルで AdWindow クラスの実装を確認できます。

T2 Gol App

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

ステップ 1. dotMemory を実行

  1. Visual Studio で Game of Life ソリューションを開きます。

  2. メニュー ReSharper | プロファイル | スタートアッププロジェクトのメモリプロファイリングを実行しています ... を使用して dotMemory を実行します。

    T2 Resharper Menu Upd D M

    これにより、プロファイラ設定ウィンドウが開きます。

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

    T2 Profiler Conf
  4. プロファイリングセッションを開始するには、実行をクリックします。これは私たちのアプリを実行し、dotMemory のメイン分析ページを開きます。

ステップ 2. スナップショットを取得する

アプリが起動すると、メモリスナップショットを取得できます。新しい広告ウィンドウをテストし、その使用箇所がメモリ使用量にどのように影響するかを確認するには、ウィンドウが表示された直後(このスナップショットを比較の基準として使用します)と別のスナップショット広告ウィンドウは閉じられます。2 番目のスナップショットは、GC がウィンドウをメモリから削除するために必要です。

  1. アプリで開始ボタンをクリックしてゲームを開始します。広告ウィンドウが表示されます。

    T2 Gol App
  2. dotMemory のスナップショットを取得するボタンをクリックします。

    T2 Get Snapshot1

    これによりデータがキャプチャーされ、スナップショット領域にスナップショットが追加されます。スナップショットを取得してもプロファイリングプロセスが中断されないため、別のスナップショットを取得できます。

  3. アプリケーションの広告ウィンドウを閉じます。

  4. dotMemory のスナップショットを取得するボタンをクリックすると、もう一度スナップショットを取得できます。

  5. Game of Life アプリケーションを終了してプロファイリングセッションを終了します。メインページには 2 つのスナップショットが含まれています。

    T2 Get Snapshot2

ステップ 3. スナップショットを比較する

次に、2 つの収集スナップショットを比較して対比します。何を見たいのですか? すべて正常に機能する場合、広告ウィンドウは最初のスナップショットには存在するが、2 番目のスナップショットには存在しないはずです。見てみましょう。

  1. スナップショットごとに比較に追加をクリックして比較領域に追加します。スナップショットを追加する順序は重要ではありません。これは、dotMemory は常に以前のスナップショットを比較の基礎として使用するためです。

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

    T2 Snapshot Comparison View

    このビューには、作成された特定のクラスのオブジェクト(新規オブジェクト列)と、スナップショット間で削除されたオブジェクト(デッドオブジェクト列)の数が表示されます。生き残ったオブジェクトは、ガベージコレクションで生き残ったオブジェクトの数、つまり両方のスナップショットに存在するオブジェクトの数を示します。現在、AdWindow クラスに興味があります。

  3. AdWindow クラスの発見を容易にするために、すべてのオブジェクトをそれらが属する名前空間でソートしましょう。これを行うには、テーブルの上部にあるグループ化リストの名前空間をクリックします。

  4. GameOfLife 名前空間を開きます。

    T2 Snapshot Comparison Namespace

    あれは何でしょう? GameOfLife.AdWindow オブジェクトは生存しているオブジェクト列にあります。つまり、広告ウィンドウはまだ生きています。ウィンドウを閉じた後、対応するオブジェクトがヒープから削除されているはずです。それにもかかわらず、何かが収集されないようにしています。

調査を開始し、広告ウィンドウが削除されていない理由を確認してください。

ステップ 4. スナップショットを分析する

dotMemory を始めるにはチュートリアルで記述されていたように、dotMemory でのあなたの作業は犯罪調査の際に考えなければなりません。容疑者(オブジェクト)の膨大なリストを分析して調査を開始し、問題の原因となるものが見つかるまで継続的にリストを絞り込みます。推論のあなたのチェーンは、dotMemory ウィンドウの左側のいわゆる Analysis Path に表示されます。

このアプローチを実際に試してみましょう:

  1. 生存している GameOfLife.AdWindow インスタンスを開きます。これを行うには、GameOfLife.AdWindow クラスの横にある生存しているオブジェクト列の番号 1 をクリックします。

    T2 Select Snapshot

    両方のスナップショットにオブジェクトが存在するため、dotMemory はオブジェクトを表示するスナップショットを指定するように指示します。もちろん、ウィンドウが収集されるべき最後のスナップショットに興味があります。

  2. 新しいスナップショットの中の "Survived Objects" を開くを選択し、OK をクリックします。

    T2 Adwindow Instance

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

  3. ビューのリストでキー保持パスをクリックします。

    T2 Instance Retention Paths

    ご覧のように、広告ウィンドウはイベントハンドラー EventHandler によってメモリ内に保持され、イベントハンドラー EventHandlerDispatcherTimer クラスのインスタンスによって参照されます。

    T2 Tick Event

    DispatcherTimer インスタンスの上にあるテキストは、もう一つの手がかりを与えます。インスタンスは Tick イベントハンドラーを介して参照されます。次に、どのメソッドがインスタンスを Tick イベントハンドラーに登録しているかを見て、コードを徹底的に見てみましょう。

  4. グラフの EventHandler インスタンスをクリックします。

    T2 Eventhandler Instance

    これにより、デフォルトの発信参照ビューで EventHandler インスタンス * が開きます。必要とするのは、インスタンスを作成するメソッドを決定することだけです。

  5. 必要な方法をすばやく見つけるには、作成スタックトレースビューに切り替えます。

    T2 Instance Stack Trace

    こちらで確認できます ! タイマーを実際に作成するスタック内の最新の呼び出しは、AdWindow コンストラクターです。コード内で見つけよう。

  6. 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; }
  7. ここで、リークが修正されたことを確認するために、ソリューションを構築してプロファイリングを再度実行してみましょう。これを行うには、ステップ 2. スナップショットを取得するステップ 3. スナップショットの比較の手順を繰り返すことができます。

    T2 Snapshot Comparison Fixed

    以上です ! AdWindow インスタンスがデッドオブジェクト列に追加されました。これは、2 番目のスナップショットを取得するまでに正常に収集されたことを意味します。リークは修正されました !

正直なところ、この種のリークは非常に頻繁に発生します。実際、dotMemory は、このタイプのリークがないかアプリを自動的にチェックすることがよくあります。

リークを含む 2 番目のスナップショットを開いてインスペクションビューを見ると、イベントハンドラーのリークチェックにすでに AdWindow オブジェクトが含まれていることがわかります。

T2 Inspections

ステップ 5. 他の漏れをチェックする

イベントハンドラーのリークが修正され、ガベージコレクタによって広告ウィンドウが正常に収集されるようになりました。しかし、漏れの原因となったタイマーはどうですか? すべて正常に動作する場合は、タイマーも収集する必要があり、2 番目のスナップショットには存在しないはずです。見てみましょう。

  1. dotMemory で 2 番目のスナップショットを開きます。これを行うには、分析パスで GameOfLife.exe のプロファイリングステップ(調査の開始)をクリックしてから、2 番目のスナップショットのスナップショット #2 リンクをクリックします。

    T2 2nd Leak Session
  2. タイプをクリックして、スナップショットの型別にグループ化ビューを開きます。

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

    T2 2nd Leak Type List
  4. このオブジェクトセットをダブルクリックして開きます。

    T2 2nd Leak Timer Obj Set

    これにより、型別にグループ化ビューのセットが開きます。今度は、このセットに広告ウィンドウで作成されたタイマーが含まれていないことを確認する必要があります。タイマーが AdWindow コンストラクターで作成されたため、これを行う最も簡単な方法は、バックトレースビューを使用してセットを見ることです。

  5. ビューのリストでバックトレースをクリックします。ビューは、オブジェクトを直接作成したものから始まり、スタックの最初の呼び出しに降りる呼び出しを表示します。

    T2 2nd Leak Back Traces

    残念ながら、AdWindow.ctor(Window owner) コールはまだこちらで確認できます。つまり、このコールで作成されたタイマーは収集されませんでした。スナップショットには、広告ウィンドウが閉じられてメモリから削除されたかどうかに関係なく存在します。これは解析すべきもう一つのメモリリークのようです。

  6. AdWindow.ctor(Window owner) コールをダブルクリックします。dotMemory は、この呼び出しによって作成された DispatcherTimer クラスのインスタンスを表示します。デフォルトでは、発信参照ビューが使用されます。次に、このインスタンスがどのようにメモリ内に保持されているかを調べる必要があります。キー保持パスビューを使ってみましょう。

  7. キー保持パスをクリックします。ご覧のとおり、2 つの主要な保存経路があります。

    T2 2nd Leak Key Paths

    タイマーの最初の保持パスは、DispatcherTimer リストにつながります。これは、グローバルであり、すべてのタイマーをアプリケーションに保存します。2 番目の方法は、タイマーが DispatcherOperationCallback オブジェクトによっても保持されることを示しています。このオブジェクトは、タイマーの実行時に作成されるデリゲートです。これは、タイマーがまだ実行中であることを意味します。DispatcherTimer クラスの特色の 1 つは、タイマーが停止した後にのみインスタンスがグローバルタイマーリストから削除されることです。リークを修正するには、広告ウィンドウを閉じる前にタイマーを停止する必要があります。コードでこれをやりましょう !

  8. AdWindow クラスの実装を含む AdWindow.cs ファイルを開きます。実際には、修正は非常に簡単になります。adTimer.Stop(); 行を Unsubscribe() メソッドに追加するだけです。修正後、メソッドは次のようになります。

    public void Unsubscribe() { _adTimer.Tick -= ChangeAds; _adTimer.Stop(); }
  9. 解決策を再構築します。

  10. ステップ 2. スナップショットを取得するを繰り返します。

  11. 型別にグループ化ビューで 2 番目のスナップショットを開き、System.Windows.Threading.DispatcherTimer タイプのすべてのオブジェクトを検索します。

    T2 2nd Leak Fixed

    ご覧のとおり、7 ではなく 6 つの DispatcherTimer オブジェクトしかありません。ガベージコレクタが広告ウィンドウで使用されているタイマーを確実に収集するように、バックトレースビューを使用してこれらのタイマーを見てみましょう。

  12. DispatcherTimer オブジェクトをダブルクリックし、ビューのリストでバックトレースをクリックします。

    T2 2nd Leak Fixed Back Traces

    すばらしいです ! リストには AdWindow コンストラクターがありません。これは、リークが正常に修正されたことを意味します。

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