はじめに
Java Silverの学習を進めている際、ガベージコレクション(GC)という言葉が出てきました。
基本情報技術者試験の勉強の際、なんかメモリを掃除してくれる機能、というような認識程度で詳しくは知らなかったので、Javaにおけるガーベジコレクションについてまとめてみようと思います。
※誤りありましたら、ご指摘いただけますと幸いです。
想定読者
初学者の方
普段Javaを使用している方
ガベージコレクション(GC)とは
日本語にすると「ゴミ回収」となるように、自動的に不要なメモリ領域を解放するプロセスのことを指します。
Javaプロセスを開始する際、メモリの割り当てが行われるわけですが、主に使われる領域のことをヒープ領域と呼びます。
ちなみに、JavaはGCを自動で行ってくれますが、CやC++といった言語では、プログラムが使用するメモリ領域の割り当てやメモリ領域の解放を、明示的に指示する必要があります。
そのため、普段実装していてこの辺りはそこまで意識することはないかと思います。
ヒープ領域とは
ヒープ領域は、下記のような構造になっています。
1. Young Generation (New領域)
短期間しか生存しないオブジェクトを格納する領域です。さらに以下の3つの領域に分けられます。
Eden領域: 新しく生成されたオブジェクトが最初に割り当てられる領域。
Survivor領域: Young Generationでガーベジコレクションを生き延びたオブジェクトが移動する領域(上記で3つの領域があるとしたのは、Survivor Spaceは2つあり、コピーが行われるため)。
2. Old Generation (Old領域)
Young Generationでのガーベジコレクションを繰り返し生き延びた長寿命のオブジェクトが格納される領域。
3. Permanent Generation (永久世代領域)
メタデータ(クラス、メソッド、定数プール)など、Javaランタイムのためのデータを格納する領域。ただし、Java 8以降では、この領域はメタスペース(Metaspace)に置き換えられています。
※Metaspaceとは
Java 8以降のJava仮想マシン(JVM)に導入されたメモリ領域で、前述のPermanent Generation(永久世代領域)を置き換えるものです。
従来のPermanent領域では、クラスやメソッドのメタ情報の他、staticな変数や定数等も領域内に格納していましたが、Metaspaceではクラス・メソッドの情報のみ保持し、その他の情報はJavaヒープ上(New領域、Old領域)の保持に変更されました。
GCの対象例
1つの例として、下記のようなコードがあったとします。
Example example1 = new Example();
Example example2 = example1;
example1 = null;
example2 = null;
1行目では、Exampleオブジェクトを生成し、2行目でexample2にexample1の参照をコピーしています。
3行目ではexample1にnullを代入して参照を解除、そして4行目でexample2にnullを代入して参照を解除しています。
この場合、3行目の時点ではexample2の参照があるため、まだExampleオブジェクトはまだGCの対象ではなく、4行目でexample2の参照が解除された時点でGCの対象となります。
イメージとしては下記のような形です。
GCの種類
大きく分けて2種類あります。
Scavenge GC (Minor GC)
New領域のGCを指します。上述の通り、New領域はEden領域とSurvivor領域から構成されています。
主にEden領域が満杯になったときに実行されます。
生き残ったオブジェクトは、Eden領域からSurvivor領域へ移動され、Eden領域は空になります。一定回数のScavenge GCを経て、まだ生存しているオブジェクトはOld領域に移動されます。
ちなみに、先述の通り、Survivor領域は2つ存在します。今回は便宜上Survivor1とSurvivor2と名付けます。
Survivor領域に存在するデータで、参照が確認されている場合は、どちらか空いているSurvivor領域へデータがコピーされ、Survivor1とSurvivor2を行き来することになります。
もちろん、Survivor領域に存在しているデータの中で、参照がなくなったものは削除されていきます。
Full GC
New領域およびOld領域を一斉に処理するGCを指します。
こちらは主にOld領域が満杯になったときに実行されます。
Scavenge GCにおいて、Survivor領域間の移動回数はカウントされており、一定回数を超えたデータはOld領域へ移行されます。しかし、Scavenge GCではNew領域のみが整理されるのみで、Old領域はSurvivor領域から移行してきたデータが山積みになっていきます。
そこで行われるのがFull GCになります。
GCの頻発を避けるために
ちなみに、Scavenge GCもFull GCも、実行されるとアプリケーションは停止されてしまいます。
なので、GCが頻発すると性能トラブルにも繋がりかねません。
そのため、メモリ効率を意識した実装が大切になります。
例えば、未使用オブジェクトの参照は明示的に開放したり、文字列連結が多い場合はString型よりもStringBuilder型を採用した方がメモリ効率がよくなります。
その他にも、final修飾子を使用することで、再代入やオーバーライドがされないことが保証され、メソッド呼び出し時のオーバーヘッド(付加処理的な)が削減されたりします。
まとめ
研修期間はそこまで膨大なデータを扱ってきていなかったので、GCを考えたりすることは一切なかったのですが、実際に調べてみて裏側を知ることで、メモリ効率の良い実装を心掛けるいい機会になったと思います。
また、調べていく中で、スレッドの観点で見たGC、GCアルゴリズム等々、今回まとめきれなかった部分もたくさんありました。この辺りも、余裕があれば勉強していきたいと思います。