Visual Studioでメモリリークを追え! ~後編~

プログラミングコードを見つめるSEたち みなさま、こんにちは。おさかなと申します。前回はメモリリーク発生の検知、Visual Studioのパフォーマンスプロファイラーツールを使用してその原因を突き止める手法を紹介しました。Visual Studioではさまざまなデバッグツールが統合されており、前回紹介したスナップショットで原因を特定できない場合に、他のアプローチで原因を究明することも可能です。本稿ではその他の手法を使用してメモリリークの解析を行ってみたいと思います。

ダンプファイルとは

今回はダンプファイルを生成し、Visual Studioでダンプファイルの解析を行いたいと思います。 そもそもダンプファイルはご存知でしょうか? ダンプファイルはPCのメモリやレジスタなどのデータを保存して書き出したファイルのことです。たとえば不具合によりブルースクリーンになってその場で原因特定が難しいときでも、その時のダンプファイルを生成しておくと後から解析を行うことができます。なお、ダンプファイルにはすべてのメモリを書き出す「フルダンプ」と一部のデータのみを書き出す「ミニダンプ」がありますが、今回はフルダンプを対象に解析を行います。

ダンプファイルの作成

それではまずダンプファイルを生成する方法を紹介します。ダンプファイルの生成手法は複数あり、一番身近な手法としてはタスクマネージャーを使用する手法があります。タスクマネージャー上で表示されているプロセスを選択し、右クリックから「ダンプファイルの作成」を選択することで作成が可能です。

タスクマネージャーでダンプファイルを作成
タスクマネージャーでダンプファイルを作成

ダンプファイルを作成する方法はいくつかありますが、今回はメモリリークを特定したいためダンプファイルを作成開始するときにさまざまな設定が可能なProcDump を使用して作成したいと思います。

メモリリークを特定する場合、ダンプファイルの作成中にメモリ使用率が100%に達してPCがダウンしてしまうことを避けるため、60%程度に達したときにダンプファイルの作成を開始するようにします。今回は下記のコマンドを使用しました。

procdump.exe -e -ma -w hogehoge.exe -m 18000

オプションは次を意味しています。

  • -e:プロセスで未処理の例外が発生したときに、ダンプファイルに書き込む

  • -ma:Fullダンプファイルを書き込む

  • -w:指定されたプロセスが実行されていない場合、起動するまで待機する

  • -m:指定したメモリ閾値(MB単位)に達するとダンプを作成する

今回アプリを起動しているPCはメモリが32GBなので、18000[MB]=18[GB]、つまり全体の60%程度使用したときにダンプファイルを生成する指定になっています。ちなみにですが、使用しているPCのメモリは「設定」⇒「システム」⇒「詳細情報」から確認することができます。

メモリ確認
メモリ確認

閾値に達すると下図のようにコンソール上でファイル保存開始のメッセージが表示されます。

ダンプファイル作成開始時の表示
ダンプファイル作成開始時の表示

10分程度してファイルの生成が完了しました。今回作成したダンプファイルのサイズはおよそ19GBになりました。ダンプファイルは非常に巨大になるため、作成するときはディスク容量に余裕を持たせた状態で行うことが重要です。

ダンプファイル作成完了時の表示
ダンプファイル作成完了時の表示

ダンプファイル解析

それではいよいよダンプファイルの解析を進めていきます。 解析にはVisual Studioを使用します。Visual Studioはダンプファイルを視覚的に表示することができ、ファイルの解析でも優れた機能を有しているためさまざまな原因の特定に役立ちます。実際にVisual Studioでダンプファイルを開いたものが以下になります。

ダンプファイルを開いたときの画面表示
ダンプファイルを開いたときの画面表示

ダンプファイルを開くとさまざまな情報を確認することができます。メイン画面に表示される主な情報を下記に示します。

  • Dump Summary:生成されたダンプファイルの情報の一覧。プロセス名、例外情報なども表示される
  • System Information:プログラムを実行した環境のシステム情報。OSのバージョンおよびランタイム情報が表示される
  • Modules:実行したプログラムで使用されているdllなどのモジュール情報の一覧。モジュールバージョン、モジュールパスが表示される

