メモリリーク
メモリリーク (memory leak) とは、プログラミングにおけるバグの一種。プログラムが確保したメモリの一部、または全部を解放するのを忘れ、確保したままになってしまうことを言う。プログラマによる単純なミスやプログラムの論理的欠陥によって発生することが多い。
2019年2月のマルチプロセスOSではメモリ空間がプロセス(プログラム)ごとに独立に確保され、プロセス終了とともに解放される。したがって、メモリリークが単独で発生する場合、あるいはそのプロセスがすぐに終了される場合は、深刻な影響をもたらすことはあまりないと言ってよい。しかし、メモリリークが繰り返し起きて大量にメモリが消費された場合、他のプログラムやOSが確保可能なメモリが少なくなり、以下の2つの現象が発生する。
OSやプログラムがメモリを確保する際にRAMからのみ確保する場合、エラーを引き起こす、または即座に停止する。
OSやプログラムが直接RAMの領域を確保するのではなく、仮想メモリを使用している場合(2019年2月のOSはこのタイプ)、プログラムのメモリ使用量(ワーキングセット)が一定値に達するとページングが多く発生するようになる。最終的に、仮想メモリを使い果たすと、メモリ確保のAPI(典型的な例ではC言語のmalloc関数)からか、メモリ確保失敗を表す異常系としてnullを返したり例外をスローしたりする。なお、メモリ確保の失敗を想定した設計になっていないプログラムはnullアクセスによるセグメンテーション違反や、ハンドルされない例外の発生などにより、通例プロセスの異常終了を引き起こす。
メモリリークが以下のような状況で起こった場合、問題は特に深刻になる。
プログラムが長期間動き続けるとき。サーバーサイドアプリケーションや組み込みシステムは年単位で稼働し続けることもある。
共有メモリのような、確保したまま終了することが許されるメモリ領域をプログラムが使っているとき。
ゲームや動画を扱うプログラムのように、メモリの確保・解放を頻繁に行うとき。
OSやシステムそのものがメモリリークを起こすとき。
組み込みシステムやポータブル機のように、メモリの絶対量が少ないとき。
AmigaOSのように、プロセスが終了してもメモリが自動的に解放されないOSを利用しているとき。メモリリークから回復する手段はシステムの再起動しかない。
メモリリークを診断するためには、一般的にはプログラムの論理構造を調べる、デバッガを使って内部状態を確認するなどの手段をとる必要がある。時系列に沿ってメモリ消費量をトレースすることによりヒントが得られることもある。キャッシュを使うプログラムの場合、設定を間違えていれば消費メモリのサイズはシステムを停止させるまで際限なく増え続けてゆくかもしれない。メモリリークが発生していることが分かっている場合、最も単純な原因調査の方法は、しばらくシステムを放置した後にメモリ上の適当なオブジェクトを拾うことである。同じ種類のオブジェクトが大量に見つかれば、おそらくその発生源がリーク元である。メモリのダンプを取得して解析することも診断手段である。Windows用にはマイクロソフトから無料のツールが用意されている。
しばしば、メモリを大量に消費する症状のみを見て、プログラムがメモリリークを起こしていると誤診断されることがある。しかし、メモリの消費量のみからメモリリークか否かを判断するのは難しい。なぜならば、仮にメモリが(不必要に思われるほど)大量に消費されていたとしても、そのプログラムが本当にそれだけのメモリを必要としていたり、将来必要になるという理由で確保したりしている可能性があるからである。単にメモリを無駄遣いしているだけということもありうる(プログラムのバグではあってもメモリリークではない)。
また、メモリを解放しているはずなのにプログラムの使用メモリが減らない現象を見て、メモリリークが疑われる場合がある。実は、ほとんどのアプリケーションソフトウェアプログラムはメモリの確保・解放処理を、OS固有の下位レベルAPIを直接利用して行なっているわけではなく、その上位層として構築されたライブラリ(C言語のmalloc関数など)や仮想マシン(Java仮想マシンや.NETのCLRなど)を経由して行なっている。アプリケーションがいったん確保・解放したメモリは、のちの再確保処理を高速化するためにライブラリや仮想マシンが蓄えておく(プールしておく)ため、OSから見えるメモリの使用量が減らないように見えているのである。
Windows上では、Intel Parallel InspectorやMicro Focus BoundsCheckerといったサードパーティー製のメモリエラー検出ツールに、メモリリーク検出機能が付属しているので、これらを利用してメモリリーク位置を特定することが可能である。ただし誤検出することもあり、完全ではないので、前記の診断方法と併用することが望ましい。メモリデバッガも参照。
プログラミング言語によってはメモリ解放をコンピューター(仮想マシン、フレームワークなどのシステム側)に委ねるガベージコレクションが取り入れられており、メモリリークが起きにくくなっている。ガベージコレクションを言語組み込みの機能として持つ言語には、LispやJava、C#/VB.NETなどの.NET言語全般、Objective-C(バージョン2.0以降)、PHP、Pythonなどがある。
一方でC言語のようなガベージコレクションが組み込まれていないプログラミング言語では、メモリリークが起きないように注意深くプログラムを設計・実装する必要がある。C++においてはガベージコレクションこそないものの、デストラクタ機能を活用したRAIIにより、所有権や参照カウントなどの仕組みを実現し、メモリ寿命管理を自動化・簡素化することができる。これは後述のリソースリークにも適用できる。ただし、参照カウント方式は循環参照によるメモリリークを回避することができないという欠点も持っており、必要に応じて弱い参照を併用する必要がある。Pythonは循環参照問題に関して、参照カウントのほかに世代別ガベージコレクションを補助的に利用することで対処している。
ガベージコレクションはどこからも参照されていないメモリ領域を自動的に解放する。しかし、プログラマの意図・把握しないところに参照(強い参照)が残っていると、今後全く利用されないはずのメモリ(オブジェクト)がガベージコレクションによる解放の対象にならず、確保されたままになることがある。これもまたメモリリークの一種であり、最終的にはメモリ不足によりシステムをクラッシュさせる等、上記のような問題と全く同種の問題を引き起こす。この種類のメモリリークに関してガベージコレクションは無力である。この問題に関しては、一部の参照を弱い参照で置き換えて従属関係を与えることにより回避できるケースもある。
リソースリーク (resource leak) とは、メモリリークをやネットワーク接続、OS、ハードウェアなど一般のリソースに拡張した概念である。
例えば、一般的なOS環境でファイルへの読み書きをする場合、最初にファイルを開く処理が必要であり、これはメモリでいえば確保する処理にあたる。ファイルに対する処理が終了した時には、ファイルを閉じ、該当ファイルの使用権をOSに返却する処理が必要であり、これはメモリを解放する処理にあたる。通例、ファイルを閉じることで自動的にストリームがフラッシュされ、変更内容も反映(コミット)される。このとき、ファイルを閉じる処理を忘れて、不要なファイルがいつまでも開いた状態にあれば、リソースリークが発生したことになる。ユーザーが他のプログラムでそのファイルを開こうとしても失敗したり、ファイルを削除しようとしても失敗したり、などの原因不明のエラーに悩まされることになる。ほとんどの場合、リソースリークを起こしたプログラムが終了すれば、OSによって正常な状態に復帰されるが、ファイルを閉じるための正常な処理を経ていない場合、ファイルへの変更内容が反映されないこともある。
Windowsでは標準のタスク マネージャーやProcess Explorerといったツールを使って、各プロセスが使用しているハンドルの数を監視することで、リソースリークを診断することが可能である。
C++ではデストラクタによるRAIIを活用することでリソースリークを防止できる。JavaやC#のファイナライザはフェイルセーフとしての役目しか期待できないが、Javaのtry-with-resources文や、C#のusing文といった、C++のデストラクタによるRAIIに類似した機能を活用することで、リソースリークを防止できる。