JavaのGCについてメモ
ガーベッジコレクション(以下GC)やメモリについて、あやふやのままにしていたところが多かったので、調べてみたことをメモしておきます。
メモリについて
プログラムの使用するメモリは、レジスタ、スタック、ヒープなど用途によって分けられますが、高級言語になるにつれてこれらは隠蔽されています。
Javaの場合はスタックとヒープ領域の二つに分けられます。
スタック
スタックはメソッド起動ごとにフレームを出し入れする線形のデータ構造です。このフレームの中にローカル変数や引数などのデータを持っています。
メソッドが終了するとフレームは破棄されるので、寿命が短いことが特徴です。
スタックはスレッドごとに割り当てられます。
GCとは
C言語などでは、mallocとfreeを使って手動でメモリ管理をしていましたが、適当なタイミングで使われなくなったメモリ領域を解放してくれるものがGCです。
Javaや多くのLL言語で実装されている機能です。
GCのアルゴリズム
基本編
名前 | 概要 | 利点 | 欠点 | 実装例 |
---|---|---|---|---|
参照カウント | オブジェクトが自身がどれだけ参照されているかをカウントし、参照が0になったらGCを発生させる。 | 分散化、処理時間が短い | 相互参照時の対策必要 | Perl5など |
Mark&Sweep | スタックやレジスタなどのヒープ領域を参照しているポインタを全走査し、参照されているオブジェクトに印付け(Mark)する。すべて走査し終わった後に、印がついていないものを解放(Sweep)する。 | 集中管理できる | 処理時間が長い | Rubyなど |
Copying | ヒープ領域を2分割し、片方がいっぱいになったら、もう片方に隙間をつめながらコピーしていく。 | 断片化しない、処理時間が短い | メモリを多く必要とする | Scehmeなど |
JavaでのGCの実装
上記のように、Javaでは世代別GCが採用されています。
世代別GCは、古いオブジェクトはその後も使われる可能性が高く、新しいオブジェクトはすぐに使わなくなる可能性が高い、という考えに基づいて実装されています。
JVMではヒープ領域がさらにNEW領域とOLD領域に分けられており、作られてすぐ不必要になるオブジェクトはNEW領域に保持され、長期間使われるオブジェクトはOLD領域に保持されます。
JVMのGCは、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を発生させてしまうことになります。
データベースのコネクションなどの生成/破棄のコストが本当に高いものの場合は使いまわしたほうがよいですが、その他の場合のインスタンスの使いまわしはパフォーマンスに悪影響を及ぼす可能性があります。