
Java 27でデフォルトGCが変更されたけど、何が変わるの?
というか、そもそもGCって何?
こんにちは!
カサレアルでJavaのコースを担当している櫻庭です。
2026年9月15日にJava 27がリリースされました!
Java 27は、2年ごとのLTS (Long Term Support: 長期サポート)バージョンのちょうど中間にあたるバージョンということもあり、新しい機能の追加はそれほどありません。
JavaはオープンソースのOpenJDKプロジェクトで開発が進められています。OpenJDKでは、Javaへの機能追加をJEP (JDK Enhance Proposal)として管理しています。JEPには機能ごとに番号がついており、Javaのバージョンごとにどの番号のJEPが取り入れられるか決まっています。
Java 27では以下の9個のJEPが導入されました。
- JEP 523: Make G1 the Default Garbage Collector in All Environments
- JEP 527: Post-Quantum Hybrid Key Exchange for TLS 1.3
- JEP 534: Compact Object Headers by Default
- JEP 536: JFR In-Process Data Redaction
- JEP 531: Lazy Constants (Third Preview)
- JEP 532: Primitive Types in Patterns, instanceof, and switch (Fifth Preview)
- JEP 533: Structured Concurrency (Seventh Preview)
- JEP 538: PEM Encodings of Cryptographic Objects (Third Preview)
- JEP 537: Vector API (Twelfth Incubator)
PreviewやIncubatorとついているJEPは、お試し機能ということを示しています。逆にいうと、PreviewやIncubatorがついていないJEPが正式な機能(Standard JEPと呼びます)となります。
Java 27でのStandard JEPは4個あります。しかし、いずれも言語仕様の変更や、APIの変更ではありません。Java 27で導入されたJEPは、ヒープの使用に関する変更や、安全性を向上させる機能になります。
これらのStandard JEPのうち、JEP 523: Make G1 the Default Garbage Collector in All Environmentsがちょっと気になるところです。
このJEPは、デフォルトのGCをG1GC (Garbafe-First Garbage Collection)に変更するというJEPです。
今までCPUやメモリーなどのリソースが潤沢にある環境ではG1GCがデフォルトのGCとして使われてきました。一方、低リソース環境ではシリアルGCがデフォルトGCになっています。
これに対し、Java 27からはすべての環境でG1GCがデフォルトのGCとして使用されるようにまります。
とはいうものの、そもそもGCって何なのかとか、シリアルGCとかG1GCって何など疑問に思われるかもしれません。
そこで、本エントリーではGCについて説明を行い、JEP 523で取りあげられている2つのGCのアルゴリズムについて紹介します。
ガベージコレクションとは
Javaのオブジェクトは、JVMが確保したメモリー領域であるヒープに配置されます。
オブジェクトのサイズは、クラスで定義されているフィールドの数などによって決まります。小さいオブジェクトもあれば大きいオブジェクトもあるということです。
このサイズの違うオブジェクトを下図のようにヒープに配置します。

しかし、オブジェクトを配置していくと、いつかヒープが一杯になり、新たにオブジェクトを配置できなくなってしまいます。

そこで、登場するのがガベージコレクション(Garbage Collection: GC)です。
GCは、まず使われているオブジェクトと使われていないオブジェクトを選別します。この処理をマーク処理と呼びます。
ところで、この使われていないオブジェクトというのは、どういうことなのでしょう。
オブジェクトが使われているというのは、そのオブジェクトを参照先として保持しているオブジェクトがあることを示しています。逆にいうと、どこからも参照されなくなったオブジェクトが使われていないオブジェクトです。
オブジェクトからオブジェクトへの参照というとフィールドで定義しているものだけのような気がしますが、ローカル変数で参照しているものも含みます。
たとえば、以下のprintYear()メソッドではLocalDateオブジェクトを生成しています。メソッドが処理されている間はローカル変数dateがLocalDateオブジェクトを参照しているので、このLocalDateオブジェクトは使われている状態です。
printYear()メソッドを抜けると、ローカル変数はもちろん使えません。このため、ローカル変数dateからの参照はなくなってしまい、LocalDateオブジェクトは参照されなくなります。
つまり、このLocalDateオブジェクトは使われなくなったということです。
void printYear() {
var date = LocalDate.now();
IO.println(date.getYear());
};マーク処理で使われていないオブジェクトを判別したら(下図の灰色のオブジェクト)、その領域を回収します。これをスイープ処理と呼びます。

オブジェクトを回収したら、空いた領域に再びオブジェクトを配置できます。

また、回収した後に、残っているオブジェクトを並べなおす処理を行うこともあります。これをコンパクション処理と呼びます。

