unnamed

Javaとか、http://twitter.com/sugarlife

「Java パフォーマンス」感想

本書の翻訳者の一人である@cero_tより献本頂きました、ありがとうございます。というわけで一週間かけて読んでみた。

www.amazon.co.jp

今現在 Java で開発している人、特に運用者や試験者は間違いなく買っておくべき本です。Javaに限らない一般的なパフォーマンスチューニングの考え方・観点から、Java アプリケーションにおいてボトルネックになりやすい GCJIT の詳細な確認方法からチューニング方法が解説されている。特にすごいのが Java の世界のみならず、OS の世界まで触れている点。流石に OS の世界はここに書かれているのが全てではないけれど、Java アプリに関わる部分で問題になりやすい点は割と触れている。

JDK8 にも対応しており、今現在手に入る情報としては一番頼もしいと思う。4000 円程度でこの知識量が手に入るなら非常に安い。

お勧めの読み方

個人的にお勧めの読み方はパフォーマンスの観点やツールの使い方が書かれている 1-3 章は誰でも先ず読んでおき (3.4 は HeapStats でも良いよ!)、使っている GC アルゴリズムに合わせて GC について書かれている 5-6 章を読むのがお勧めです。GC ログの読み方から解説されているので、今現在で問題が出てないかの確認も行えるのが便利。CMS GC に関しては拙作のスライドもあわせてどうぞ。

Java で今まさにコーディングしている人は 9,12 章でコーディングにおける効率的な書き方を学びつつ、メモリ使用状況をどう確認するべきか 7-8 章 で学ぶのが良さそう。これらの章以外もちょくちょく読み進めておき Java の世界(JITとか)と Java を取り囲む世界でどのように確認すべきか少しずつ観点を増やしていけます。

この本の凄いところ

この本でサポートできないのは、よりコアな一般的でないチューニング(大抵稼働がかかり過ぎるのでやるべきではない)や、JVM の実装に依存する内容(バグとか)、JNI や sun.misc を利用した黒魔術コーディング、よりハードウェアを意識したコーディング/チューニング等なので、基本的にこの本に書かれている内容を実践するだけで大方の運用面における問題は解決するか、解決の糸口が掴めると思います。

コアな一般的でないチューニングとは言うけれど、この書籍のように Large Page や TLB キャッシュ率、オブジェクトのロックコストなどに一冊で言及した書籍なんてそうそうありません。特に GC でどういった状況が発生したら問題で、その解決方法(チューニング)を割と網羅しており、各種ランタイム情報の確認方法まで書かれているので、この本で解決しないなら結構な経験者や OpenJDK に詳しい人を引っ張りださないと辛いと思う。

ちなみに CMS GC の注意すべきケースとして promotion failed というのがありますが、これは「昇格の失敗」という日本語になっていました。訳としては正しいのですがログには promotion failed と出ますが、その単語からは索引できないので注意が必要です。

おわりに

日本語でこのレベルを読めるようになったのは便利だなあ。数年前からJavaメモリモデルを意識したコーディングや、ハードウェアを意識したチューニングが JavaOne とかで発表されてましたが、より多くの所がその水準に行くための土台ができてき始めてるかもしれませんね。

