[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以上になってしまいます。