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

プログラミングコードを見つめるSE みなさま、こんにちは。おさかなと申します。今回は私が以前、Visual Studioでの開発でメモリリークに直面した際の解決方法についてお伝えしたいと思います。実は私は入社するまで開発であまりVisual Studioを使用してこなかったのですが、バグを特定するにあたり非常に強力なデバッグツールがたくさん使えることを知りました。本記事でトラブルシューティングに苦しんでいる新米エンジニアの方々の一助になれば幸いです。

なお、本記事は前編・後編の二部構成とし、今回はメモリリークの現象確認からVisual Studioを使用したメモリリークのデバッグの導入部分までを紹介します。

メモリリークとは

そもそもになりますが、「メモリリーク」をご存じでしょうか? メモリリークとはプログラムのバグのことで、不要になったメモリを開放することなく確保し続けることを言います。英語では「leak」と書きメモリが減っている、つまり漏れ出しているという意味になります。メモリリークしている状態でプログラムを起動させ続けると、必要なメモリ量が増え最終的にはPC内の他の機能やソフトを圧迫し、機能の停止に陥ります。このように、メモリリークはプログラムを動作させるうえで非常にクリティカルなバグとなります。

メモリリークはなぜ起こるのか

それではなぜメモリリークが発生するのか解説します。発生の原因は非常にシンプルで、前述したように「確保したメモリの使用が終わっても解放しない」からです。プログラムでは変数や画像や音楽データなどのさまざまな情報を保持するためにコンピューターが用意した専用の保存領域である「メモリ」を使用します。メモリにはコンパイル時に事前に固定長で確保される「静的メモリ」と、ユーザの入力値などに応じて実行時に動的に確保される「動的メモリ」があります。

静的メモリ割り当ては一般的な変数宣言などをすると確保されます。(C++の場合の例)

int memory1;            //←4バイトのメモリを確保  
double memory2;        //←8バイトのメモリを確保  

一方で、動的メモリ確保は下記のように行われます。

int* memory3 = new int[5];  //←int型の要素を5つ連続して確保  
int *p = (int*)malloc(1024);  //←1024バイトのメモリ確保

動的にメモリを確保する場合はnew演算子、またはmalloc関数などを使用してメモリ領域を割り当てます。静的メモリはユーザーが確保や解放を意識する必要はありませんが、動的メモリはユーザが手動で解放しなければならないため注意が必要になります。解放は下記のように行います(C++の場合の例)。

int* memory3 = new int[5];  //←int型の要素を5つ連続して確保  
delete[] memory3;     //←memory3のメモリ解放  
int *p = (int*)malloc(1024);   //←1024バイトのメモリ確保  
free(p)          //←pのメモリ解放  

上記のようにnewでメモリを確保したときはdeleteで解放、mallocでメモリを確保したときはfreeで解放することができます。このように、動的に確保したメモリを解放せずに処理を続けているとPCのメモリが減り、最悪の場合はPCがフリーズして強制シャットダウンせざるを得ない状況に陥ることもあります。

メモリリークの発生検知

それでは私が開発中にメモリリークが発生した際に追跡した方法を例に、メモリリークの確認を行ってみましょう。まず、一番簡単な確認方法はタスクマネージャーを見る方法です。タスクマネージャーはPCの処理を管理するプログラムで、PC上で実行されているタスクを確認したり、そのタスクを強制終了したりすることが可能です。タスクマネージャー上で詳細表示を行うとタスクごとの「CPU使用率」「メモリ使用率」「ディスク使用率」「ネットワーク帯域」などを確認できます。プログラム内でメモリリークの発生が疑わしい場合には主にメモリ使用率を確認します。

確認の手順としては、最初にプログラム起動直後のメモリ使用率を確認し、そのあとにプログラムで任意の処理を走らせてから再度メモリ使用率を確認します。そうすることでプログラムの処理によってどの程度のメモリ使用率が上昇したかを知ることができます。

それでは実際に確認していきます。まず、プログラム起動直後のメモリ使用率は40%程度になっていることが確認できました。

タスクマネージャー

その後、アプリで任意の処理を実行した状態でタスクマネージャーを開くと、該当するタスクのメモリ使用量が全体の96%と不自然に高くなっていてPCが機能停止に陥る寸前であることが確認されました。これによりアプリでメモリリークしている可能性が非常に高いことが分かりました。

処理後のタスクマネージャー