(´-`).oO(新人教育や知識継承、この本読めば済みそう)

Crypt 200: Decrypt it(Easy)

ctpm として SECCON 2014 オンライン予選(en) に出ました。25位でした。

上記事の Decrypt it(Easy) を途中から一緒にやりつつ後を引き継いだのに、アホなミス('A')で取りこぼしたので、終了後に解き直したスクリプトを覚え書きとして残す。

なお、「Sage - Open-Source Mathematical Software System」を使っている。

#! /usr/bin/python
# -*- encoding: utf-8 -*-

from sage.all import *

def decode(target):
  if len(str(target)) % 2 == 0:
    print str(hex(target)).decode('hex')

for n,b,c in ([0xb8ae199365,0xffeee,0x8d5051562b],
    [0xb86e78c811, 0xfffee, 0x5ffa0ac1a2], [0x7bd4071e55, 0xfefef, 0x6008ddf867]):
  p,q = map( lambda x : x[0], factor(n) )

  for x in range(p):
    if (x*x + b*x - c) % p == 0:
      x_p = x
      #break
  for x in range(q):
    if (x*x + b*x - c) % q == 0:
      x_q = x
      #break

  decode( crt(x_p, x_q, p,q) )
  decode( crt(p-b-x_p, x_q, p,q) )
  decode( crt(p-b-x_p, q-b-x_q, p,q) )
  decode( crt(x_p, q-b-x_q, p,q) )
  print "-" * 16

実行結果は以下の通り

# sage -python solver.py
SECCO
aNウq
    eh(
W_V"
----------------
2_5
N{Ra_
V
if
  ----------------
b1_N}
U4

%2
----------------

Rabin暗号単射じゃないので一意ではない。意味ある文字は SECCON{Ra_b1_N} ですね。

コアダンプから Java 起動オプションの抜き出し方

この記事は JVM Advent Calendar 2日目 の記事です。

JVM を触っているとコアを吐いて落ちる事がしばしばあります。ええ、しばしばあります。
こういう時にどのようなオプションで起動していたかをうっかり忘れると後々面倒です。
でも、実は簡単に解ります。

簡単なやり方

# strings core.12672 | grep -- -Xmx
-XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=80 -XX:+UseCMSInitiatingOccupancyOnly -Xmx1500m -Xms1500m -Xloggc:gc.log -XX:+PrintGCDetails -XX:+CMSClassUnloadingEnabled

-Xmxを指定していないときは -XX: とかで grep すると引っ掛かります。

JVM に興味があってちゃんと取りたい人向け

Arguments::_jvm_args_arrayJava 起動オプションを格納した配列で、Arguments::_num_jvm_args がこの配列長(Java 起動オプションの数)です。この静的メンバ変数らはコアダンプが出力された時も存在しています。
なので、gdb で、この配列から要素を引っこ抜いてやれば全 Java 起動オプションが取れます。

たとえば、コアを gdb で起動して以下のコードを実行させれば全 Java 起動オプションが表示されます。

set $num = Arguments::_num_jvm_args
set $i = 0

while($i < $num)
  x/a Arguments::_jvm_args_array + $i
  x/s $__
  set $i++
end

※:バージョンによってはシンボル名が違ったりマングリングされてたりで、Arguments::_num_jvm_args が _ZN9Arguments13_num_jvm_argsE になってたりする場合もあります。

実証 (上のコードを test.gdb として保存しておく)

# java -Xmx100M -Xms100M -XX:+UseConcMarkSweepGC -Xloggc:gc.log -XX:+PrintGCDetails -XX:+CMSClassUnloadingEnabled Test &
[1] 12672
# gcore -o ./core 12672()
Saved corefile ./core.12672
# gdb /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.25.x86_64/jre/bin/java-abrt core.12672 < test.gdb()
gdb-peda$  > > > >
0x7fbd80003120: 0x7fbd80003050
0x7fbd80003050: "-Xmx100M"
0x7fbd80003128: 0x7fbd80003070
0x7fbd80003070: "-Xms100M"
0x7fbd80003130: 0x7fbd80003090
0x7fbd80003090: "-XX:+UseConcMarkSweepGC"
0x7fbd80003138: 0x7fbd80003030
0x7fbd80003030: "-Xloggc:gc.log"
0x7fbd80003140: 0x7fbd80003100
0x7fbd80003100: "-XX:+PrintGCDetails"
0x7fbd80003148: 0x7fbd800030b0
0x7fbd800030b0: "-XX:+CMSClassUnloadingEnabled"
gdb-peda$ quit

おわりに

strings の方が楽でしたね!僕は gdb スクリプト書いてから気付きましたばーかばーか!(つД`)・゚・

CMS GC おさらい

この記事は Java Advent Calendar 2014 の一日目の記事です。

先日の JJUG CCC 2014 FallCMS GC について話してきました。
結構遅めの時間帯にも関わらず、200人規模の部屋がいっぱいに埋まるぐらいの盛況振りで、みなさんGCにお困りなんだなあと実感しました。スライドは以下に公開しています。CMS GC の挙動から GC ログの読み方、どういうケースが厄介なのかを紹介しているので是非ご覧ください!

嬉しいことにセッションの反応は良かったのですが、「遅めの時間帯で頭も疲れてるとガチ話辛い」という声もあったので、今回は CMS GC について比較的重要な点についてだけ簡単におさらいしたいと思います。

オプションについて

