バツイチとインケンのエンジニアブログ
プログラムやプログラムじゃないこと

JVMチューニング: G1GCの使いどころとCMS GCからのマイグレート

2016-09-14
書いた人 : バツイチ
カテゴリ : jvm | タグ : java

Java7 Update4 (java7u4) で正式サポートされたG1GC(ガベージ・ファーストGC)ですが、Java9ではデフォルトGCになることが確定しています。
参考: JEP248
またG1GCは、CMS GCを長期的に置き換えるものとして計画されています。

そこで、どのようなアルゴリズムなのか知っておいたほうが良さそうなので調査しました。

G1GCが向いているケース

G1GCが向いているのは下記の環境です。

  • ヒープサイズが大きな環境(6GB以上)
  • 一時停止可能時間がシビア(0.5sec未満)

Oracleの 9 ガベージファースト・ガベージ・コレクタによると、CMS GCもしくはParallel GCを使っていて次のいずれかに該当したらG1GCへの切り替えを検討しましょうとのことです。

  • Javaヒープの50%超がライブ・データ(≒必要なデータ)で占められている。
  • オブジェクトの割当て率または昇格率が大きく変化する。
  • ガベージ・コレクションまたは圧縮によるアプリケーションの一時停止の長さが望ましくない(0.5から1秒を超える)。

私はヒープの50%以上がライブ・データで占められており、一時停止可能時間がシビアなアプリケーションをCMS GCで扱っているので、この謳い文句には大変惹かれました。

G1GCの特徴

それでは、G1GCはどのようなアルゴリズムなのでしょうか?
G1GCの特徴は大まかには次の3点になります。

  • ヒープを不連続で細かいリージョンという単位で管理する
  • 目標停止時間を守って収集を行う
  • ガベージ率が高そうなヒープに対して収集・圧縮を集中する

ヒープは均等サイズのリージョン・セットに分割され、若い世代も古い世代もこのリージョンで管理されます。

G1によるヒープの分割
G1によるヒープの分割 より

 

G1GCは、ヒープ全体のオブジェクトがライブかどうかを判断する、同時グローバル・マーキング・フェーズを実行し、どのリージョンにガベージ(≒ゴミ)が多いかを認識します。
ガベージでいっぱいとなっている可能性が高いヒープ領域に対して、収集・圧縮を集中するので G1 = ガベージ・ファースト と呼ばれています。
このときG1GCは一時停止予測モデルを使って、指定された一時停止時間目標に基づいて収集するリージョン数を選択します。
この仕組みによりマルチ・プロセッサ環境下での低レイテンシ・高スループットを実現しています。

STW stop-the-world

万能にみえるG1GCにもSTW(stop-the-world)があります。
まず収集の最後に部分的なSTW、コンカレントなクリーンアップ・フェーズがあります。

またCMS GCと同様に、あるリージョンから別リージョンへのオブジェクト・コピー中にアプリケーションが新たなオブジェクトを割り当てた場合、ヒープの枯渇(割当の失敗)が発生します。
これはSTWフル・コレクションを引き起こします。

CMS GCからG1GCへのマイグレート

G1GCの特性が、私の扱っている環境に適していそうだったので、CMS GCからマイグレートしてみることにしました。
マイグレートの際に大変参考にさせて頂いたのがこちらのスライドです。

Garbage First Garbage Collector (G1 GC) – Migration to, Expectations and Advanced Tuning from Monica Beckwith

まずは現状の把握から。
変更前はCMS GCを使っていました。
対象となるアプリケーションは秒間100〜200程度のリクエストを捌いています。

サーバのスペックは

CPU: 8個
メモリ: 26G

です。

javaのバージョンは下記のとおり。

java version "1.8.0_31"
Java(TM) SE Runtime Environment (build 1.8.0_31-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.31-b07, mixed mode)

CMS GC時の起動オプション

${JAVA_HOME}/bin/java \
  -Xmx13703M \
  -Xms13703M \
  -Xloggc:gc.log \
  -XX:NewRatio=1 \
  -XX:SurvivorRatio=2 \
  -XX:TargetSurvivorRatio=90 \
  -XX:MaxTenuringThreshold=15 \
  -XX:+PrintGCDetails \
  -XX:+PrintGCDateStamps \
  -XX:+UseConcMarkSweepGC \
  -Dcom.sun.management.jmxremote \
  -Dcom.sun.management.jmxremote.port=7900 \
  -Dcom.sun.management.jmxremote.authenticate=false \
  -Dcom.sun.management.jmxremote.ssl=false \
  -Djava.rmi.server.hostname=`hostname -f` \
  -classpath ${CLASS_PATH} \
  ${MAIN_CLASS} \
  $@

VisualVMで見たCMS GCのヒープ使用率

jvm_cms3_2

細かくマイナーGCを行いつつも、徐々に使用ヒープが増えていき、閾値を超えるとメジャーGCでクリーンアップされている様子がわかります。

マイグレート

上記のスライドp48によると、G1GCはCMS GCよりもCPUを使うとのこと。
停止時間の予測している側面からだけ見てもオーバーヘッドが大きいのは納得。

さて、起動オプションですが、G1GCは目標停止時間を守るアルゴリズムなので、まずは -XX:MaxGCPauseMillis をどのくらいにするかを考えるべきでしょう。
このオプションのデフォルト値は200msです。
これは人間の知覚の限界と言われているので、まあ人間に対してレスポンスを返すようなアプリケーションであれば200msで良い気がしますが。

それからもちろん -XX:+UseConcMarkSweepGC を-XX:+UseG1GC にする必要があります。