コンパクション処理を行うと、効率的にオブジェクトを配置できます。しかし、コンパクション処理は時間がかかります。このため、GCのアルゴリズムによっては、通常はスイープだけで、本当にヒープが逼迫した時だけコンパクションを行うなどの戦略をとるものがあります。
この一連の処理を行うのがGCなのです。
世代別ガベージコレクション
前節で説明したGCはヒープを1つの領域として管理するGCです。このようなGCのアルゴリズムのうち、もっとも基本となるのがマーク&スイープGCです。
しかし、広いヒープ領域を常に監視するには、時間がかかってしまいます。このため、GCを効率化するためにさまざまなアルゴリズムが提案されています。
それらのGCアルゴリズムの1つに世代別GCがあります。
世代別GCは、若いオブジェクトはすぐに使われなくなり、古いオブジェクトはずっと使われ続けるという傾向を利用します。
ここでいうオブジェクトの若い、古いというのは、オブジェクトがGCを生き延びた回数によります。新しく生成されたオブジェクトは、まだGCを経ていないので、0歳ということができます。そのオブジェクトがGCを3回生き延びたとしたら3歳ですね。
通常は決められたしきい値でオブジェクトの若い、古いを判別します。しきい値が7であるならば、3歳のオブジェクトは若いオブジェクトとなります。
たとえば、先ほどのprintYear()メソッド内のLocalDateオブジェクトは生成された後、メソッドを抜けたらすぐに使われなくなります。このメソッドの処理時間は短いことが予想されるので、LocalDateオブジェクトがGCを経る回数は少ないでしょう。
このように若いオブジェクトはすぐに使われなくなる傾向があるということです。
この傾向を利用して、ヒープを若いオブジェクト用の領域と古いオブジェクト用の領域に分割し、それぞれを別々にGCするのが世代別GCです。若いオブジェクト用の領域をYoung領域(New領域と呼ぶ場合もあります)、古いオブジェクト用の領域をOld領域と呼びます。
オブジェクトが生成されたら、必ずYoung領域に配置されます。Young領域はOld領域に比べ狭いため、すぐに一杯になります。このため、Young領域は頻繁にGCが行われます。
Young領域に配置されたオブジェクトがしきい値以上にGCを生き延びた場合、そのオブジェクトはOld領域に移動させられます。
Old領域はYoung領域に比べると広く、またオブジェクトの配置もYoung領域に比べると少ないので、Old領域に対するGCは頻度が少なくなります。
Young領域とOld領域に対するGCは、それぞれ同じGCアルゴリズムを使用する場合もあれば、異なるGCアルゴリズムを使用する場合もあります。
つまり、世代別GCは、それぞれの領域に対してどのようなGCを行うかによって、さまざまなGCアルゴリズムが存在します。
JEP 523で俎上にあげられているシリアルGCとG1GCも世代別GCを使用しています。どちらもGCアルゴリズムは異なるのですが、領域の区分のしかたなど共通する部分も多くあります。
そこで、ここでは世代別GCの一例として、シリアルGCとG1GCに共通したアルゴリズムについて紹介しましょう。
シリアルGCとG1GCにおける世代別GC
シリアルGCとG1GCで使用される世代別GCでは、Young領域をさらにEden領域とSurvivor領域に分割します。さらに、Survivor領域を二分割します。
Old領域はTenure領域と呼び、分割せずに使用します。

領域のサイズはTenure領域がもっとも大きく、その次にEden領域が続きます。2つのSurvivor領域が一番狭い領域になります。
オブジェクトが生成されると、必ずEden領域に割り当てられます。これを表したのが下図です。

オブジェクトの左上の数値は、オブジェクトの年齢を表しています。ここでは新しく生成されたオブジェクトなので、すべて0歳です。
Eden領域の使用率が高くなると、Young領域に対するGCが行われます。
この時、Eden領域で使用中のオブジェクトはSurvivor領域の一方に移動させられます。移動させられたオブジェクトは、GCを経ることで1歳になります。

Eden領域が空になったので、新しいオブジェクトを割り当てるられるようになりました。

再びEden領域の使用率が高くなると、今度は先ほどとは異なるSurvivor領域に使用中のオブジェクトが移動させられます。この時に、もう一方のSurvivor領域のオブジェクトのうち使用中のものがあれば、同様にもう一方のSurvivor領域に移動させられます。
つまり、Young領域に対するGCの後は、Eden領域と、どちらか一方のSurvivor領域は常に空の状態になります。

このように、一方の領域から他方の領域にオブジェクトを移動させてGCを行うアルゴリズムを、コピーGCと呼びます。
コピーGCはオブジェクトの移動によってコンパクションと同等の効果が得られるため、高速にGCを行うことができます。その一方で、常に一方の領域を空にしておかなくてはならないため、ヒープの使用効率は下がります。
このようにコピーGCをくり返していると、しきい値以上に生き延びているオブジェクトが現れます。このようなオブジェクトはSurvivor領域からTenure領域に移動させられます。
たとえば、しきい値が2だとすると、Survivor領域の2歳のオブジェクトはTenure領域に移動させられます。

