ガベージコレクションについて基本的なこと

これまでガベージコレクションについて、あまり深く調べたりする勉強することもなかったのですが、 もう少しちゃんと知っときたいなぁと思い、Javaパフォーマンスでお勉強しました。
今回は「第5章 ガベージコレクションの基礎」の内容をもとに、Javaガベージコレクションの基本的な部分についてまとめたいと思います。

そもそもガベージコレクションとは

ガベージコレクションとはJVMがオブジェクトのライフサイクルを管理してくれる機能のことです。
利用されなくなったオブジェクトを探し出し、それに関連付けられたメモリを解放、ヒープのコンパクト化を行います。
Java11(OpenJDK)では実験的なものも含め6つのガーベージコレクションのアルゴリズムが選択できます。

CMS、G1、ZGCはコンカレント型とも呼ばれています。
これらアルゴリズムが、それぞれ異なる方法でガベージコレクションを行っており、どれを選択するかによって性能に大きく影響してきます。
今回はシリアル型、スループット型、CMS、G1について説明しますが、まずは基本的なオブジェクトの管理の仕方について説明します。

オブジェクトの管理の仕方

基本的にオブジェクトは以下のような複数のヒープ領域に分割して管理されます。

  • old領域
  • young領域
    • eden空間
    • survivor空間

なぜ複数の領域で管理しているのかというと、Javaのオブジェクトはほとんどが一時的にしか利用されないため、領域を分けることによりオブジェクトの探索などの処理が高速化します。

young領域に対する処理(マイナーガーベージコレクション)

オブジェクトはまず、young領域の大部分を占めるeden空間に割り当てられます。young領域がいっぱいになると、アプリケーションスレッドをすべて停止してyoung領域を空にします。 その時、eden空間のオブジェクトは破棄、もしくは利用されているものは別の場所に移動します。移動先はsurvivor空間かold領域のいずれかとなります。 すべてのオブジェクトがなくなるため、young領域に対するコンパクト化は必要なくなります。 このyoung領域を空にする処理をマイナーガベージコレクションと呼び、今回説明する4つのガベージコレクションアルゴリズムでは必ず発生しますが、処理自体はすぐに完了します。

old領域に対する処理(フルガベージコレクション

old領域はyoung領域から移動してきたオブジェクトを管理する領域となります。 old領域もyoung領域同様、いっぱいになるタイミングが発生します。JVMはold領域内の使われていないオブジェクトを探して破棄するわけですが、ここの処理がアルゴリズムによって大きく異なります。
このold領域に対する手順をフルガベージコレクションと呼び、アプリケーションが停止する時間が長くなります。

シリアル型

一番シンプルなアルゴリズムで、クライアントクラス(単一CPU32ビットJVMとか)のマシンではシリアル型がデフォルトになります。
ヒープの処理を行うスレッドは1つで、マイナーガベージコレクションとフルガベージコレクション両方がアプリケーションスレッドを全て停止して実行されます。
フルガベージコレクションではold領域のコンパクト化まで行われます。

スループット型(パラレル型)

Java8のサーバクラス(複数CPUもしくは64ビットJVMとか)のマシンでは、スループット型がデフォルトになります。
Java7u4以降ではyoung領域、old領域に対する処理が複数スレッドで行われるため、マイナーガーベージコレクション、フルガベージコレクションともにシリアル型よりは高速になっています。

CMS(Concurrent Mark Sweep)

スループット型やシリアル側のようなフルガベージコレクションに伴う停止時間を短くするために、CMSは設計されました。
マイナーガーベージコレクションの際は、すべてのアプリケーションスレッドを停止して複数スレッドで処理をします。
フルガベージコレクションの際にはアプリケーションスレッドを停止しなくてすむように、複数のスレッドを使ってバックグラウンドでold領域の探索を行いオブジェクトの破棄を行います。アプリケーションスレッドはマイナーガベージコレクションの時しか停止しないので、停止時間の合計はスループット型よりはるかに短くなります。
ただし、old領域のコンパクト化をバックグラウンド処理の中では実施しないため、ヒープの断片化が進みオブジェクトの割り当てができなくなってしまった場合、シリアル型と同様にアプリケーションスレッドを停止し、単一スレッドでold領域のコンパクト化を実施します。
また、アプリケーションスレッドと並行してバックグラウンドでヒープの探索を行うため、CPUの使用率は大きくなります。

G1(Gargage 1st)

Java9以降のサーバクラスのマシンではG1がデフォルトになります。(Java9以降でもクライアントクラスの場合はシリアル型がデフォルト)
およそ4G以上の大きなヒープに対し、最低限の停止時間になるようにという思想の基、設計されています(ヒープ4GB以上じゃないと使用してはいけないというわけではありません)。世代に基づいて処理が行われるのは他と変わりませんが、大きな違いはヒープをリージョンという単位に分けて管理している点です。下記の絵はそのイメージですが、Oはold領域を、Sはsurvivor空間を、Eはeden空間を表しています。

f:id:darshuheider:20190508101658p:plain
G1イメージ
old領域の処理はバックグラウンドで行われるため、ほとんどの場合アプリケーションスレッドは停止しません。また、リージョンに分割されていることでヒープの断片化が発生する可能性がとても低くなっており、CMSよりもフルガベージコレクションが発生する可能性が低くなっています。
young領域を空にする場合は他のアルゴリズム同様、アプリケーションスレッドは全て停止します。

まとめ

沢山種類があってどれを選択すればよいか迷っちゃいそうですが、それぞれの仕組みを理解し、 アプリケーションの要件と照らし合わせながら、選択するのがよいと思います。 また今回調べていて、G1以外にもZGCやShenandoah GCなどあることを初めてしりました。 また余裕があったらこの辺も調べてみたいと思いました。

wiki.openjdk.java.net

wiki.openjdk.java.net