ダンプファイルをVisual Studio上で開くと、画面右上に「Actions」という項目が存在します。

ダンプファイル解析のモード
ダンプファイル解析のモード
Actionsではダンプファイルからデバッグを行うことができる機能で、例外発生原因となっているスレッドやコードにジャンプすることができ、問題の分析が非常に簡単に行えます。それぞれの項目によって適したデバッグを行うことが可能です。

  • マネージドのみでデバッグ:.NET Frameworkを使用している場合に選択。(C#やVB.NETなど)
  • 混合でデバッグ:マネージドコードとネイティブコードが混在している場合に選択。両方のコードのデバッグが可能
  • ネイティブのみでデバッグ:.NET Frameworkを使用していない場合に選択。(C++など)
  • マネージドメモリのデバッグ:マネージドコードのメモリを調査するときに選択。マネージドヒープのスナップショットを取得し、その時点でのアプリのメモリ状態を確認可能

今回はメモリの使用状況を見たいので、Actionsの中の「マネージドメモリのデバッグ」を実行してみます。すると、マネージドメモリの詳細な結果を確認することができます。 ※マネージドメモリのデバッグの項目はVisual Studioのエンタープライズプランでしか利用できない機能のため、コミュニティやProfessionalプランを使用中の場合は別途「WinDbg」などの解析ツールを別途使用する必要があります。Visual Studioのプランによって利用できる機能については公式サイト を参照ください。

メモリの詳細をサイズでソートしてやると、前回と同様にTask関連で非常に多くのメモリを使用していることが分かります。また、その中にはThreadPoolWorkQueueの表示があり、スレッドプールの枯渇が原因でメモリリークしていることが確認できます。これは前回記事のスナップショットを取得したときに登場したものと同様で、スレッド生成数に問題があることが分かりました。

マネージドメモリの詳細
マネージドメモリの詳細

なお、今回はメモリリークを検出するためにオブジェクトの生成数やメモリ使用率を表示して確認しましたが、例外のトラブルシューティングをする際には発生した例外の内容とその該当箇所のコードを表示することもできます。このように、不具合発生時にはダンプ出力してVisual Studioで表示すると、視覚的に内部情報を確認することができるので、トラブルに遭遇した際にはひと手間かけてダンプファイルを出力することを検討されてはいかがでしょうか。

※ソースコードレベルで原因を特定するには、ダンプファイルのデバッグ時にVisual Studioでソースコードを含むプロジェクトをあらかじめ開いておく必要があります。

プロセスアタッチ

ここまで、あらかじめ再現性のあるバグに対してデバッグ実行することで原因の特定を進めてきました。しかし、システムの実行中に思わぬ現象が起こり、今すぐデバッグ実行して確認したいこともあるはずです。そこで、最後に実行中のプログラムをデバッグできる機能を紹介します。それがVisual Studioのデバッグメニューに存在する「プロセスにアタッチ」する機能です。

プロセスにアタッチする方法は以下の通りです。

  1. デバッグしたいプログラムを実行する
  2. 実行しているプログラムのプロジェクトをVisual Studioで開く
  3. Visual Studioでデバッグメニューからプロセスにアタッチを選択する
  4. 該当するプロセスを選択して「アタッチ」を押下する

プロセスアタッチ画面
プロセスアタッチ画面

④まで実行するとデバッグ起動時と同様の画面となり、起動中のプログラムに対してデバッグを行うことができます。これにより、コンソール画面や変数などを確認したりすることができ、効率的なデバッグを行うことができます。もちろん、こちらの機能を利用してメモリ使用量が増加していないか検証することも可能です。

まとめ

今回は前回の記事に引き続き、Visual Studioを使用してメモリリークを追跡する手法を紹介しました。前回に比べ、ダンプファイルの作成は時間を要するためハードルは高いですが、プロファイルの取得やブレークポイントなどでは発見できないような潜在的なバグを発見することができます。

Visual Studioにはメモリリークだけでなくさまざまなデバッグツールが備わっているので、この記事を見て実際のデバッグに役立てていただけると幸いです。

それではまた次回の記事でお会いしましょう!