その他上記のスライドp52を参照すると、 -XX:SurvivorRatio, -XX:TargetSurvivorRatio, -XX:MaxTenuringThreshold は消してしまえと。
各領域をどのくらいの比率で使うかは G1GC に任せたほうが良いということですね。

あとは マーキング・サイクルを開始するJavaヒープ占有率のしきい値を設定する -XX:InitiatingHeapOccupancyPercent と 適切な世代サイズを出力する -XX:PrintAdaptiveSizePolicyを設定しました。

G1GC時の起動オプション

${JAVA_HOME}/bin/java \
  -Xmx13703M \
  -Xms13703M \
  -Xloggc:/var/log/ultima/gc.log \
  -XX:+PrintGCDetails \
  -XX:+PrintGCDateStamps \
  -XX:+PrintAdaptiveSizePolicy \
  -XX:+UseG1GC \
  -XX:MaxGCPauseMillis=200 \
  -XX:InitiatingHeapOccupancyPercent=50 \
  -Dcom.sun.management.jmxremote \
  -Dcom.sun.management.jmxremote.port=7900 \
  -Dcom.sun.management.jmxremote.authenticate=false \
  -Dcom.sun.management.jmxremote.ssl=false \
  -Djava.rmi.server.hostname=`hostname -f` \
  -classpath ${CLASS_PATH} \
  ${MAIN_CLASS} \
  $@

VisualVMで見たG1GCのヒープ使用率

変更後、VisualVMでヒープ使用率を見てみました。

jvm_g1gc3

たまにCPUの使用率がスパイクしますが、おそらくこのタイミングでグローバル・マーキング・フェーズが実行されているのでしょう。
ヒープの使用量は安定しています。
つまり効率的にヒープ領域をライブ・データに割り当てているといって良さそうです。
ただ、別の監視ツールで見るとCPU使用率はやはりCMS GCに比べて増えていました。

まとめ

G1GCよりCMS GCの方が早い、というコメントをたびたびネット上で拝見しますが、環境及びアプリケーション特性によって違う、というのが私の見解です。
※ 事実G1GCの方がCPUを使うのは確かなので、CPUリソースが逼迫していたらG1GCの利用は難しいでしょう。

Oracleが書いている通り、ヒープサイズが大きく目標停止時間がシビアな環境がG1GCに向いているのは間違いないと思います。
GCアルゴリズムも適材・適所ですね。

このエントリーをはてなブックマークに追加
Tweet

← Tableauタブロー で競馬の格言を検証する part1
ジョブ・スケジューラ Rundeck で高機能 cron を実現する →

 

最近書いた記事

  • ぽちっとジャンキー
  • ホテルでリモートワーク・テレワーク
  • Ryzen7 3800XT でmini ITXオープンフレームPCを作る
  • Pythonで機械学習入門 競馬予測
  • HP ENVY 15 クリエイターモデルレビューとRAID0解除

カテゴリー

  • Android
  • Apache Flink
  • API
  • AWS
  • bazel
  • BigQuery
  • Cassandra
  • Docker
  • Druid
  • Elasticsearch
  • Git
  • Golang
  • gradle
  • HDFS
  • JavaScript
  • jvm
  • Linux
  • MongoDB
  • MySQL
  • Nginx
  • Nodejs
  • PaaS
  • PHP
  • Python
  • RabbitMQ
  • Raspberry Pi
  • React Native
  • Redis
  • Riak
  • rust
  • scala
  • Scheme
  • SEO
  • solr
  • Spark
  • spray
  • Sublime Text
  • Swift
  • Tableau
  • Unity
  • WebIDE
  • Wordpress
  • Youtube
  • ひとこと
  • カンファレンス
  • スケジューラ
  • マイクロマウス
  • リモートワーク
  • 広告
  • 技術じゃないやつ
  • 株
  • 機械学習
  • 競馬
  • 自作キーボード
  • 自然言語処理

アーカイブ

  • 2022年2月
  • 2022年1月
  • 2021年4月
  • 2021年2月
  • 2021年1月
  • 2020年3月
  • 2020年2月
  • 2020年1月
  • 2019年10月
  • 2019年9月
  • 2019年8月
  • 2019年7月
  • 2019年6月
  • 2019年5月
  • 2019年4月
  • 2019年2月
  • 2019年1月
  • 2018年12月
  • 2018年11月
  • 2018年9月
  • 2018年5月
  • 2018年3月
  • 2018年2月
  • 2017年9月
  • 2017年8月
  • 2017年6月
  • 2017年4月
  • 2017年3月
  • 2017年1月
  • 2016年10月
  • 2016年9月
  • 2016年8月
  • 2016年6月
  • 2016年5月
  • 2016年4月
  • 2016年3月
  • 2016年2月
  • 2016年1月
  • 2015年12月
  • 2015年11月
  • 2015年10月
  • 2015年9月
  • 2015年8月
  • 2015年6月
  • 2015年5月
  • 2015年2月
  • 2015年1月
  • 2014年12月
  • 2014年11月
  • 2014年9月
  • 2014年6月
  • 2014年5月
  • 2014年3月
  • 2014年2月
  • 2014年1月
  • 2013年12月
  • 2013年11月
  • 2013年10月
  • 2013年9月
  • 2013年8月

書いた人

  • バツイチちゃん
  • インケンくん

このブログについて

エンジニアとしての考え方が間逆な2人がしょーもないこと書いてます。

バツイチ

アイコン

IT業界で働くエンジニアです。名前の通りバツイチです。
理論や抽象的概念が好きだけど人に説明するのが下手。

インケン

アイコン

バツイチちゃんと同じ業界で働いています。
理論とか開発手法とかは正直どうでもよくて、
生活する上で役に立つことに使いたい

Copyright 2023 バツイチとインケンのエンジニアブログ