Javaにおけるガベージコレクション(パラレル型)の仕組み
ガベージコレクタ
ここではJavaのガベージコレクタのうち、パラレル型ガベージコレクタ(スループット型ガベージコレクタとも呼ばれます)について紹介します。パラレル型ガベージコレクタはUnixベースのマルチCPU環境ないしは、64ビットJVMではデフォルトのガベージコレクタとなっています。young領域、old領域の処理をマルチスレッドで実行することが特徴です。 パラレル型ガベージコレクタの他には、シリアル型ガベージコレクタ、CMS、G1GCというガベージコレクタがあります。
JVMのメモリ構造
まずパラレル型ガベージコレクタの動作を知るために、パラレル型ガベージコレクタのヒープ構造について確認しておきます。 JVMのメモリ構造は以下のような図になっています。なおヒープの構造はパラレル型ガベージコレクションを前提にしたメモリ配置になっています。
図1
オブジェクトの割り当てのアルゴリズムとしては、まずyoung領域のeden空間に割り当てられます。eden空間が一杯になるとガベージコレクションが実施されます。eden空間のオブジェクトはすべて移動または破棄されます。利用されているオブジェクトはsurvivor空間またはold領域のいずれかに移動します。これはマイナーガベージコレクションと呼ばれます。マイナーガベージコレクションを繰り返し、長い期間利用されているオブジェクトはold領域に移動します。old領域のガベージコレクションはフルガベージコレクションと呼ばれます。これはsurvivor空間を含むyoung領域のすべてのオブジェクトが破棄されます。old領域に残るオブジェクトはアクティブな参照を保持しているもののみになります。残されたオブジェクトはコンパクト化され、old領域の先頭に続く部分だけが使用されます。
この動きを実際にプログラムを実行させて確認します。以下のようなJavaのサンプルプログラムを用意しました。 これは、elements配列にObjectをpushし続けるプログラムです。OutOfMemoryErrorを防ぐため、elements配列が一定の大きさになった場合は参照されなくなった配列にnullをセットし、新たな配列を作成します。 以下のオプションとともにこのプログラムを起動させます。
import java.util.Arrays; import java.util.EmptyStackException; public class Test1 { private static Object[] elements; private static int size = 0; private static int MEMORY_CLEAR_SIZE = 8912895 ; public static void main(String[] args) throws InterruptedException { elements = new Object[16]; while (true) { Object o = new Object(); push(o); Thread.sleep(10); } } public static void push(Object e) { ensureCapacity(); elements[size++] = e; } public static Object pop() { if (size == 0) throw new EmptyStackException(); return elements[--size]; } public static void ensureCapacity() { // memory_clear_size になったら、不要な参照を破棄する if (elements.length == MEMORY_CLEAR_SIZE) { size = 0; for (int i = 0; i < elements.length; i++) { elements[i] = null; } elements = new Object[16]; } if (elements.length == size) { elements = Arrays.copyOf(elements, 2 * size + 1); } } }
ヒープのサイズは1024メガバイトに固定して観察します。ガベージコレクタの挙動を確認するためにはログを確認するのが有効です。詳細を確認するために以下のパラメータを有効にしています。
- -verbose:gc
- -Xloggc:/var/log/java/verboseTest1.log
- -XX:+PrintGCDetails
- -XX:+PrintGCTimeStamps
- -XX:+PrintGCDateStamps
早速、以下のようにプログラムを起動させます。
なお環境としては、
- CPUは2コア(Intel(R) Core(TM) i7-4790 CPU @ 3.60GHz)
- OSはLinux(centos7)
- メモリは8GB
- Javaのバージョンは1.8.0_161、Java HotSpot(TM) 64-Bit Server VM (build 25.161-b12, mixed mode)
で実施しています。
java -Xms1024M -Xmx1024M -verbose:gc -Xloggc:/var/log/java/verboseTest3.log -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps Test1
マイナーガベージコレクション
起動するやいやな、マイナーガベージコレクションが発生します。マイナーガベージコレクションのログを確認しましょう。
2018-02-04T22:46:46.520+0900: 63.531: [GC (Allocation Failure) [PSYoungGen: 116736K->38432K(232960K)] 291328K->213024K(932352K), 0.0456891 secs] [Times: user=0.09 sys=0.00, real=0.05 secs]
これは、プログラム開始から63.531秒後にGCが発生しています。発生前にはyoungに116736キロバイトのオブジェクトが存在していましたが、GCによって38432キロバイトに減少し、survivor空間に移動しています。この時点でのyoung空間全体のヒープサイズは232960キロバイトです。 また、ヒープ全体(young空間+old空間)の使用量は291328キロバイトから213024キロバイトに減少しています。この時点でのヒープ全体のサイズは932352キロバイトです。
フルガベージコレクション
フルガベージコレクションのログを確認します。
2018-02-04T22:50:13.691+0900: 3626.849: [Full GC (Ergonomics) [PSYoungGen: 185305K->0K(232960K)] [ParOldGen: 699384K->174592K(699392K)] 884689K->174592K(932352K), [Metaspace: 2614K->2614K(1056768K)], 1.7537211 secs] [Times: user=2.53 sys=0.00, real=1.75 secs]
young空間のサイズは185305キロバイトありましたが、フルガベージコレクションによって使用量は0バイトになりました。old空間は699384キロバイトから174592キロバイトに減少しました。old空間全体のサイズは699392キロバイトです。ヒープ合計としては884689キロバイトから174592キロバイトに減少しました。 メタスペースのヒープサイズは減少ありません。フルガベージコレクションにかかった時間は、CPU時間として2.53秒、実時間として1.75秒かかっていることが分かります。
参考
- [1]6 パラレル・コレクタ:https://docs.oracle.com/javase/jp/8/docs/technotes/guides/vm/gctuning/parallel.html#parallel_collector
- [2]Scott Oaks著,「Javaパフォーマンス」,オライリージャパン,2015