JavaのGCについてメモ

ガーベッジコレクション(以下GC)やメモリについて、あやふやのままにしていたところが多かったので、調べてみたことをメモしておきます。

メモリについて

プログラムの使用するメモリは、レジスタ、スタック、ヒープなど用途によって分けられますが、高級言語になるにつれてこれらは隠蔽されています。
Javaの場合はスタックとヒープ領域の二つに分けられます。

スタック

スタックはメソッド起動ごとにフレームを出し入れする線形のデータ構造です。このフレームの中にローカル変数や引数などのデータを持っています。
メソッドが終了するとフレームは破棄されるので、寿命が短いことが特徴です。
スタックはスレッドごとに割り当てられます。

ヒープ

インスタンスなどを保持する領域です。GCによって、メモリの管理がなされます。
JVMで一つ割り当てられ、全てのスレッドから共有されます。
クラス構造などの静的なデータも保持されており、その領域はメソッドエリアと呼ぶそうです。


GCとは

C言語などでは、mallocとfreeを使って手動でメモリ管理をしていましたが、適当なタイミングで使われなくなったメモリ領域を解放してくれるものがGCです。
Javaや多くのLL言語で実装されている機能です。


GCアルゴリズム

基本編
名前 概要 利点 欠点 実装例
参照カウント オブジェクトが自身がどれだけ参照されているかをカウントし、参照が0になったらGCを発生させる。 分散化、処理時間が短い 相互参照時の対策必要 Perl5など
Mark&Sweep スタックやレジスタなどのヒープ領域を参照しているポインタを全走査し、参照されているオブジェクトに印付け(Mark)する。すべて走査し終わった後に、印がついていないものを解放(Sweep)する。 集中管理できる 処理時間が長い Rubyなど
Copying ヒープ領域を2分割し、片方がいっぱいになったら、もう片方に隙間をつめながらコピーしていく。 断片化しない、処理時間が短い メモリを多く必要とする Scehmeなど
応用編
名前 概要 利点 欠点 実装例
Incremental GC Mark&SweepやCopyingの処理時間がかかるという問題を解決するため、allocateなどのときに、少しずつGCを進める方法 停止時間が短い スケジューリングが面倒 Luaなど
世代別GC 世代の若いオブジェクトを優先的にGCしていく方法 プログラムの停止時間を改善 Full GCのとき時間がかかる Javaなど

JavaでのGCの実装

上記のように、Javaでは世代別GCが採用されています。
世代別GCは、古いオブジェクトはその後も使われる可能性が高く、新しいオブジェクトはすぐに使わなくなる可能性が高い、という考えに基づいて実装されています。
JVMではヒープ領域がさらにNEW領域とOLD領域に分けられており、作られてすぐ不必要になるオブジェクトはNEW領域に保持され、長期間使われるオブジェクトはOLD領域に保持されます。
JVMGCは、NEW領域を対象する「Scavenge GC」と、NEWとOLD領域両方を対象とする「Full GC」という2種類のGCが存在します。
Scavenge GCは頻繁に行われ、処理時間も短いのが特徴です。Scavenge GCを一定回数超えてもNEW領域に存在するオブジェクトがOLD領域に移動をします。
一方、Full GCは、Scavenge GCに比べると処理時間が長いので、あまり頻繁に行われるとパフォーマンスが悪くなってしまいます。
Scavenge GCの仕組みについても説明をすると長くなるので、詳しく知りたい方は「Javaのヒープ・メモリ管理の仕組み (1/2):Javaパフォーマンスチューニング(3) - @IT」を参照してください。


GCを考慮したパフォーマンスの向上方法

GCに考慮してパフォーマンスを向上させるには、Scavenge GCとFull GCの特性を考えた場合、いかにFull GCの頻度を減らすかが重要になってきます。その点から考えて、以下の3つの方法が考えられます。

1. インスタンスの使いまわしを減らす

インスタンス生成や解放のオーバーヘッドを減らすために、インスタンスの使い回しが推奨される場合がありますが、あまり長く保持しているとそのインスタンスがOLD領域へ移動してしまい、結果としてコストの高いFull GCを発生させてしまうことになります。
データベースのコネクションなどの生成/破棄のコストが本当に高いものの場合は使いまわしたほうがよいですが、その他の場合のインスタンスの使いまわしはパフォーマンスに悪影響を及ぼす可能性があります。

2. NEW領域のメモリ割り当てを増やす

NEW領域がメモリ使用がいっぱいになるとScavenge GCが発生します。あまり頻繁に、Scavenge GCが発生するとすぐにインスタンスがOLD領域へ移動してしまいます。
これに対処するためには、NEW領域を増やしてやり、Scavenge GCを発生しにくくする方法が考えられます。
あるいは、OLD領域へ移動するためのNEW領域にいる回数を指定するMaxTenuringThreshold(デフォルト32)の値を多くしてもよいかもしれません。

3. OLD領域のメモリ割り当てを増やす

単純に、OLD領域のメモリ割り当てが増えれば、Full GCの頻度は減ります。しかし、GC時の処理時間が増えてしまうので注意がいります。


Javaのメモリ関係のオプション

Javaの実行時に以下のようなオプションを付けることで、確保するメモリの調節ができます。

  • スタック領域の拡張(デフォルト512KB)・・・-Xss[n]
  • ヒープ領域の起動時のサイズ(デフォルト2MB) ・・・ -Xms[n]
  • ヒープ領域の最大サイズ(デフォルト64MB) ・・・ -Xmx[n]
  • NEW領域のサイズ ・・・ -Xmn[n]
例:
java -Xmn160m -Xmx480m -Xms480m <クラス名>