Garbage Collection

Revised: 27th/May/2004; Since: Mar./3rd/2002

Java プラットフォームでは、オブジェクトはメモリ容量の許す限り幾つでも作成することが可能であり、明示的にメモリ上から削除する必要もありません。 Java 実行環境(runtime environment)は、それ以上使わないオブジェクトを特定し、削除してくれます。このプロセスは Garbage Collection (ゴミ集め)と呼ばれています。

データ・エリア管理

GC (Garbage Collector) は、ヒープに対する自動記憶域管理システムです。メモリ上のヒープ内で不要なった領域 (garbage) を再利用可能にし (recycle) 、断片化した領域を再配置する (defragmentation) 、バック・グラウンドのオーバーヘッド・プロセスとして起動します。

Java にはポインタが無く、その帰結として、データエリア管理という概念がありません。Java では、データエリアの管理は GC が自動的に行ってくれるので、プログラマが意識して管理するものではありません。メモリ・リークや領域違反が発生すれば GC のバグです。プログラマは、インスタンス管理として、GC の動作を補助することになります。

ポインタとランダム・アクセス

一般に、メモリ上のアドレス/オフセット値を明示的に指定する仕組みをポインタと呼びます。ポインタを持つ言語の場合は、メモリ上の任意のリソースに、いつでもランダム・アクセス可能なことが特徴です。アプリケーション・ロジックに依存しないで、オーバーヘッド処理によってメモリ領域の要/不要を判断することはできません。したがって、自分で割り当てた領域は、自分が責任を持って解放してやる必要があります。メモリ解放の遺漏(解放し忘れること)を、メモリ・リーク (memory leak) と呼び、不正なポインタによって、他の領域のデータを壊してしまうことを、ストレージ・オーバーレイ(記憶域保護違反)と呼びます。

メモリ・リークが発生すると、実行環境が配下として割り当てたメモリ領域を全てクリーンナップするまで、永遠にメモリの当該領域を占有し続けることになります。ランダム・アクセス可能な仕組みの下では、メモリ・リークを解消することは極めて困難です。というのも、ポインタでランダム・アクセス可能な仕組みの下では、占有されているメモリ領域が、解放し忘れているのか、先々利用する可能性があるので居座っているのかを判断することが、原理上できないからです。特定の処理で、メモリの使用率が上昇する場合に限り、メモリ・リークの可能性が疑われることになります。

参照とインスタンス管理

Java には、ユーザが利用するポインタの仕組みがありません。ポインタの代わりになるのは、オブジェクトに対する参照です。参照は、演算子 new によって JVM から返される、テーブルで管理されたシンボリックなものです。デバイスの論理アドレスに依存せず、コードが明示的に指定できるものではありません。

new で返される参照は変数に代入して初めてアプリケーションの処理対象になります。JVM から参照が返されるのは一度きりなので、あるオブジェクトの参照のコピーを保持する全ての変数がメモリ上からドロップされたら、再び同じインスタンスを参照することは不可能です。つまり、一時点でコンピュータ制御が到達可能なコードの範囲内に、当該オブジェクトへの参照が無くなったら、当該オブジェクトのインスタンスが占有しているメモリ領域には、永遠にコンピュータ制御が到達不可能であると判断できるわけです。

オブジェクトを参照していた変数がそのスコープから外れると、通常はその変数の参照はドロップされます。或いは、その変数に特別な値である null 値を代入することで、明示的に GC の回収対象にすることも出来ます。一般に、プログラムは、同じオブジェクトに対して複数の参照を持つので、全ての参照がドロップされていなければ、 GC の対象とならないようになっています。

このような、死んだオブジェクトの識別ロジックをマーキングと呼び、その処理の間はアプリケーションが停止します。死んだオブジェクトのマーク (mark)、回収 (sweep)、断片化の解消とヒープサイズの圧縮 (compact) が GC の主な仕事になります。

GC のアーキテクチャ

現代的な GC は、単純な mark-sweep-compact のみではなく、複数のアルゴリズムを組み合わせて、実行時のリソース使用率に応じて、動的に動作を変えています。ここで、簡単に GC アルゴリズムを時系列で紹介します。

インクリメンタル GC

そもそも、GC プロセスは、メモリ(セントラル・ストレージ)上の物理アドレスに依存する処理を実行するスレッドで、実行時にはアプリケーションの処理を停止させるものです。一旦 GC が起動するや、GC が終了するまでアプリケーション処理が停止してしまう場合は、サーバサイドなどで、スタック階層が深い場合は、秒単位で JVM 上の業務アプリケーションを停止させます。

これを嫌って、GC 処理を中断可能にして、タイムスライス(CPU 時分割)でアプリケーションと平行して処理できるようにしたのが、インクリメンタル GC です。

コンカレント GC

