ヒープ領域におけるメモリ管理について

プログラマによるマニュアル操作

  • mallocやnewによってヒープ領域のメモリを割り当てたり、freeやdeleteによってそのメモリを解放したり、といった操作を指す。Cではヒープ領域を扱うために必須の操作となる。
  • これらの関数や演算子で割り当てたメモリは、そのアドレスを指し示す(参照する)変数を媒介としてアクセスが可能である。解放を行う際は、そのアドレスを指し示す変数を対象として、freeやdeleteを呼び出す。
  • その変数が宣言されたスコープを抜けたとしても、そのメモリ自体は解放されず、単にそのメモリアドレスを指し示すための手段がなくなることになる。これがメモリ解放漏れとなり、そのプログラムの実行中は不必要にメモリを消費していることになる。
  • 解放済のメモリを再解放してしまった場合の挙動は未定義であり、そのような不具合もある。
  • プログラム上、透過的にメモリの解放を行うことはできず、全てはプログラマの仕事になる。メモリアドレスを指し示す変数が存在するスコープを外れる際に、解放が出来ていれば問題無いが、プログラムが複雑になればそのような単純な規則で管理できなくなる。

ランタイムによる動的な自動メモリ管理

  • JavaScala)やGoのGC(Garbage Collection)がそれにあたる。
  • ヒープ領域に確保したメモリの解放はプログラマの仕事ではない。JavaランタイムのVMによって、プログラム実行中に適宜GCが実行される。
  • Javaで採用されているGCアルゴリズムの一つとして、NEW領域とOLD領域を用いてScavenge GCとFull GCを行う世代別方式があり、前者はより多い頻度で軽いGC、後者はより少ない頻度で重いGCとなる。
  • Scavenge GCは、NEW領域が一杯になったときに発動し、NEW領域の未使用(変数によって参照されていない)のメモリを解放する。このとき、使用中のメモリについては、何世代分のScavenge GCを乗り切ったかをカウントしていき、長い世代生き残ったアドレスはNEW領域からOLD領域に移行させる。
  • Full GCはOLD領域が一杯になったときに発動し、NEW領域とOLD領域の未使用アドレスを解放する。
  • Goで採用されているGCアルゴリズムは、世代別方式に比べてより単純なMark-and-Sweep方式がベースになっているようである*1。Mark-and-Sweep方式では、未使用のメモリをマークするフェーズと、マークされたメモリを解放するフェーズ、を繰り返す。*2

コンパイラによる静的な自動メモリ管理

  • Objective-CのARC(Automatic Reference Counting)や、C++のスマートポインタがそれにあたる。
  • GCに対して"静的"と呼んでいるのは、コンパイル時にメモリ管理(解放)の戦略が決定されるからである。

ARC

  • Objective-CにARCが導入*3されたことにより、ARC管理配下のファイル*4については、メモリ解放をプログラマが明示的にコーディングすることはコンパイルエラーとなる。
  • ARCでは、あるヒープ領域のメモリアドレスを指し示す変数がいくつあるかを参照カウント数としてカウントし、参照カウント数が0になった時点でそのメモリは解放対象となる。
  • 変数には強参照(__strong)あるいは弱参照(__week)という修飾子が付与される*5。強参照の変数は、あるメモリアドレスに対する参照カウントを増減させるが、弱参照の変数は参照カウントに対して影響を与えない。つまり弱参照は所有権を持たず、弱参照の変数のみによって指し示されているメモリアドレスは解放対象となる*6
  • 弱参照を効果的に用いることによって、循環参照*7を未然に防ぐことが可能になる。

スマートポインタ

  • C++03でauto_ptrが導入された。auto_ptrは変数としてT*型のポインタを保持し、その変数がスコープを抜ける際に、その変数が所有権を持つメモリが存在すれば、ディストラクタが呼ばれる際にそのメモリに対してdeleteを実行してくれる。なお、あるメモリに対する所有権は、なんらかの変数が持っている。
  • auto_ptrはメモリの所有権が(代入により)移動してしまう、という致命的な欠陥があるため、C++11以降では使用してはいけない。本来所有権を持つべき変数以外の変数に所有権が渡ってしまい、意図せずメモリを解放してしまう、という不具合が混入しやすい。
  • C++11では、unique_ptr、shared_ptr、weak_ptr、という3種のスマートポインタが追加された。
  • unique_ptrでは、あるメモリの所有権を持つ変数はただ一つに限定される(代入が許されない)ため、auto_ptrのような所有権が移動してしまう問題はない。
  • C++11で導入されたmove()によって、明示的に別の変数に所有権を移すことは可能。bool()によって所有権があるかどうかを確認することができる。if(ptr)
  • shared_ptrやweak_ptrは、ARCの強参照や弱参照に似た考え方で、参照カウントを管理してそれが0になったときにメモリを解放する。同様に循環参照の問題も存在するため、その対処としてweak_ptr(ARCでいうところの弱参照)が存在する。
  • C++14で、make_xxx*8ヘルパー関数が導入された。これらを使ってスマートポインタ変数を初期化する。
  • c.f. C++11スマートポインタ入門 - Qiita

(補足)Slab Allocator

  • メモリの割り当て・解放を繰り返すことにより、メモリ空き領域が分断されるフラグメンテーションが発生する。
  • 割り当てたいメモリのサイズが、メモリ空き領域全体のサイズよりも小さいサイズにも関わらず、メモリが確保できない。
  • Slab Allocatorはこのような問題に対して、ある程度大きなサイズを一つの単位としてメモリを切り出すことによって、メモリの空き領域の分断を起きにくくする。
  • In-Memory KVSのmemcachedではフラグメンテーションの問題が起きやすかったために、Slab Allocatorが採用されている。

(補足)自動変数

  • Cにおいて、関数内で宣言した変数は自動変数と呼ばれ、auto修飾子が暗黙的に付加される。この変数はスタック領域内にメモリが確保され、メモリ解放を明示的にコーディングする必要はなく、スコープから外れるときにそのメモリが解放される。

*1:http://golang.org/doc/faq#garbage_collection

*2:Go 1.6ではGCに大きな改良が加えられるらしい→https://docs.google.com/document/d/1kBx98ulj5V5M9Zdeamy7v6ofZXX3yPziAf0V27A64Mo/mobilebasic?pli=1&viewopt=127

*3:XcodeではSDK4.2よりサポートされている

*4:-fobjc-arcコンパイラオプションが付与されたファイル

*5:省略した場合にはデフォルトの__strongが付与される

*6:強参照の変数にメモリの所有権を委ねていると言える

*7:お互いに強参照し合ってしまうことによってデッドロック状態に陥り、決して解放対象にならないような参照関係を作ってしまうこと

*8:[xxx|{unique,shared,weak}]