メモリリークを見つける
サンプルアプリケーション | |
このチュートリアルでは、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 つの収集スナップショットを比較して対比します。何を見たいのですか? すべて正常に機能する場合、広告ウィンドウは最初のスナップショットには存在するが、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
によってメモリ内に保持され、イベントハンドラー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 番目のスナップショットのスナップショット #2 リンクをクリックします。
タイプをクリックして、スナップショットの型別にグループ化ビューを開きます。
開いた型別にグループ化ビューで、フィルターフィールドに dispatchertimer と入力します。これによりリストが絞り込まれ、クラス名にこのパターンを含むオブジェクトのみが残ります。ご覧のとおり、ヒープには 7 つの
System.Windows.Threading.DispatcherTimer
オブジェクトがあります。このオブジェクトセットをダブルクリックして開きます。
これにより、型別にグループ化ビューのセットが開きます。今度は、このセットに広告ウィンドウで作成されたタイマーが含まれていないことを確認する必要があります。タイマーが
AdWindow
コンストラクターで作成されたため、これを行う最も簡単な方法は、バックトレースビューを使用してセットを見ることです。ビューのリストでバックトレースをクリックします。ビューは、オブジェクトを直接作成したものから始まり、スタックの最初の呼び出しに降りる呼び出しを表示します。
残念ながら、
AdWindow.ctor(Window owner)
コールはまだこちらで確認できます。つまり、このコールで作成されたタイマーは収集されませんでした。スナップショットには、広告ウィンドウが閉じられてメモリから削除されたかどうかに関係なく存在します。これは解析すべきもう一つのメモリリークのようです。AdWindow.ctor(Window owner)
コールをダブルクリックします。dotMemory は、この呼び出しによって作成されたDispatcherTimer
クラスのインスタンスを表示します。デフォルトでは、発信参照ビューが使用されます。次に、このインスタンスがどのようにメモリ内に保持されているかを調べる必要があります。キー保持パスビューを使ってみましょう。キー保持パスをクリックします。ご覧のとおり、2 つの主要な保存経路があります。
タイマーの最初の保持パスは、
DispatcherTimer
リストにつながります。これは、グローバルであり、すべてのタイマーをアプリケーションに保存します。2 番目の方法は、タイマーがDispatcherOperationCallback
オブジェクトによっても保持されることを示しています。このオブジェクトは、タイマーの実行時に作成されるデリゲートです。これは、タイマーがまだ実行中であることを意味します。DispatcherTimer
クラスの特色の 1 つは、タイマーが停止した後にのみインスタンスがグローバルタイマーリストから削除されることです。リークを修正するには、広告ウィンドウを閉じる前にタイマーを停止する必要があります。コードでこれをやりましょう !AdWindow
クラスの実装を含む AdWindow.cs ファイルを開きます。実際には、修正は非常に簡単になります。adTimer.Stop();
行をUnsubscribe()
メソッドに追加するだけです。修正後、メソッドは次のようになります。public void Unsubscribe() { _adTimer.Tick -= ChangeAds; _adTimer.Stop(); }解決策を再構築します。
ステップ 2. スナップショットを取得するを繰り返します。
型別にグループ化ビューで 2 番目のスナップショットを開き、
System.Windows.Threading.DispatcherTimer
タイプのすべてのオブジェクトを検索します。ご覧のとおり、7 ではなく 6 つの
DispatcherTimer
オブジェクトしかありません。ガベージコレクタが広告ウィンドウで使用されているタイマーを確実に収集するように、バックトレースビューを使用してこれらのタイマーを見てみましょう。DispatcherTimer オブジェクトをダブルクリックし、ビューのリストでバックトレースをクリックします。
すばらしいです ! リストには
AdWindow
コンストラクターがありません。これは、リークが正常に修正されたことを意味します。
もちろん、このタイプのリークは特にアプリケーションにとって重要ではないようです。dotMemory を使用しなかった場合、この問題に気付かなかったかもしれません。それにもかかわらず、他のアプリでは(たとえば、サーバーサイドのものが 24 時間 365 日働いている)、OutOfMemory
例外を引き起こしてこの漏れが現れることがあります。
関連ページ:
dotMemory を使ってみる
サンプルアプリケーション人生ゲーム、このチュートリアルでは、dotMemory を実行してメモリスナップショットを取得する方法を学習します。さらに、dotMemory のユーザーインターフェースと基本的なプロファイリングの概念について簡単に説明します。dotMemory の出発点として、このチュートリアルを検討してください。基本用語:質問するかもしれません: " メモリスナップショットとは何ですか、なぜ入手する必要がありますか? " これは、dotMemory を使用している間に出くわすいくつ...
メモリトラフィックを最適化する
サンプルアプリケーション人生ゲーム、このチュートリアルでは、dotMemory を使用してアプリケーションのメモリ使用を最適化する方法を説明します。「メモリ使用量を最適化する」とはどういう意味でしょうか? オペレーティングシステムのどのプロセスと同様、ガベージコレクタ(GC)はシステムリソースを消費します。ロジックは単純です: GC のコレクションが増えるほど、CPU オーバーヘッドが大きくなり、アプリケーションのパフォーマンスが低下します。通常、これは、アプリケーションが限られた期間に必要な多...