どうも!@yokotaso です!
2018/05/26のJJUG CCC 2018で「ざっくりわかった気になるモダンGC入門」というタイトルで登壇させていただきました。
現在開発中の新しいGCアルゴリズムをざっくり理解することをテーマに発表しました。
発表練習用に作ったカンペの内容を公開します。ブックマークコメントでもツイートでも感想を書いていただけると喜びます!
発表資料は、speakerdeck にあります。はじまり〜はじまり〜
はじめに
今日はざっくりわかった気になるモダンGC入門というお話をさせていただきます。
現在開発中のGCアルゴリズムの全体像を理解してもらうことを目的としたセッションです。よろしくおねがいします。
さて今日のアジェンダですが、まず簡単にこれまでのGCを復習した後に新しいGCが必要になってきた背景について少し話します。
次にShenandoahGC、ZGC、Epsilon GCと3つ紹介します
これまでのGC
まずは、これまでのGCについて少し
まずはParallel GCです。young/old領域でわかれたメモリ管理をしておりまして、これに合わせてyoung GC/old GCを行います。 この世代を分けるメモリ管理ですが、多くのGCで採用されています。
young/oldのGCそれぞれで、コンパクションGCをします。GCスレッドが動いている間は、アプリケーションスレッドは停止します。
次にコンカレントマークスイープGCです。young領域はコンパクションを伴うGCをします。
Old領域はコンパクションを行わず、アプリケーションスレッドを動かしながらマーキングと呼ばれるゴミオブジェクトの確定作業をします。
ゴミが確定したオブジェクトの領域はメモリは開放されて再利用されます。
マークスイープ型GCの問題点としましては、old領域がフラグメントし、メモリ利用効率が悪くなる問題があります。
そしてG1GCです。メモリを細切れのサイズに分解するリージョン型のメモリ管理をしています。リージョンごとにyoung/oldなどの世代を設定します。
young GCはアプリケーションスレッドを停止してコンパクションGCをします。 old GCも同様にアプリケーションスレッドを停止しますが、目標停止時間を定めてGCをします。
young/old gcはコンパクションGCなので、メモリのフラグメントはしません
3つのGCをまとめて見てみます。
今利用できるGCはコンパクションGC時にアプリケーションスレッドの停止が発生します。これは、young/old GC関係なく発生してしまいます。
逆にコンパクションGCをしないとメモリがフラグメントしていきます。メモリ利用効率が悪くなってしまうわけです。
なぜ新しいGCが必要になっているのか
こういった話を踏まえて、なぜ新しいGCが提案されるようになったのかを見ていきましょう
ひとつは巨大なヒープを利用するアプリケーションの台頭です。
こういったアプリケーションの場合、young領域も大きくなる可能性があるので、コンパクションGCに伴うアプリケーションの停止も無視できなくなってきます。
あわせて複数コアや大量メモリといったハードウェア上の進化もあり新しいGCの必要性がでてきています。
Shenandoah GC
さて、前説を終えたので、Shenandoah GCを見ていきたいと思います
こちらredhat社によって開発されています。 100GBと2GBの停止時間がかわらないという触れ込みです。
なお、利用したい場合はバックポートしてjavaをビルドしなおせば利用できます。
GCの特徴としましては、これまでのGCで利用されていた世代型メモリ管理をしません。 G1GCと同じリージョン型のメモリ管理を採用しています。
並列マーキングは他GCでも行われていますが、このGCの特徴として並列コンパクションを採用している点があります。
アプリケーションと並列で動くコンパクションがなぜ難しいのかを見ていきましょう。
アプリケーションと並列に動作するため、コンパクションGCに伴うオブジェクトの移動とアプリケーションスレッドの参照の更新が同時に起る可能性があります。
GCの実装を工夫しないと、捨てる予定のオブジェクトを参照してしまう危険性があります。
これを解決するために、JVM内部のオブジェクトの中にポインタとは別にBrooks pointerと呼ばれるヘッダをつけて解決しています。
オブジェクトが待避されていなければ、自分自身のポインタをヘッダが指すようになります。この図でいうと右の図です。
オブジェクトが待避済みの場合は、ポインタは待避後のオブジェクトを参照するようにします。この図で言うと左の図です。
このおかげで、待避前・待避後のオブジェクトであっても、ポインタを辿って、待避後のオブジェクトを参照できるようになります。
アイデアのキモは説明できたので、GC全体を眺めてみましょう。
まず、アプリケーションスレッドを停止してルートから辿れるオブジェクトをマークしてきます。
次にアプリケーションと並列でInitial-Markでマークしたオブジェクトをさらに辿っていきます。
アプリケーションスレッドを停止して、前のフェーズから変更があったオブジェクトがゴミオブジェクトになっていないかの最終確定をします。
あとは、ゴミオブジェクトの多いリージョンがマーキングでわかっているので、そのリージョンに対してコンパクションGCをしていきます。
説明したBrooksポインタで古いオブジェクトの参照を新しいオブジェクトのヘッダに付け替えていきます。
このフェーズはアプリケーションが並列でうごいているので参照の更新がおこります。
並列コンパクションをしているときから発生した参照の追加と更新をアプリケーションスレッドを止めて最終確定します。これでGCが完了しました。
さて、GCの全体像を最後に眺めてみます。
コンカレントコンパクションのおかげで、アプリケーションスレッドの停止を減らしつつ、メモリのフラグメントも回避できているのが お分かりいただけたかと思います。
ZGC
さて、続きましてZGCの方に行きたいと思います。
ZGCですが、Oracleで開発されたGCです。数TBのヒープでもGCによる停止時間が多くないという触れ込みです。 こちらもZGCを利用するにはバックポートが必要です。
ちょっとShenandoah GCでお腹いっぱいの方もいらっしゃる方もいらっしゃるかと思いますが…
基本的なコンセプトは、ShenandoahGCと同じなので、安心してください。 世代管理型のメモリ管理をせず、リージョン型のメモリ管理です。そして、並列コンパクションをします。
ZGCの大きな特徴としてLinux 64bit OSでしか今のところ利用できません。Javaの世界観からするとロックな存在ですね。 ZGC特有の特徴ですが、おおきくは3つほどあります。あとから詳しく説明していきます。
カラーポインタ、仮想メモリの有効活用、最後にフォワーディング・テーブルです。
一つずつ見ていきましょう。まずはカラーポインタです。
ShenandoahGCのときもオブジェクトが待避済みかどうかを判定するのがキモでした。
ZGCでは、オブジェクトの状態をカラーとして表現するわけです。 マーク済み0、マーク済み1、オブジェクト待避済みと3つのカラーを持っています。
マークはマーキングフェーズで利用するのですが、0と1の二種類が存在する理由はあとできっちり回収しますのでお待ち下さい。
カラーポインタの色の管理ですが、64bitのメモリアドレスをフル活用します。特定のアドレスのフラグを立てて管理します。
オブジェクトが配置されている位置は下位42ビットを利用しています。そして、4ビットを使ってオブジェクトの状態を管理します
これを64bitのアドレス空間全体をみてみます。最大4TB分のヒープメモリは42ビットで表現されます。
43bitから上位4bitを使って、オブジェクトの状態を表現します。アドレスのカラーポインタのビットが立っていたり立たなかったりします。
64bitのアドレス空間をすべてつかうと128TBのメモリが理論上は必要になってきますがそんなマシンみたことないですね
見せかけの巨大メモリの正体は仮想メモリです。LinuxのOSに詳しくないと、想像しにくいかとおもいます。 最近Linuxのすばらしい入門書に仮想メモリの章があるので、興味がある方は是非購入していただければと思います。
さて、GC全体を眺めていきましょう
アプリケーションスレッドを停止してRootから辿れるオブジェクトをマーキングしていきます。 次にアプリケーションと並列でRootからさらにオブジェクトの参照を辿っていきます。
並列マーキング中に発生したオブジェクトの参照の更新を更新して、ゴミオブジェクトの最終確定をします。
ゴミの割合が多いリージョンがマーキングでわかるので、待避リージョンを決めます。 待避したオブジェクトには待避済みのマークを付けておきます。
待避すると決めたリージョンにはフォワーディングテーブルを作ります。
このフォワーディングテーブルですけれども、Shenandoah GCだとすべてのオブジェクトにヘッダをヘッダを付けているので、オーバーヘッドがあります。
ZGCでは、カラーポインタで待避済みかどうかは判定できるので退避先を参照するフォワーディングテーブルを作り待避後のオブジェクトの参照先を管理します。 要するに、メモリ節約したいんですね。
実はこれでGCは終わりなんですが、参照の更新が終わってません。
GCのサイクルを全体で眺めてみましょう。並列コンパクションまではみました。 参照の付け替えですが、2週目のマーキングフェーズで一緒にやります。図のように、マーキングと参照の付替えがオーバラップします。
そのため、ZGCではConcurrent-Markは厳密には正しくないです。正しくはCuncurrent-Mark/Cuncurrent-Remarkなのです。
参照の付替えフェーズを別途設けずにオーバーラップすることでオブジェクトの参照をたどる回数を節約できるんですね。
ただ、こうすると前のGCサイクルでマーキングされたのか?今のGCサイクルでマーキングされたのか?判断がつかなくなります。 これを見分けるために、マーク用に定義されたカラーポインタが2個(mark0/mark1)存在しています。
Cuncurrent Mark/Remapのフェーズを見てみますと前のGCサイクルとは違う色でマーキングをしていきます。
そうすると、前のGCサイクルでマークされたか、新しく参照されてマークされていないオブジェクトの判断が カラーポインタからできます。その結果をもとに、マーキングし直したり、オブジェクトを待避したりします。
リージョンが空にできました。これでZGCの説明はおしまいです。
Epsilon GC
最後にEpsilon GCです。
アプリケーションスレッドの停止をしないCPUの負荷がほとんどないGCの時間は限りなく0に近いというGCになっております。
そんな夢みたいな話あるのかと思うかもしれません。そんなGCがあるんです。
なぜならこのGCはなにもしないGCだからです。
オチが付いたところで、種を明かしますと
JVM開発目的のGCです。 パフォーマンス測定でGCによる性能劣化を排除するために作られました。ヒープが埋まるとOutOfMemoryErrorが発生します。
本番環境では使わないようにしてください。
まとめ
現在、開発中の巨大ヒープ・多コアが前提のアプリケーション用GCを紹介しました。 運用中のアプリケーションのGCによる停止時間が問題になっている場合、将来的に検討の余地があるかもしれません。
Linux 64 bit OSならZGC。それ以外ならSheandoah GCが利用できます。Epsilon GCは何もしないGCです。
ご清聴ありがとうございました。
最後に
最後まで読んでいただきありがとうございます!ざっくり理解していただけたでしょうか?
実際に生の発表を聞きに来てくださった方もありがとうございました!