動的メモリー管理とガベージコレクションを理解する

公開2011-03-30
更新2011-03-30

動的メモリー管理

 プログラムは、実行中に、必要になった分のメモリーを OS から割り当ててもらって確保したり、逆に不要になった分を解放して OS に返したりします。いわゆる「動的なメモリー管理」です。C 言語では標準関数の malloc と free で、C++ では主に演算子の new と delete で、これを行います。

 必要なサイズを指定してメモリーを要求すると、「確保」されたメモリーの塊の「アドレス」が渡されます。このアドレスは変数に入れて覚えておきます。アドレスを覚えている限り、その場所にあるメモリーにアクセスしてデータを取り出したり書き換えたりできます。

 使い終えて不要になったメモリーは OS に返却しなければなりません。アドレスを渡して「これはもう要らない」と「解放」を指示します。解放されたメモリーアドレスは無効になります。解放後に無理にアクセスすると「不正な処理」でエラー扱いになります。

 コーディングによる手動のメモリー管理で問題になるのは解放です。たとえば「不要になったのに解放するのを忘れている」箇所があると、動作を続けるうちに OS のメモリーを消費し尽してしまう可能性があります。(解放せずに割り当てばかりを一方的に要求するループ、など。)これを「メモリーリーク」と呼びます。

 このようなケースでは「ガベージコレクター」(以下 GC)による自動メモリー管理が役に立ちます。

GC だいしゅき!

 GC を持つプログラミング言語の実行環境では、メモリーの確保と解放は GC が仲介します。要求された全てのメモリーとそれらのアドレスを GC は把握しています。

 GC はプログラムの実行中、使われていない、「参照」されなくなった「オブジェクト」(=確保したメモリーの塊)を「ゴミ」と判断して、定期的に回収・解放します。

 回収のタイミングが遅くなることはあっても、「忘れる」ことはありません。使い終わって不要になった(ことが利用状況から見て明らかな)オブジェクトは、いつかは回収されます。通常、新しいメモリーが要求され、それを確保するときには、GC は必要な分のゴミ回収作業を終えていることが保証されるはずなので、「コーディングのミスによるメモリーリーク → OS のメモリー不足」に陥る不具合は、これで大きく削減できます。

 GC があれば、もはや不注意によるメモリーリークは怖くありません。それどころか、必要なだけメモリーを要求して、解放については考えません。使い終わったものは GC が適当に片付けてくれます。――これはプログラムのコードを一変させます。まるでメモリーが無限に湧き出るかのごとく、一方的にメモリーを要求し続けるような記述(C 言語では駄目、絶対!)をして、しかし実際には要求した全てのメモリーを常時フル使用することはないので、不要になった未使用分は裏で自動的に回収され、再び使い回されるのです。賢いですね。

 もう一つの大きな利点として、オブジェクトの「参照のミス」がなくなります。GC のある環境では、あるオブジェクトから他のオブジェクトに「リンク」を張るとき、その参照先のメモリーアドレスは常に正しく存在するオブジェクトであることが保証されます。(GC が全てのオブジェクトを、解放のタイミングも含めて管理しているため。)参照先のオブジェクトが勝手に消えてしまうこともないので、C++ ではありがちなポインターの参照ミスとは無縁になります。(既に存在しない、不正な参照先を指すポインターは、非常に悩ましい問題。)

 たとえリンクを不用意に外してしまったとして、それでその参照先が誰からもリンクされていない、誰にもアドレスを知られない「迷子」になってしまったとしても、メモリーリークに陥ることはありません。まさにそのような迷子こそが GC の回収対象なのです。GC だけはアドレスを覚えています。(迷子になったオブジェクトは二度とプログラマーの手に戻ることはなく、メモリー空間を独り永遠に彷徨うことになる。――それは存在価値のないゴミである。)

GC の留意点

 ここまでは GC の利点を述べてきました。しかし GC も完全ではありません。留意すべき点やトレードオフもあります。

 まず、コーディングのミスによるメモリーリークが完全に解消されるわけではありません。GC がオブジェクトをゴミだと判断する唯一の基準は、「参照の基点から、そのオブジェクトに辿り着けないこと」です。本来は不要であるはずのオブジェクトを、やはりコーディングのミスによって、到達可能な生存オブジェクトから参照し続けていたら、それは当然、回収できません。(ちょっとややこしいですが、「誰からも参照されていないかどうか」だけでは不十分です。誰かから参照されていたとしても、結局、それらが「ルート」と呼ばれる基点からの参照元を持たず到達不能であれば、まとめてゴミ扱いになります。)

 二点目、GC の管理下では、逆にオブジェクトのメモリーをプログラマーの任意のタイミングで解放することはできません。(それを許すことは、上記の「参照ミス」の発生の可能性を受け入れること。)回収作業を強制的に発動させることは可能ですが、アルゴリズム上の都合から、強制回収は推奨されません。「とにかく回収を頻繁にやらせれば常にメモリーを節約できる」というのは誤った認識です。(GC が採用しているアルゴリズムの種類によって特性は異なる。「世代別 GC」では、かえって回収効率が悪化、回収が遅くなる場合も。)オブジェクトの回収とメモリーの解放のタイミングは、あくまで GC の気分次第です。動作の「予測」を付けにくいのです。

 また、たとえば「ファイルハンドル」など、汎用メモリー以外の OS のリソースについては、その管理は OS の管轄であり、あくまで一介のプログラムの一部品に過ぎない GC では対応しきれません。一応の対処手段として、「リソースに関連付けられたオブジェクトがゴミとして回収される際に、同時にそのリソースも解放する」ような仕組みにすることは可能です。(実際、.NET Framework ではそうなっています。)が、先述の通り、GC による回収のタイミングは「未定義」であり、リソースを任意のタイミングで解放しなければならないケースでは GC の挙動だけに任せられません。依然として手動での積極的なリソース解放操作が必要になります。(C# 言語で「IDisposable オブジェクトは using ブロックで囲って利用すべき」理由がこれです。)

 もう一つ、GC は高コストであることも覚えておかねばなりません。オブジェクトの生死を判断する処理、ゴミとなったメモリーを回収する処理。これらは比較的に負荷の高い処理であり、愚直な実装だと、回収作業の都度、プログラムが一時停止状態になってしまい、処理のもたつきで反応が悪くなることもあります。リアルタイムの応答性が要求される分野、あるいは大規模なオブジェクト数を扱う分野では、GC は(つまり、GC のある言語は)選択できない場合もあるのです。(もちろん、昨今の言語処理系の実装では「世代別管理」や並列化による改良が重ねられており、普通のデスクトップアプリケーションの範疇では、そうそう酷い状態にはならないでしょう。)

連絡先



Copyright © 2008-2011 Toru TAKAGI