コンピュータサイエンスにおいて、メモリリークとは、コンピュータプログラムがメモリ割り当てを誤って管理し、不要になったメモリが解放されない場合に発生するリソースリークの一種です[ 1 ] 。メモリリークは、オブジェクトがメモリに格納されているにもかかわらず、実行中のコードからアクセスできない場合(つまり、到達不能なメモリの場合)にも発生する可能性があります[ 2 ] 。メモリリークは他の多くの問題と似た症状を示し、一般的にはプログラムのソースコードにアクセスできるプログラマーのみが診断できます。
関連する概念として「スペースリーク」があり、これはプログラムが過剰なメモリを消費するものの、最終的にはそれを解放する現象です。[ 3 ]
メモリリークは、アプリケーションの実行中に利用可能なシステムメモリを使い果たしてしまう可能性があるため、ソフトウェアの劣化の原因または要因となることがよくあります。
プログラムにメモリリークがあり、メモリ使用量が着実に増加している場合でも、通常はすぐに症状は現れません。最新のオペレーティングシステムでは、アプリケーションが通常使用するメモリは、アプリケーションが終了すると解放されます。つまり、短時間しか実行されないプログラムのメモリリークは気づかれず、深刻な問題になることはまれであり、ゆっくりとしたリークはプログラムの再起動によって隠蔽されることもあります。すべての物理システムには有限のメモリ量があり、メモリリークが(例えば、リークしているプログラムを再起動することによって)封じ込められない場合、最終的にはユーザーに問題を引き起こします。[ 4 ]
現代のほとんどの家庭用デスクトップオペレーティングシステムは、RAMマイクロチップに物理的に格納されているメインメモリと、ハードドライブなどの二次記憶装置の両方を備えています。メモリ割り当ては動的で、各プロセスは要求しただけのメモリを取得します。アクティブなページは高速アクセスのためにメインメモリに転送され、非アクティブなページは必要に応じて二次記憶装置に押し出されて空き領域が確保されます。単一のプロセスが大量のメモリを消費し始めると、通常はメインメモリをますます占有し、他のプログラムを二次記憶装置に押し出し、システムのパフォーマンスを著しく低下させます。メモリをリークしているプログラムが終了しても、他のプログラムがメインメモリにスワップバックしてパフォーマンスが正常に戻るまでには時間がかかる場合があります。結果として生じる速度低下と二次記憶装置への過剰なアクセスは、スラッシングとして知られています。
プログラムが終了する前に利用可能なメモリをすべて使い切ってしまった場合(仮想メモリがある場合でも、組み込みシステムのようにメインメモリしかない場合でも)、それ以上メモリを割り当てようとしても失敗します。通常、これによりメモリを割り当てようとしたプログラムが自己終了するか、セグメンテーション違反が発生します。一部のプログラムは、このような状況から回復するように設計されています(おそらく、事前に予約されたメモリにフォールバックするなど)。メモリ不足を最初に経験するプログラムは、メモリリークが発生しているプログラムである場合もあれば、そうでない場合もあります。
マルチタスクオペレーティングシステムの中には、メモリ不足に対処するための特別なメカニズムを備えているものがあります。例えば、プロセスをランダムに強制終了する(これにより「無関係な」プロセスにも影響が出る可能性があります)、あるいはメモリ内で最も大きなプロセス(おそらく問題の原因となっているプロセス)を強制終了する、といったものです。また、オペレーティングシステムによっては、プロセスごとにメモリ制限を設けて、1つのプログラムがシステム全体のメモリを占有してしまうのを防ぐものもあります。この仕組みの欠点は、グラフィック、ビデオ、科学計算など、大量のメモリを必要とするプログラムが適切に動作するように、オペレーティングシステムを再構成する必要が生じる場合があることです。
メモリリークがカーネル内で発生している場合、オペレーティングシステム自体が動作不能になる可能性が高い。組み込みシステムなど、高度なメモリ管理機能を持たないコンピュータは、持続的なメモリリークによってシステム全体が動作不能になる場合もある。
さらに深刻な情報漏洩としては、以下のようなケースが挙げられます。
メモリリークはプログラミングでよくあるエラーで、特にCやC++のように自動ガベージコレクションが組み込まれていない言語を使用している場合に多く発生します。通常、メモリリークは動的に割り当てられたメモリが到達不能になったために発生します。メモリリークバグの蔓延により、到達不能なメモリを検出するためのデバッグツールが数多く開発されてきました。BoundsChecker 、Deleaker、Memory Validator、IBM Rational Purify、Valgrind、Parasoft Insure++、 Dr . Memory、memwatchなどは、CおよびC++プログラムでよく使われるメモリデバッガです。組み込み機能としてガベージコレクション機能を持たないプログラミング言語には、「保守的な」ガベージコレクション機能を追加することができ、CおよびC++プログラム向けには、これを行うためのライブラリが用意されています。保守的なコレクタは、到達不能なメモリのほとんどを検出して解放しますが、すべてではありません。
メモリマネージャは到達不能なメモリを回復できますが、到達可能で潜在的に有用なメモリを解放することはできません。そのため、最新のメモリマネージャは、プログラマがメモリをさまざまなレベルの有用性で意味的にマークするための手法を提供しており、これはさまざまなレベルの到達可能性に対応しています。メモリマネージャは、強く到達可能なオブジェクトを解放しません。オブジェクトは、強い参照によって直接到達可能か、強い参照の連鎖によって間接的に到達可能かのいずれかの場合に、強く到達可能です。(強い参照とは、弱い参照とは異なり、オブジェクトがガベージコレクションされるのを防ぐ参照です。)これを防ぐために、開発者は使用後に参照をクリーンアップする責任があります。通常は、不要になった参照をnullに設定し、必要に応じて、オブジェクトへの強い参照を保持しているイベントリスナーを登録解除します。
一般的に、自動メモリ管理は開発者にとってより堅牢で便利です。なぜなら、解放ルーチンを実装したり、クリーンアップの順序を気にしたり、オブジェクトがまだ参照されているかどうかを気にしたりする必要がないからです。プログラマーにとって、オブジェクトが参照されなくなったタイミングを知るよりも、参照が不要になったタイミングを知る方が簡単です。ただし、自動メモリ管理はパフォーマンスのオーバーヘッドをもたらす可能性があり、メモリリークの原因となるプログラミングエラーをすべて解消するわけではありません。
ウェブサーバーやルーターなど、一般にアクセス可能なシステムは、攻撃者が情報漏洩を引き起こす一連の操作を発見した場合、サービス拒否攻撃を受けやすい。このような一連の操作はエクスプロイトと呼ばれる。
リソース取得は初期化である(RAII) は、C++、D、Adaで一般的に採用されている問題解決手法です。これは、スコープ付きオブジェクトを取得したリソースに関連付け、オブジェクトがスコープ外になったときにリソースを自動的に解放するというものです。ガベージコレクションとは異なり、RAII はオブジェクトが存在するときと存在しないときを把握できるという利点があります。以下の C と C++ の例を比較してください。
C言語では:
#include <stdlib.h>void someOperation ( int * a ) { // ... }void f ( int n ) { int * a = ( int * ) calloc ( n , sizeof ( int )); someOperation ( a ); free ( a ); }C++では:
import std ;std :: vectorを使用します。void someOperation ( vector < int >&a a ) { // ... }void f ( int n ) { vector < int > a ( n ); someOperation ( a ); }例に示されているC言語版では、明示的な解放が必要です。配列は動的に割り当てられ(ほとんどのC言語実装ではヒープから)、明示的に解放されるまで存在し続けます。(ただし、とは異なりmalloc()、calloc()はすべての要素をで初期化します0。)
C++ 版では明示的な解放は不要です。a例外がスローされた場合も含め、オブジェクトがスコープから外れるとすぐに自動的に解放されます。これにより、ガベージ コレクションスキームのオーバーヘッドの一部が回避されます。また、オブジェクト デストラクタはメモリ以外のリソースも解放できるため、RAII は、マーク アンド スイープ ガベージ コレクションでは適切に処理されない、ハンドルを介してアクセスされる入力リソースと出力リソースのリークを防ぐのに役立ちます。これには、開いているファイル、開いているウィンドウ、ユーザー通知、グラフィックス描画ライブラリ内のオブジェクト、クリティカル セクションなどのスレッド同期プリミティブ、ネットワーク接続、Windows レジストリや他のデータベースへの接続などが含まれます。
しかし、RAIIを正しく使用することは必ずしも容易ではなく、落とし穴も存在します。例えば、注意を怠ると、参照渡しでデータを返すことでダングリングポインタ(または参照)を作成してしまい、そのデータを含むオブジェクトがスコープから外れたときにデータが削除されてしまう可能性があります。
DはRAIIとガベージコレクションを組み合わせて使用し、オブジェクトが元のスコープ外からアクセスできないことが明らかな場合は自動的に破棄し、そうでない場合はガベージコレクションを実行します。
より現代的なガベージコレクション方式は、多くの場合、到達可能性の概念に基づいています 。つまり、対象のメモリへの使用可能な参照がない場合、そのメモリはガベージコレクションされます。その他のガベージコレクション方式は、参照カウントに基づいています。この場合、オブジェクトは自身を指す参照の数を追跡する責任を負います。参照数がゼロになると、オブジェクトは自身を解放し、メモリの再利用を許可します。このモデルの欠点は、循環参照に対応できないことです。そのため、今日ではほとんどのプログラマーが、よりコストのかかるマークアンドスイープ方式のシステムを受け入れることを厭いません。
以下のVisual Basicコードは、典型的な参照カウント型メモリリークを示しています。
Dim A , B Set A = CreateObject ( "Some.Thing" ) Set B = CreateObject ( "Some.Thing" ) ' この時点で、2 つのオブジェクトはそれぞれ 1 つの参照を持ちます。Set A . member = B Set B . member = A ' これでそれぞれ 2 つの参照を持つことになります。セットA = Nothing ' まだ抜け出せる可能性はある...Set B = Nothing ' これでメモリリークが発生しました!終わり実際には、このような些細な例はすぐに発見され、修正されるだろう。しかし、実際のほとんどの例では、参照の循環は2つ以上のオブジェクトにまたがり、検出がより困難になる。
この種のメモリリークの有名な例として、WebブラウザにおけるAJAXプログラミング技術の台頭に伴い、「リスナーの不在問題」が挙げられます。DOM要素をイベントハンドラに関連付けたJavaScriptコードが、終了前に参照を削除しなかった場合、メモリリークが発生します(AJAX Webページは、従来のWebページよりも特定のDOMをはるかに長く保持するため、このリークはより顕著になります)。

メモリ使用量が「のこぎり歯状」に変動する場合、特にアプリケーションの再起動や再開時に垂直方向の減少が見られる場合は、アプリケーション内でメモリリークが発生している可能性を示唆している。ただし、ガベージコレクションポイントでも同様のパターンが発生する可能性があり、その場合はヒープが適切に使用されていることを示しているため、注意が必要である。
メモリ使用量が絶えず増加しているからといって、必ずしもメモリリークが発生しているとは限りません。アプリケーションによっては、メモリに(キャッシュなどとして)ますます多くの情報を格納する場合があります。キャッシュが大きくなりすぎて問題が発生する場合は、プログラミングまたは設計上のエラーである可能性がありますが、情報は名目上は使用されているため、メモリリークではありません。また、プログラマが特定のタスクに対して常にメモリが十分であると想定しているため、プログラムが不当に多くのメモリを必要とする場合もあります。たとえば、グラフィックファイルプロセッサは、画像ファイルの内容全体を読み込み、それをすべてメモリに格納することから始める場合がありますが、非常に大きな画像が使用可能なメモリを超える場合は、これは現実的ではありません。過剰なメモリ使用がメモリリークによるものであることを確認するには、プログラムコードにアクセスする必要があります。
以下の擬似コードで書かれた例は、プログラミングの知識がなくても、メモリリークがどのように発生し、どのような影響があるかを示すことを目的としています。このプログラムは、エレベーターを制御するために設計された非常にシンプルなソフトウェアの一部です。このプログラムのこの部分は、エレベーター内の誰かが階のボタンを押すたびに実行されます。
ボタンが押されたとき: 階数を記憶するために使用するメモリを用意してください。 階数をメモリに入力してください 私たちは既に目標の階に到達しているのでしょうか? そうだとすれば、我々にやることは何もない。 さもないと: エレベーターが停止するまで待ってください 目的の階へ移動してください 階数を覚えるために使っていた記憶を解き放つ
要求された階数がエレベーターの現在位置する階数と同じ場合、メモリ解放の条件が満たされないため、メモリリークが発生します。このケースが発生するたびに、リークされるメモリ量が増加します。
このようなケースは通常、すぐに影響が出ることはありません。人々は自分がいる階のボタンを頻繁に押すわけではありませんし、そもそもエレベーターには十分なメモリ容量があるため、このような操作が数百回、あるいは数千回繰り返されても問題ないかもしれません。しかし、いずれはエレベーターのメモリ容量が不足します。これには数ヶ月、あるいは数年かかる場合もあるため、徹底的なテストを行っても発見されない可能性があります。
その結果は好ましくないものとなるでしょう。少なくとも、エレベーターは別の階への移動要求(例えば、エレベーターを呼ぼうとしたり、中にいる人が階のボタンを押したりした場合など)に応答しなくなります。プログラムの他の部分(例えば、ドアの開閉を担当する部分)にもメモリが必要な場合、誰もエレベーターに入ることができなくなり、もし中に人がいたとしても、閉じ込められてしまうでしょう(手動でドアを開けることができない場合)。
メモリリークはシステムがリセットされるまで続きます。例えば、エレベーターの電源が切れたり停電したりすると、プログラムは停止します。電源が再びオンになると、プログラムは再起動し、すべてのメモリが再び利用可能になりますが、メモリリークというゆっくりとしたプロセスはプログラムとともに再開され、最終的にはシステムの正常な動作を阻害します。
上記の例におけるメモリリークは、「release」操作を条件式の外に出すことで修正できます。
ボタンが押されたとき: 階数を記憶するために使用するメモリを用意してください。 階数をメモリに入力してください 私たちは既に目標の階に到達しているのでしょうか? そうでない場合: エレベーターが停止するまで待ってください 目的の階へ移動してください 階数を覚えるために使っていた記憶を解き放つ
以下のC++関数は、割り当てられたメモリへのポインタを失うことで、意図的にメモリリークを引き起こします。
void causeLeak () { int * a = new int [ 5 ]; a = nullptr ; /** * 'a' 内のポインタはもはや存在しないため、解放できません が、 * メモリはシステムによって割り当てられたままです。 * プログラムが解放せずにこのようなポインタを作成し続けると、 * メモリを継続的に消費します。 * したがって、メモリリークが発生します。 * 対応する delete は new 呼び出しと一致する必要があります。 */ }