torutkのブログ

ソフトウェア・エンジニアのブログ

[Java][JavaFX]Javaプログラムを多数動かす場合のチューニング

JavaFXでデスクトップに表示するアナログ時計プログラムを作成しています。
JavaFXとアナログ時計

このプログラムを、Windows 10を入れたノートPC上で多数(試しに24個)起動してみたところ、CPUとメモリがいっぱいいっぱいな状況になってしまいました。

CPU Intel Core i5 4210Y(2コア/4スレッド、1.5GHz)
メモリ 8GB
OS Windows 10 64bit版
JavaVM Oracle Java 8 Update60 64bit版

こちらがCPUの状況です。CPUが常に80%強とすごいことになっています。ノートPCのファンが全開で動いています。このままではバッテリーの消費も早そうです。

こちらがメモリの状況です。搭載メモリの8割が使われています。

こちらは、プロセス一覧から動かしているアナログ時計プログラムについて抜粋したものになります。
一つ一つのプログラムは、CPU使用率が1〜4%、プロセス固有の物理メモリ使用量(プライベートワーキングセット)は100MB前後です。ただし、これが20個のプログラムとなると、相当なCPUとメモリの使用状況をもたらしています。

プログラムは、JARファイルを起動しただけなので、オプションなし(デフォルト設定)で動作しています。

デフォルトで使用するCPUやメモリはどれだけか?

詳細は次に記述したとおりですが、まとめると、64bit JavaVMはデフォルトで

  • プログラム(プロセス)ごとに初期メモリで100MBを確保する
  • プログラム(プロセス)ごとにガベージコレクションの際に搭載CPUをフルに使う

となって、20個のプログラムを起動すると、メモリも2GB、CPUについてはGCの度にがんがん使う状態となってしまいます。

対策については後述します。

書籍「Javaパフォーマンス」の情報

ちょうどJava読書会BOFで現在読み進めている書籍「Javaパフォーマンス」によると、Windows OSで64bit版のJavaVMを動かす場合のCPUとメモリ関係は次のとおりです。

Windows OS 64bit JVMでは、デフォルトのガベージコレクターは、スループット型ガベージコレクターが使用され、ヒープサイズは上述のノートPC(メモリ: 8GB)の場合次のとおりです。

初期サイズ 64MB
最大サイズ 1GB 注)「1GBと物理メモリの1/4のうち少ないほう」

ヒープサイズ以外のメモリ関係の値としては、次があります。

コードキャッシュの初期サイズ 2.4MB
メタスペースの初期サイズ 20.75MB
スレッド毎のスタックサイズ 1MB

スレッド数は、20前後ほど動いています(jcmdでThread.print実行したときの数)。

このことから、64bit版Java SE 8をオプション指定なしで実行した場合、初期メモリ使用量は、64MB + 2.4MB + 20.75MB + (1MB×20) でおおよそ100MBほどになります。(コードキャッシュは除く)

また、スレッド数については、アプリケーションで作るスレッド以外に次のスレッドが動きます。
(CPUが4つの場合)

コンパイラスレッド 3 C1が1つ、C2が2つ
GCスレッド(スループット型) 4 CPU数と同じ
JavaVMの実装値、実測値

確認したい環境において、-XX:+PrintFlagsFinal を指定すると、JavaVMの各種パラメータの値が一覧表示されます。

コードキャッシュの初期サイズ InitialCodeCacheSize 2555904
メタスペースの初期サイズ MetaspcaceSize 21807104
ヒープの初期サイズ InitialHeapSize 134217728

となっています。

プログラムを実行してから、JConsoleで接続して得られた情報によると、最大ヒープサイズが1.8GBとなっています。書籍情報では1GBとなっているので、実装で少し違いがあるようです。

対策

デスクトップアプリケーションなどで、同じ計算機上で複数のJavaプログラムを動かす場合は、リソースの使用が少ない32bit JavaVMを使うとよいです。32bit JavaVMを使うと

  • ポインタサイズが32bitになって同じコードでも使用メモリが64bit JavaVMよりも少ない(OOP圧縮をしてもネイティブコード側は64bit)
  • クライアントコンパイラだけを使用できる
  • デフォルトでシリアル型ガベージコレクタが使われる
  • デフォルトの使用メモリが少ない
    • ヒープサイズ、スタックサイズ、コードキャッシュなど

といったメリットがあります。

32bitと64bitのJavaVMのメモリ、スレッド関係のデフォルト値を整理すると次のようになります。

項目 32bit JVM 64bit JVM 備考
初期ヒープサイズ 16MB 64MB
最大ヒープサイズ 256MB 2GB
初期コードキャッシュ 160KB 2.4MB
メタスペースの初期サイズ 12MB 20.75MB
スレッドスタックサイズ 320KB 1MB
コンパイラスレッド数 1 3 CPU数4つのとき
ガベージコレクションスレッド数 1 4 32bit JVMはシリアル型GC、64bit JVMスループットGC

64bit JavaVMを使うときは、これらの値をJavaVMオプションで明示的に指定することで使用するリソースを抑えることができます。ただし、コンパイラはクライアントコンパイラを指定することができません。

32bit JavaVMを使用した場合でも、デフォルト値より小さい値を明示的に指定することで、よりリソース負荷を減らすことができます。

  • 初期ヒープサイズ、最大ヒープサイズ、メタスペースの初期サイズは、プログラムを動かして実際に使用するメモリを見ながら可能ならより少ないサイズにします。
  • スレッドスタックサイズは、書籍「Javaパフォーマンス」の記述を引用すると128KBでいけます。

一般的には、32ビットJVMで128キロバイト、64ビットJVMで256キロバイトあれば多くのアプリケーションを実行できます。(p.285)

64bit JavaVMを使用せざるを得ない場合は、メモリ関係の指定を32bit JavaVMのデフォルト+αにするとともに、ガベージコレクタをシリアル型にすることでリソース負荷を減らすことができます。

プログラムを実行するマシンに32bit JavaVMがインストールされていない場合を考慮すると、32bit JavaVMをネイティブバンドルしたアプリケーションとして配布するのがよいかもしれません。
Windows OSであれば、ネイティブバンドルとしてMSI形式またはEXE形式のインストーラを作ることができます。ただし、JREを丸ごと抱えるのでインストールサイズが50MB以上になってしまいます。

最後に

世の中のJavaに関するパフォーマンスチューニングの情報は、サーバー向けや、いかに速く動かすか(そのためにはふんだんにCPU、メモリを使う)というものが多いです。
しかし、デスクトップツールなどを動かす際にはそれとはベクトルが違うパフォーマンスチューニングが必要になることが分かりました。今までは、シリアルGCなんか使うことはないと思っていましたが、今回、必要な状況があるのだと実感しました。とくにIntel CPUは、ハイパースレッディングで見かけのCPU数がコア数の倍になりますが、その数に見合うハードウェア性能が出せないので、抑え目にしておくのが必要です。