@nekop さんの「Java 7 CMS GCの基本的な情報の整理 - nekop's blog」を読めば OK 。

上の記事から追記するとしたら

  • -Xmx と -Xms は同じ値にしたほうが、拡張/シュリンク時に Major GC などの負荷が発生しないのでお勧め。
    • 積極的にシュリンクさせたいなら MaxHeapFreeRatio も変更しないと多くの場合では期待している動作にならないので注意。
    • ちなみに JDK8 以前はこのバグで実はシュリンクできていなかったケースとかもある。
  • -XX:+CMSClassUnloadingEnabled は JDK8 でデフォルト True なので、もう True で良いと思う。
    • JDK 8 で False にしてると、(Metaspaceを使いまくるアプリなら) Metaspace が際限なく増えて最終的に JVM 落ちるんじゃないかな。
    • 有効にすることで増える Remark フェーズの GC ポーズの秒数は下のスライドのログ見れば解る。

他、CMS GC の発生条件のチューニングとして、以下のスライドで CMSInitiatingOccupancyFraction と UseCMSInitiatingOccupancyOnly を紹介しました。

UseCMSInitatingOccupancyOnly を使わずに、チューニングすることも可能ですが、自分が知っている限りでこれが無効の場合の CMS GC 開始条件は、

  • CMS 実行間隔の統計的に、今実行しないとヒープが不足すると判断された場合
  • 最初の CMS 実行時に、ヒープ使用率が CMSBootstrapOccupancy で指定した値を超えていた場合
  • ヒープ領域拡張した結果、GC を行わなくてもメモリ確保に成功した場合
  • 断片化によってまとまったメモリ空間の確保に失敗すると判断された場合

と、これ以外(インクリメンタル CMS やパーマネント領域関係とか)にもあるのでここら辺を一つ一つやるよりは UseCMSInitiatingOccupancyOnly で調整したほうが楽です。

GC ログの読み方について

