メモリリーク
メモリリーク (英: memory leak) とは、プログラミングにおけるバグの一種。プログラムが確保したメモリの一部、または全部を解放するのを忘れ、確保したままになってしまうことを言う。プログラマによる単純なミスやプログラムの論理的欠陥によって発生することが多い。
典型的な例
[編集]典型的なメモリリークは、動的メモリ確保をした後、解放を忘れることによって発生する。
動的メモリ確保の典型的なAPIのひとつとして、C言語の標準ライブラリ(標準Cライブラリ)にあるmalloc関数が挙げられるが、このmalloc
関数で動的に確保したメモリ領域は、必要がなくなったとき、free
関数で明示的に解放しなければならない。
以下のようなコードは、func
関数の内部で動的確保したメモリ領域が解放されておらず、メモリリークを引き起こす。
#include <stdlib.h>
#include <stdio.h>
static void func(size_t count) {
int* array = (int*)malloc(sizeof(int) * count);
}
int main(void) {
func(100);
return 0;
}
malloc
は、引数で指定したサイズのメモリの確保に成功した場合、割り当てられた領域のアドレスを戻り値として返す。上記の例でアドレス値を受けるポインタ型の変数array
は自動記憶域期間を持つ自動変数(ローカル変数)であり、コールスタック領域に確保されるため、関数を抜けると自動的に解放される。一方、malloc
で確保したメモリ領域のほうは動的記憶域期間を持ち、自動的に解放されず、使い終わった後はメモリ領域を指すポインタをfree
関数に渡して解放する必要があるが、上記の例ではfunc
関数を抜けるとポインタが失なわれてしまうため、malloc
で確保したメモリ領域を解放する機会を永遠に失なってしまう。
仮にsizeof(int)
が4
の環境だとすると、上記の例では合計400バイトのリークをしていることになるが、実際にはヒープ領域の連結リストによる管理などのために必要な付随データも併せてメモリ確保されるため[1]、400バイト+アルファのメモリ領域がデッドスペースと化してしまう。
上記の例は極めて単純なため、動的メモリ確保の基本的な仕組みを理解してさえいればメモリリークの発生箇所を容易に発見できるが、実際には、確保をするコードと解放をするコードがソースコード上で離れた位置にある、複数の動的メモリ確保をする、return文などによる関数の脱出位置が複数ある、などの複合的な要因によってメモリリークが起きるため、一見では問題に気づきにくいことのほうが多い。
動的メモリ確保は、プログラムの実行時にならないとデータ型や配列要素数が決まらないような場面で柔軟にメモリを確保する際に必要なテクニックだが、C言語のように明示的な解放処理の記述が必要なプログラミング言語では、解放を忘れると容易にメモリリークを引き起こすというリスクもある。
同様に、C++においても、new
演算子やnew[]
演算子を使って動的に確保したオブジェクトや配列は、delete
演算子やdelete[]
演算子を使った解放を忘れるとメモリリークする。ただし、C++の場合は、後述するようにデストラクタを使うことで解放を自動化する仕組みも備わっている。
影響
[編集]近年[いつ?]のマルチプロセス(マルチタスク)オペレーティングシステム (OS) ではメモリ空間がプロセス(プログラム)ごとに独立に確保され、プロセス終了とともに解放される。したがって、メモリリークが小規模かつ単独で発生する場合、あるいはそのプロセスがすぐに終了される場合は、深刻な影響をもたらすことはあまりないと言ってよい。しかし、メモリリークが繰り返し起きて大量にメモリが消費された場合、他のプログラムやOSが確保可能なメモリが少なくなり、以下の2つの現象が発生する。
- OSやプログラムがメモリを確保する際にRAMからのみ確保する場合、エラーを引き起こす、または即座に停止する[要説明]。C言語のmalloc関数はメモリ確保に失敗した場合NULLポインタを返すことが規定されているものの[2][3]、設定によっては呼び出し元に制御を返さずスピンし続ける環境もある(ハングアップ、フリーズ)[4]。C++の
new
演算子の場合はメモリの確保に失敗したときstd::bad_alloc
例外をスローする[5][注釈 1]が、std::nothrow
を指定すると例外をスローせずNULLポインタを返す[7]。Javaの場合はOutOfMemoryError
をスローする。.NETの場合はSystem.OutOfMemoryException
をスローする[8]。 - OSやプログラムが直接RAMの領域を確保するのではなく、仮想メモリを使用している場合(最近[いつ?]のOSはこのタイプ)、プログラムのメモリ使用量(ワーキングセット)が一定値に達するとページングが多く発生するようになる。最終的に、仮想メモリを使い果たすと、メモリ確保のAPI(典型的な例ではC言語のmalloc関数)から制御が戻らなくなって停止する[要出典]か、メモリ確保失敗を表す異常系としてnullを返したり例外をスローしたりする。
いずれにせよ、メモリ確保の失敗を想定した設計になっていないプログラムはnullアクセス[注釈 2]や、ハンドルされない例外の発生などにより、通例プロセスの異常終了を引き起こす。
メモリリークが以下のような状況で起こった場合、問題は特に深刻になる。
- プログラムが長期間動き続けるとき。サーバーサイドアプリケーションや組み込みシステムは年単位で稼働し続けることもある。
- 共有メモリのような、確保したまま終了することが許されるメモリ領域をプログラムが使っているとき。
- ゲームや動画を扱うプログラムのように、メモリの確保・解放を頻繁に行うとき。
- OSやシステムそのものがメモリリークを起こすとき。
- 組み込みシステムやポータブル機のように、メモリの絶対量が少ないとき。
- AmigaOSのように、プロセスが終了してもメモリが自動的に解放されないOSを利用しているとき。メモリリークから回復する手段はシステムの再起動しかない。
なお、Microsoft WindowsやmacOS、Linuxのようなデスクトップオペレーティングシステムでは、物理メモリが不足したときにHDDやSSDのようなストレージデバイスをスワップ領域に使用するが、AndroidやiOSといったモバイルオペレーティングシステムでは、ストレージデバイスをスワップ領域に使用せず、物理メモリが不足したときはメモリを浪費しているプロセスやバックグラウンド状態になっているプロセスなどを積極的に強制終了していく仕組みになっている[9][10][注釈 3]。これは頻繁な書き込みによるストレージの寿命低下を防ぎ、かつシステム全体の安定を保つためである。つまり、モバイルOS環境にてメモリが不足している状態でmallocを呼び出すと、制御が戻る前にそのままプロセスが強制終了されてしまうこともありえる。
WOW64のように64ビットOS上で32ビットプロセスを動作させる場合は、各プロセスの論理アドレス空間の上限が32ビットであり、1つのプロセスが利用できるメモリ量はせいぜい4 GiB程度である。そのため、仮にメモリリークが発生したとしても、大容量メモリを搭載している環境では物理メモリの枯渇よりも先にアドレス空間の枯渇が起きてしまう可能性が高い。しかし、64ビットプロセスの論理アドレス空間の上限は64ビットであり、仮想アドレス空間や物理アドレス空間は48ビット程度に制限されている[12]とはいえ、物理メモリが先に枯渇してしまう可能性が高く、また大規模なメモリリークが発生すると仮想メモリも使い切ってしまう。
診断
[編集]メモリリークを診断するためには、一般的にはプログラムの論理構造を調べる、デバッガを使って内部状態を確認するなどの手段をとる必要がある。時系列に沿ってメモリ消費量をトレースすることによりヒントが得られることもある。キャッシュを使うプログラムの場合、設定を間違えていれば消費メモリのサイズはシステムを停止させるまで際限なく増え続けていくかもしれない。メモリリークが発生していることが分かっている場合、最も単純な原因調査の方法は、しばらくシステムを放置した後にメモリ上の適当なオブジェクトを拾うことである。同じ種類のオブジェクトが大量に見つかれば、おそらくその発生源がリーク元である[13]。メモリのダンプを取得して解析することも診断手段である。Windows用にはマイクロソフトから無料のツールが用意されている。
しばしば、メモリを大量に消費する症状のみを見て、プログラムがメモリリークを起こしていると誤診断されることがある。しかし、メモリの消費量のみからメモリリークか否かを判断するのは難しい。なぜならば、仮にメモリが(不必要に思われるほど)大量に消費されていたとしても、そのプログラムが本当にそれだけのメモリを必要としていたり、将来必要になるという理由で確保したりしている可能性があるからである。単にメモリを無駄遣いしているだけということもありうる(プログラムのバグではあってもメモリリークではない)。
また、メモリを解放しているはずなのにプログラムの使用メモリが減らない現象を見て、メモリリークが疑われる場合がある。実は、ほとんどのアプリケーションソフトウェアプログラムはメモリの確保・解放処理を、OS固有の下位レベルAPIを直接利用して行なっているわけではなく、その上位層として構築されたライブラリ(C言語のmalloc関数など)や仮想マシン(Java仮想マシンや.NETのCLRなど)を経由して行なっている。アプリケーションがいったん確保・解放したメモリは、のちの再確保処理を高速化するためにライブラリや仮想マシンが蓄えておく(プールしておく)ため、OSから見えるメモリの使用量が減らないように見えているのである。
Microsoft Visual C++のデバッグ用ランタイムライブラリ (CRT) には、malloc
関数やnew
演算子をフックしてメモリリークの検出を容易にするための診断機能が用意されている[14]。この機能を活用することで、解放されずリークを引き起こしているメモリ確保処理がソースコード上でどの位置にあるのかということを突き止めることもできる。
サードパーティー製診断ツール
[編集]Windows上では、Intel Parallel InspectorやMicro Focus BoundsCheckerといったサードパーティー製のメモリエラー検出ツールに、メモリリーク検出機能が付属しているので、これらを利用してメモリリーク位置を特定することが可能である。ただし誤検出することもあり、完全ではないので、前記の診断方法と併用することが望ましい。メモリデバッガも参照。
プログラミング言語による対策
[編集]プログラミング言語によってはメモリ解放を仮想マシンやフレームワークなどのシステム側に委ねるガベージコレクション (GC) が取り入れられており、メモリリークが起きにくくなっている。ガベージコレクションを言語組み込みの機能として持つ言語には、LispやJava、C#/VB.NETなどの.NET言語全般、JavaScript、Lua、Perl、PHP、Python、Rubyなどがある。特に動的型付け言語はその性質上、何かしらのGC機構を持っている。
一方でC言語のようなガベージコレクションが組み込まれていないプログラミング言語では、メモリリークが起きないように注意深くプログラムを設計・実装する必要がある。C++においてはガベージコレクションこそないものの、デストラクタ機能を活用したRAIIにより、所有権や参照カウントなどの仕組みを実現し、メモリ寿命管理を自動化・簡素化することができる。これは後述のリソースリークにも適用できる。ただし、参照カウント方式は循環参照によるメモリリークを回避することができないという欠点も持っており、必要に応じて弱い参照を併用する必要がある。Pythonは循環参照問題に関して、参照カウントのほかに世代別ガベージコレクションを補助的に利用することで対処している。なお、参照カウントによる寿命管理を広義のGCに含める場合もあるが、ダイクストラの定義によれば、参照カウントは自動メモリ管理の一形態ではあるものの、GCではないとされている[15]。
Objective-Cはもともと参照カウント方式でオブジェクトライフサイクルを管理しており、さらにバージョン2.0でGCを導入したが、Automatic Reference Counting (ARC) の導入に伴い、GCは非推奨化・廃止された[16]。SwiftもARCを標準的に採用している。
ガベージコレクションの限界
[編集]ガベージコレクションはどこからも参照されていないメモリ領域を自動的に解放する。しかし、プログラマの意図・把握しないところに参照(強い参照)が残っていると、今後全く利用されないはずのメモリ(オブジェクト)がガベージコレクションによる解放の対象にならず、確保されたままになることがある。これもまたメモリリークの一種であり、最終的にはメモリ不足によりシステムをクラッシュさせる等、上記のような問題と全く同種の問題を引き起こす。この種類のメモリリークに関してガベージコレクションは無力である。この問題に関しては、一部の参照を弱い参照で置き換えて従属関係を与えることにより回避できるケースもある。
リソースリーク
[編集]リソースリーク (英: resource leak) とは、メモリリークをファイルやネットワーク接続、OS、ハードウェアなど一般のリソースに拡張した概念である。
例えば、一般的なOS環境でファイルへの読み書きをする場合、最初にファイルを開く処理が必要であり、これはメモリでいえば確保する処理にあたる。ファイルに対する処理が終了した時には、ファイルを閉じ、該当ファイルの使用権をOSに返却する処理が必要であり、これはメモリを解放する処理にあたる。通例、ファイルを閉じることで自動的にストリームがフラッシュされ、変更内容も反映(コミット)される。このとき、ファイルを閉じる処理を忘れて、不要なファイルがいつまでも開いた状態にあれば、リソースリークが発生したことになる。ユーザーが他のプログラムでそのファイルを開こうとしても失敗したり、ファイルを削除しようとしても失敗したり、などの原因不明のエラーに悩まされることになる。ほとんどの場合、リソースリークを起こしたプログラムが終了すれば、OSによって正常な状態に復帰されるが、ファイルを閉じるための正常な処理を経ていない場合、ファイルへの変更内容が反映されないこともある。
Windowsでは標準のタスク マネージャーやProcess Explorerといったツールを使って、各プロセスが使用しているハンドルの数を監視することで、リソースリークを診断することが可能である。
C++やDelphiではデストラクタによるRAIIを活用することでリソースリークを防止できる。デストラクタによる解放処理の自動化は、仮にメモリあるいはリソースの確保処理と解放処理の間で例外が発生した場合も、確実に解放を実行できるという特性がある(例外安全)。JavaやC#には仮想マシンの管理下にある「マネージリソース」と管理下にない「アンマネージリソース」があり、マネージリソースはどこからも参照されなくなったとき(GCルートから到達不可能になったとき)にガベージコレクタが自動解放してくれるが、ファイルストリームやネットワーク接続、あるいはJNIやP/Invokeを使って直接確保したアンマネージリソースはGCの管理下になく、明示的に解放処理を記述する必要がある[17]。JavaやC#ではtry-finally文を使うことで、例外が発生した場合も必ずリソースの解放処理を実行するようなコードを記述できるものの、いささか煩雑である。Java 7以降のtry-with-resources文や、C#のusing文といった、C++のデストラクタによるRAIIに類似した機能を活用することで、簡潔にリソースリークを防止できる。なお、JavaやC#のファイナライザは保険(最終防壁)としての役目しか期待できず、GCの性能を低下させることもあるため、一般的には非推奨な方法である。
脚注
[編集]注釈
[編集]出典
[編集]- ^ 動的メモリ管理に関する脆弱性(その2):もいちど知りたい、セキュアコーディングの基本(7)(1/2 ページ) - @IT
- ^ malloc - cppreference.com
- ^ ISO/IEC 9899:1999
- ^ CC1352R: malloc() hangs the system when running out of heap - Bluetooth forum - Bluetooth®︎ - TI E2E support forums
- ^ operator new, operator new[] - cppreference.com
- ^ Deep C++ - CとC++での例外処理、第5部 | MSDN, Internet Archive
- ^ nothrow_t - cpprefjp C++日本語リファレンス
- ^ OutOfMemoryException Class (System) | Microsoft Learn
- ^ プロセス間のメモリ割り当て | App quality | Android Developers
- ^ 2. Memory Management - High Performance iOS Apps [Book]
- ^ iPadOS 16、本日提供開始 - Apple (日本)
- ^ ASCII.jp:メモリー不足を根本的に解決する64bit OSの仕組み (4/4)
- ^ Chen, Raymond『Windowsプログラミングの極意 歴史から学ぶ実践的Windowsプログラミング!』アスキー、2007年(原著2006年12月)。ISBN 978-4756150004。
- ^ CRT debug heap details | Microsoft Learn
- ^ Understanding Classic Java Garbage Collection - InfoQ
- ^ NSGarbageCollector | Apple Developer Documentation
- ^ アンマネージ リソースのクリーンアップ - .NET | Microsoft Learn