そして、Tenure領域の使用率が高くなった場合に、マーク&スイープGCを使用してTenure領域も含めてGCされます。
このように、ヒープを分割して、オブジェクトの世代ごとにその特徴を利用したGCアルゴリズムを適用するのが世代別GCです。Javaで使用されているシリアルGCやG1GCでは、Young領域に対してはコピーGC、Old領域に対してはマーク&スイープGCを使用しています。
シリアルGCとG1GCの両方とも世代別GCを使用していることは分かりましたが、ではこの2つのGCは何が違うのでしょうか?
シリアルGC
JVMでは、アプリケーションを実行するスレッドとGCを行うスレッドは異なります。シリアルGCはGCを行うスレッドが1つだけであるGCです。
GCを行う時には、すべてのアプリケーションスレッドを一時停止します。そして、GCスレッドによりGCを行います。GCが完了したら、一時停止していたアプリケーションスレッドを再開して、再びアプリケーションを動作させます。

アプリケーションスレッドを一時停止して、再開させるまでの時間をSTW (Stop the World)と呼ぶことがあります。文字通りアプリケーションが止まってしまうからですね。
シリアルGCでは、GCスレッドが1つだけのため、どうしてもSTWが長くなってしまいます。
しかし、コアが1つだけという環境であれば、GCスレッドが1つなのはリーズナブルです。シングルコアの場合、たとえGCスレッドが複数あっても、GCスレッド間の切り替えなどが発生してしまって、非効率になってしまうからです。
また、ヒープのサイズが小さければ、GCスレッドが1つでもSTWは短く抑えることができます。
このため、低リソース環境ではシリアルGCがデフォルトGCとして使われてきました。
ちなみに、複数のGCスレッドを使用するのがパラレルGCです。パラレルGCはG1GC以前にデフォルトのGCとして使用されていました。
G1GC
シリアルGCやパラレルGCは基本的にはYoung領域とOld領域は固定です。ヒープが拡張する場合も、Young領域とOld領域の割合は同じです。もちろん、Javaの起動時オプションで割合の変更は可能です。
しかし、アプリケーションによって若いオブジェクトと古いオブジェクトの比率は異なるため、使用比率に応じたGCのチューニングが必要になります。また、同じアプリケーションだとしても、処理によってその比率が変化することも考えられます。
そこで、G1GCはヒープにおける各領域をダイナミックに変更できるようにしました。
このために、G1GCではヒープをある程度の大きさに分割しておきます(これをリージョンと呼びます)。そして、リージョンを使用する時に、ダイナミックにEden/Survivor/Tenureの各領域に割り振っていきます。
Young領域のコピーGCでは、GC後にEden領域とSurvivor領域の一方が空になります。空になったリージョンは、次に使用される時にどの領域として使用するか決まります。
このようにリージョンごとにダイナミックに領域を割り当てることで、ヒープの使用効率を向上させます。
また、STWを短縮するために、GCの処理の一部をアプリケーションと同時に実行することも行われています。
コアが多数ある状況ではアプリケーションスレッドとGCスレッドを同時に実行しても、パフォーマンスに与える影響は少なく抑えることができます。
また、メモリーがふんだんにあれば、ダイナミックなリージョンの割り当ても効率的に動作させることが可能です。
このため、従来はリソースに余裕がある環境ではデフォルトでG1GCが使用されてきました。
JEP 523: Make G1 the Default Garbage Collector in All Environments
さて、JEP 523です。
従来、低リソース環境にはシリアルGCが使用されてきました。これをG1GCに変更するというのがJEP 523です。
G1GCはGCのアルゴリズムが複雑であるため、低リソース環境ではシリアルGCほどのスループットを得ることができませんでした。
しかし、G1GCは着実に改良されてきました。たとえば、Java 24における JEP 475 や、Java 26における JEP 522 などがあります。また、JEPになっていない改良も多くあります。
このため、低リソース環境であってもシリアルGCと同等のスループットを得ることができるようになってきました。
そこで、低リソース環境でもデフォルトGCをG1GCに変更されました。
この変更により影響を受けやすいのが、コンテナーでJavaを動作させている場合です。コンテナーを1コアで、メモリーもそこそこの割り当てしかない場合、シリアルGCが使われているはずです。
GCの種類は、Javaの起動時オプションで指定できます。今までデフォルトのまま使用していたのであれば、シリアルGCを使用するように指定するか、デフォルトのG1GCを使うかの選択が必要です。
次のLTSは2027年9月にリリース予定のJava 29です。Java 29が実際に仕事で使われるようになるまでには、まだ猶予があるはずです。この期間にシリアルGCとG1GCのどちらを使うか判断すればよいのではないでしょうか。
カサレアルでは、Javaを学ぶ方に向けて「Javaプログラミング入門」や「Javaプログラミング基礎」などのコースを開催しています。
Javaに関するコースの詳細や開催日程に関しては以下のリンクをご覧ください。