次に、よりメモリ使用状況を詳細に確認する手法として、リソースモニターを使用します。リソースモニターはWindowsには標準で入っているプログラムで、PCのCPUやメモリ・プロセス情報を確認することができます。タスクマネージャーと比べて細かな情報まで確認することができるため、メモリリークの疑いがある場合には原因特定の役に立ちます。実際にプログラム実行後にリソースモニターで確認したものが下図になります。

リソースモニター

リソースモニターではメモリタブでプロセスを選択すると、「ハードフォールト/秒」「コミット」「ワーキングセット」「共有可能」「プライベート」の項目を確認することができます。 ハードフォールトとは仮想メモリでのページフォールトの秒単位での回数を意味しており、物理メモリが不足していると大きくなります。また、コミットは使用されている仮想メモリ、ワーキングセットは物理メモリの量を表しています。今回の場合ではプログラムによって20GB程度もメモリが使用されていることが確認でき、メモリリークの疑いがかなり強くなりました。

Visual Studioでメモリリーク解析~スナップショット~

それではメモリリークが発生していそうなことが確認できたので、実際にコードのどこでメモリを使用しているかの特定を行っていきます。今回メモリリークが発生したアプリはVisual Studioを使用して開発したアプリなのでVisual Studioに入っているツールを使用してデバッグしていきたいと思います。

今回はVisual Studioのデバッグメニュー内の「パフォーマンスプロファイラツール」を使用します。パフォーマンスプロファイラツールではプログラム起動中のCPU使用率、GPU使用率、メモリ使用率など目的に応じて調査することができるため、処理の重い箇所を特定するなどさまざまなバグの原因を確認することが可能です。今回はメモリリークの確認を行いたいため分析ターゲットに「メモリ使用量」を設定してデバッグを開始します。

パフォーマンスプロファイラツール

パフォーマンスプロファイラツールでのメモリ使用量の確認中には「スナップショット」を取得することが可能です。スナップショットではプログラム実行中のその瞬間に存在するオブジェクトの種類・数・サイズなどを保存することができます。アプリ起動直後に実際に取得したスナップショットが以下の図になります。

スナップショットの取得

数秒~数十秒程度で取得することができ、デバッグ中に複数回取得することが可能です。それではメモリ使用量が75%程度に達したときに再度スナップショットを作成してみます。

スナップショットの取得(2回目)

取得したスナップショット同士は比較することが可能で、インスタンスの数やメモリサイズの増減を確認することができます。実際に比較結果を行ったのが以下の図になります。

スナップショットの比較

比較結果を「サイズの相違」で大きい順番に並び替えると「MemoryStream」や「Task」に関するメモリが増えていることが分かります。気になる項目をクリックすると画面真ん中にそのオブジェクトのさらに詳細な情報を確認することができます。上図は「Task」をクリックしたときのものですが、ThreadPoolWorkQueueと表示されておりスレッドプール関連で大量にメモリを消費していることが考えられます。

今回動作させているアプリではC#を使用して大量のTaskと呼ばれるスレッドを生成して非同期処理を行っているのですが、一般的にスレッドは必要となるたびに生成しているとパフォーマンスが低下してしまいます。そこで、C#ではスレッドを再利用して使いまわすスレッドプールという仕組みが用いられています。これによりスレッドの数を減らして効率的に処理を行うことができるのですが、今回はそのスレッド生成数が上限に達しリソースが枯渇しているのではないかということが分かりました。

実際に確認すると生成されるスレッド数が10万程度も生成されていることが分かりました。この数を10〜100程度に減らすことでメモリリークを回避することができました。

ちなみにですが、Visual Studioでデバッグ実行を行うときには「pdb」ファイルをというファイルを使用します。pdbファイルとはプログラムデータベースと呼ばれるもので、中にはソースコードのファイル名や行番号、シンボル情報を保持しており、デバッグのために使用されています。ビルド時に自動で生成されるファイルなのですが、設定で生成の有無を変更できるため、Releaseビルドなどでは不要なファイルとして生成されない場合も多くあります。デバッグを行うときにはpdbファイルが欠けていたり誤ったファイルが配置されていると正しくデバッグができないため注意が必要です。

まとめ

今回の記事ではメモリリークとは何なのか、どうすれば原因の特定ができるかについて述べ、Visual Studioでデバッグする方法を紹介しました。次回記事ではVisual Studioを使用して別手法でデバッグし、原因を特定する方法まで紹介したいと思います。 では次回の記事でお会いしましょう!