GC ログの読み方はスライドの P.37 以降に一つ一つ書いているのでそちらで!
特に重要そうな点は以下の通りです。

  • CMS GC と Full GC は別物
    • CMS GC は Initial mark ([1 CMS-initial-mark) と Remark ([1 CMS-remark) フェーズでアプリケーションが止まる。それ以外の時間はアプリは動く。
    • Full GC は Mark Sweep Compaction アルゴリズムであり、実行時はアプリケーションが止まる。
      • promotion failed か concurrent mode failure が出力された時にFullGCが実行される。[Full GC が出力されない場合もある。
      • Full GC対策はスライド参照(P.71から)。
  • CMS: abort preclean due to time」はエラーじゃない。
  • GC locker: Trying a full collection because scavenge failed」が出たら CMS GC は諦めて コンパクションのある GC を使うのがお勧め。

おわりに

CMS GC ではパフォーマンス的にどうにもならない時もあります。どうにもならないことが掴めたら G1GC や Parallel GC、Azul Zingを使うことを検討しましょう。

これ以外にも OpenJDK にはまた新しい GC として G1GC リプレース型の Pauseless GC である Shenandoah がやってきます。JavaOne2014 でも発表していましたが、資料はこっちの方(「Shenandoah An ultra-low pause time Garbage Collector for OpenJDK」)が解りやすいかな。Remembered set絡みの処理が早くなるアイデアで、G1GC より停止時間が優れてる感じっぽいと受け取りましたが、出てくるのはしばらく後ですね。こんな感じで GC との付き合いはしばらく続きそうです。

Java 起動(Launcher)の仕組み

※:この記事は下書き中に本文ががっつり消えたため、知らずに部分的に端折ってるところがあるかもしれません。(´;ω;`)

Java、すなわち JVM (HotSpot) を立ち上げた時、どういった処理が行われているのでしょうか。正確に知りたい場合は OpenJDK のソースコードを読むのが最も確実ですが、概要レベルでどのような処理が行われていて、それがソースコードのどのあたりに書かれているのか案内があった方がすんなりと理解できます。と言うわけで、自分用のメモ書きをちょっとだけ整理してここで公開してみます。

なお、自分の理解をベースに記述しているので間違いが含まれている可能性があります。見つけた場合はそっとコメントか @sugarlife にお教え頂けると大変喜びます。

Java の動作概要について

Java、特に HotSpot の動作概要については、OpenJDK コミュニティによって「HotSpot Runtime Overview」という記事で解説されています。Java の起動時の処理についても「VM Lifecycle」という項目で説明されています。動作の概要を知りたい場合はこれらの記事を読むだけで理解できます。

和訳については有志らが取り組んでいる状況です。Java の動作に興味がある人は是非和訳をしてみませんか:)

この活動を見て、自分もこのメモ書きをちょっと手直しして公開してみようと思った次第です。

Java の起動 (Launcher)

HotSpot Runtime Overview で書かれている起動時における処理内容としては Launcher と JNI_CreateJavaVM が書かれています。Launcher というのは、ユーザが利用する java コマンド等のコマンド群 (java, javac, etc... ) 、およびそこから呼ばれるライブラリ (JLI、libjli) 部分を指します。「Launcher」とだけ書かれていた場合は、java コマンド等のコマンド群を指すことが多いです。
JNI_CreateJavaVM は The invocation API と呼ばれる API の一つで、JVM(HotSpot) そのものをロード & 初期化するメソッドです。現在の実装では一つのプロセスに複数VM は作成不可能なため、一度のみ実行されます。

ここでは主に Launcher 部分について触れて行きます。なお、ソースコードへのリンクは jdk8 の tip(最新コミット) にリンクしています。jdk8 の最新版ではないことに注意してください。最新版は jdk8 の部分を jdk8u に変えることで確認できます。

  • java コマンドを叩く
  • main() / WinMain() [jdk/src/share/bin/main.c]
  • JLI_Launch() [jdk/src/share/bin/java.c]
    • LoadJavaVM() より libjvm をロード。Linux の場合は libjvm.so ファイルをダイナミックロード。
    • HotSpot Runtime Overview に記載されている Launcher の 1. ~ 4. 部分が実行される。
    1. コマンドライン引数の解析。適切な VM を起動するために -client とか -server はランチャ内で利用され、それ以外の引数は JavaVMInitArgs を通じて VM に渡される。
    2. ヒープサイズやコンパイラ種別(client or server)が設定されていない場合、適当な値を設定する。
    3. LD_LIBRARY_PATHやCLASSPATH等の環境変数を設定する。
    4. メインクラスが指定されて居ない場合、jar ファイルのマニフェストファイルからメインクラスを確認する。
  • ContinueInNewThread() [jdk/src/share/bin/java.c]
  • ContinueInNewThread0() [OSによって異なる]
    • non primordial thread を作成、実行する。non primordial thread のエントリポイントは JavaMain()
  • JavaMain() [jdk/src/share/bin/java.c]
    • HotSpot Runtime Overview に記載されている 5. 以降の処理が行われる。
    1. (ContinueInNewThread0() で non primordial thread を作成し、)JNI_CreateJavaVM()を呼んで VM を作成・初期化する。新しいスレッド(non primordial thread)を作成しているのは、この処理自体を行っているスレッド(primordial thread)が、環境によっては色々な制限があり、VMの作成上で制約(Windows上でのスタックサイズなどによる制約など)があるため。
    2. メインクラスを読み込んで、メインクラスからメインメソッドの属性を取得する。
      • LoadMainClass() 以降の処理。メインメソッドを取得しているのは GetStaticMethodID()。
    3. CallStaticVoidMethod() とコマンドラインに渡された引数を使用してメインメソッドを呼び出す。
    4. メインメソッドが完了したら、待機中の例外 (pending exception) がないか確認し、exit status が渡されてないことを確認する。例外は ExceptionOccurred() でクリアされる。メソッドが成功していれば戻り値は 0 に、そうでなければ呼び出し元のプロセスに返却される。
    5. メインスレッドは DetachCurrentThread() によりデタッチされる。これによりスレッドカウントをデクリメントし、DestroyJavaVM() を安全に呼び出し、且つこのスレッドが VM 内の操作やスタック上に Java フレームが存在しないことを保証できる。
      • JavaMain()は最終的に LEAVE() マクロを呼び出し、DetachCurrentThread()、DestroyJavaVM()を呼んでいる。

以上、Launcher 編でした。

JNI_CreateJavaVM() について。

ここは真面目にやると深いので、

まとめ

OpenJDKこわい。

JJUG CCC 2014 Fall で Concurrent Mark & Sweep Garbage Collection について話します。良かったらマサカリを置いて来てね!