インクリメンタル GC は、タイムスライスによって、長期間のアプリケーションの停止を避ける方法ですが、何れにせよ、アプリケーションが停止することに変わりはありません。並行処理可能な部分を平行に実行する GC が、コンカレント GC です。コンカレント GC の場合、マーキング以外の時間はアプリケーションが停止しません。最も長時間掛かるのがコンパクトの間だったので、コンカレントでない GC と比べると、殆ど停止しないといえます。

世代別 GC

JVM 1.3 以上で採用されている世代別 GC は、ヒープを、若い世代 (young generation) 、古い世代 (Old Generation) 、永続的な世代 (permanent generation) の三つに分け、オブジェクトを生存時間に応じてコピーしていきます。オブジェクトの生存時間が短い若い世代には、一時オブジェクトを初めとして、ゴミになるオブジェクトが占める割合が圧倒的に高いために、若い世代に対して集中して GC を行うことで、少ないコストで高い効果が挙げられます。

若い世代の領域も、エデン・スペース (eden space) と二つの生存スペース (survival space) に分けられます。エデンス・ペースには、スタック状のデータ構造でオブジェクトを割り当て、これがオーバーフローすると、生きているオブジェクトだけが生存スペースにコピーされ、エデンス・ペースは空の状態に戻り、再びオブジェクトの割り当てが可能になります。若い世代の領域のコピーによる GC を scavenge (廃品回収、腐肉喰らい)と呼びます。

これらの領域の使用率と CPU 使用率に応じて、世代間コピーや mark-sweep-compact などの、複数のアルゴリズムの GC 処理を組み合わせて実行することで、リソースの消費を抑え、アプリケーションの停止時間が短くなるように調整されています。

GC の挙動は、複数の処理を組み合わせて効率を上げるように設計されているため、明示的に挙動を指定することはできません。JVM のパラメタで与えた構成情報に従ってバックグラウンド処理として動作します。

GC のオプション

GC に依存するオプションに、ヒープサイズを指定するものがあります。ここで指定可能なものは、Garbage Collected Heap と呼ばれる領域のサイズです。ヒープサイズが小さいと、GC が頻繁に発生するため、サーバのスループットが悪化します。必要なだけの領域が確保できなくなると、 OutOfMemoryError が発生して、アプリケーションが実行不能になります。一方、ヒープサイズが大きいと、GC の起動回数は減りますが、一回の GC に必要な時間が長くなり、その過程の、アプリケーション停止時間 (stop-the-world) も長くなって、レスポンスタイムに問題が発生する可能性があります。

オプション指定値
-Xms初期ヒープサイズ(全体)
-Xmx最大ヒープサイズ(全体)
-Xmn (-XX:NewSize)young generation領域サイズ
-XX:NewRatioyoung generation領域とtenured generation 領域の比率
-XX:SurvivorRatioeden領域とsurvivor領域の比率
-XX:TargetSurvivorRatioyoung generation が full になるまでに survivor space で利用可能なスペースの比率
-XX:MinHeapFreeRatioGC 後に拡張されるまでに消費されるヒープの比率
-XX:MaxHeapFreeRatioGC 後にシュリンクされるまでに消費されるヒープの比率

-Xms-Xmx の値を等しくとると、全ヒープサイズが固定され、拡張もシュリンクもしなくなります。

デフォルトでは、young generation のサイズは、NewRatio で制御できます。例えば、-XX:NewRatio=3 とすれば、young generation 対 tenured generation が 1:3 になります。つまり、 eden スペースと survivor スペースの合計サイズが、全ヒープサイズの 1/4 になります。

NewSizeMaxNewSize は、 young generation サイズの最小値と最大値を指定します。これらの値を等しく指定すると、全ヒープの中の、 young generation 領域のサイズが固定されます。NewRatio で young generation のヒープサイズを動的に指定するよりも、パフォーマンスがよくなる可能性があります。

SurvivorRatio によって、survivor スペースと eden スペースの比率を指定できます。例えば、-XX:SurvivorRatio=6 とすれば、survivor 対 eden が 1:6 になります。つまり、二つの survivor スペースの各々のサイズは、young generation のサイズの 1/8 になります。

TargetSurvivorRation は、young generation が GC されて、オブジェクトが old generation に移動されるまでに利用可能な survivor スペースの比率を指定するものです。例えば、-XX:TargetSurvivorRatio=90 とすれば、 survivor スペースの 90% が使われるまで、young generation が full だと考えられることはありません。それまで、オブジェクトが old generation にプロモートされないということを意味します。

MinHeapFreeRatioMaxHeapFreeRatio JVM がヒープを拡張/シュリンクするときに、キープするフリー領域の比率を指定します。ヒープの全体サイズは、-Xms-Xmx で指定します。

GC の詳細は、Sun の文書を参照ください。特に、Diagnosing a Garbage Collection problem には、目を通しておくと良いでしょう。サードベンダー実装の JVM の場合は、対応する文書を参照する必要があります。例えば、IBM の場合は、次のような文書が一般向けに公開されています:

Pinned Object

参照

オブジェクトは、参照されなくなると GC 対象になります。実際に回収されて再利用可能にされるかどうかは、実行時の状況によりますが、 GC が物理アドレス上からインスタンスを移動、又は削除できるということです。逆に、参照が存在する場合は、 GC がインスタンスを移動/削除できません。これを、ピンド・オブジェクトと呼びます。

J2SDK 1.2 以上 (Java2 以上) では、java.lang.ref パッケージが導入されました。これは、通常の参照に加えて、三つの参照形態を導入します。

強参照
通常の参照。コンストラクタによって生成されたインスタンスに対する参照を new 演算子で返したものを変数に保持している状態。当該変数がスコープから外れるまで、メモリ上からのドロップ対象にならない。
ソフト参照
メモリセンシティブキャッシュ。 ソフト参照されているオブジェクトは、GC の回収対象であると同時に参照できる。強参照(強可到達)の場合は、 GC 回収対象になるということは、参照できないことと同義語。ソフト参照(ソフト可到達)の場合は、つい今しがた迄参照可能であったオブジェクトが、リソースの消費率や生存時間に応じて、GC に回収されてメモリからドロップされる可能性を持つ。nice to have なキャッシュに適している。
弱参照
カノニカライズ化マップ。ソフト参照よりも弱い参照。ソフト参照の場合は、GC 実行時でも、リソースの消費率や生存時間に応じて、回収される場合もされない場合もあるが、弱参照(弱可到達)の場合は、GC 起動時に発見されていれば必ず回収される。但し、GC が弱可到達を発見できない場合もあり、その場合は当然回収されず、次回の起動時に同様に判断が下される。
ファントム参照
回収直前に、対応するキューにエンキュー (Enter Queue) されるので、キューを監視していれば、回収直前のオブジェクトの後処理が行える。回収対象になったのに生存しているファントムのようなオブジェクトに対する参照。他の参照の場合は、エンキューされるのは回収後なので、回収前に察知できるのが特徴。

これらの参照の使い方は、後続の節に回します。ここでは、強参照の難点をクローズアップするものとして、ピンド・オブジェクト (Pinned Object) について説明します。

ピンド・オブジェクト

Pinned Object (Un-Movable Object) は、GC (Garbage Collector) が物理的にメモリ上から移動できないオブジェクトです。スタック上に参照があるオブジェクトや、レジスタにあるオブジェクトは全て移動できないピンド・オブジェクトになりますが、長期間にわたって居座るピンド・オブジェクトは、JNI (Java Native Interface) で参照しているオブジェクトなどにその可能性があります。この場合は、コーディング・ミス/設計ミスである可能性も疑われます。多くのプロファイラは、ピンド・オブジェクトを検知して報告します。

一般に、JVM のメモリ領域はページング不可なので、利用するオブジェクトは、必ずイン・メモリー(物理的にメモリ上に存在する状態)であることが必要です。当該 JVM (アプリケーション・サーバ)の存在時間に比べて、比較的長期間に渡って生存するオブジェクトは、GC の動作を阻害する可能性があります。

インクリメンタルやコンカレントでない GC の場合、GC 実行期間中は、アプリケーションの処理が停止します。特に、コンパクト処理は非常に長い時間が掛かります。コンパクトとは、メモリ上のデータをリオルグ(デフラグ)して、ヒープサイズを圧縮(シュリンク)することです。一回の GC 起動に対して、コンパクトは最大でも一回しか試みられまれません。正常にシュリンクできれば、ヒープサイズはかなり小さくなるので、コンパクトは稀にしか実行されません。

GC によって移動できないピンド・オブジェクトが、ヒープ (Heap) のトップ付近に存在すると、一回の GC ではシュリンク (Shrink) できないために、次回の GC 起動時にもシュリンクを試み、更にその次回にも試みるという寸法なので、アプリケーションの応答速度、スループットに対して莫大な影響を及ぼすことがあります。

Pinned Object
図:Pinned Object

ピンドオブジェクトによるシュリンクの頻繁な実行を抑制するためには、二つの方法が考えられます。

  1. JVM の起動時パラメタで、ヒープの初期サイズと最大サイズを等しく指定する。
  2. 起動時オプションで、連続長を指定する。

初期サイズ -xms[n] と最大サイズ -xmx[n] が同じであれば、そもそ拡張しないので、シュリンクの必要もありません。しかし、初期サイズと最大サイズを等しくとると、デフラグメンテーション(断片化)が発生して、重量級のオブジェクトの生成で、OutOfMemoryError が発生する可能性があります。避けるように強く推奨されています。

これを回避するために、連続してアロケーション可能なメモリ上の空き領域のサイズを指定することで、シュリンクの起動を回避することが考えられます。Sun の JVM の場合は、-XX:MaxHeapFreeRatio で指定できます。詳細は、Java HotSpot VM Options や、Tuning Garbage Collection などのほか、該当ベンダーのドキュメントを参照ください。



Copyright © 2001-2004 SUGAI, Manabu. All Rights Reserved.