Javaの性能
Javaの性能(ジャバのせいのう)では、Javaプラットフォームの性能について説明する。プログラミング言語としてのJavaに対する批判や、Javaプラットフォームの性能に対する批判は「Javaに対する批判」の記事を参照のこと。この記事ではJavaプラットフォームの性能について批判以外の説明をする。
プログラミング言語Javaは、その「ネットワークから送り込まれるプログラムの安全な実行」や「write once, run anywhere」というスローガンを、業界にありがちなスローガンだけのスローガンではなく可能な限り達成するべく、Javaバイトコードにコンパイルするコンパイラと、Javaバイトコードを解釈実行するインタプリタであるJava仮想マシン (Java VM, JVM)、という構成の実装を、公式の実装として伴って発表された。
コンピュータ科学的には特に目新しいものではない。しかし、従来のC言語あるいはC++といったネイティブコードにコンパイルする言語で書かれたアプリケーションソフトウェアとの性能比較や、当初はJVMのチューニングや高速化手法が進んでいなかったことによる性能の制限、また、当時の一般ユーザーが使っていたMicrosoft Internet Explorerにおいて、Javaアプレットが埋込まれたウェブページを表示しようとすると、JVMを起動するために数十秒から最悪の場合は数分も待たされたことから(起動してしまえば実は高性能なVMだったのだが)、「Javaは遅い」などと言われるようになったため、「Javaの性能」が議論されるようになった。
Javaプログラムの実行速度はJITコンパイルの導入(1997年 / 1998年のJava 1.1以降)や
、
コードの解析の機能が言語に追加されたこと、Java仮想マシン自体の最適化(2000年からサン・マイクロシステムズのVMで標準的に動作するようになったHotSpot技術など)によって大きく向上した。
オラクル(旧サン・マイクロシステムズ)のJVMの性能は徐々に向上してきたが、このJVMはいくつかの最適化を実装した最初の仮想機械であることも多く、そうした技術は類似のプラットフォームでも使用されている。
初期のJava仮想マシンはバイトコードのインタプリタであり、このことが性能に対する大きな足かせ(平均的なアプリケーションで、Java対Cで10〜20倍程度)になっていた。
Java 1.1で JIT コンパイラが導入された。
Java 1.2でHotSpotと呼ばれる技術が導入された。これは、Java仮想マシンがプログラムの頻繁に実行される箇所、「ホットスポット」の性能解析を実行中に実行し続けるもので、解析した情報は最適化に利用して、他のパフォーマンスに影響のないコードには余分な負荷をかけることなく、性能を向上させることができる。
Java 1.3でHotSpotが標準で用いられるようになった。
HotSpot技術により、コードはまずインタプリタ実行され、「ホットスポット」が動的にコンパイルされる。Javaの性能測定においてベンチマークをとる前にプログラムを数回実行させる必要があるのはこのためである。
HotSpotによるコンパイルではインライン展開、ループ展開、境界チェックの省略、アーキテクチャ固有のレジスタ割り付けなどの様々な最適化手法が用いられる。
ベンチマークによってはこうした手法により10倍の性能向上が見られる。
適応的最適化 (adaptive optimization) とは、計算機科学においてプログラムの一部を現在の実行結果のプロファイルに基づき動的再コンパイル (dynamic recompilation) する技術である。単純な実装では、適応的最適化はジャストインコンパイルとインタプリタ実行を選択するだけだが、より高水準のものでは、データの局所的な状態を用いて分岐を取り除き、インライン展開してコンテキスト切り替えを削減することもできる。
HotSpotのようなJava仮想マシンは、一度JITコンパイルされたコードを解消 (deoptimization: 脱最適化) することも可能である。これによって、積極的な(場合によっては危険な)最適化を実行し、後で最適化を解消し安全な実行方法に戻ることもできる。
Java 1.0と1.1のJava仮想マシンでは、ガベージコレクション実行後のヒープが断片化する可能性のあるマーク・アンド・スイープ方式のガベージコレクションを採用していた。
Java 1.2より、Java仮想マシンは世代別ガベージコレクションを用いるようになり、断片化が起こりづらくなった。
現代的なJava仮想マシンは、ガベージコレクションの性能を改善する様々な手法を用いている。
クラスの実行に先立ち、オラクルのJVMはバイトコードの検証を行う。バイトコードのロードと検証は特定のクラスがロードされ実行する準備ができた場合のみ行われ、プログラムの開始時点で行われるわけではない。しかし、Javaのクラスライブラリも正規のJavaクラスであり、使用時にロードされるため、Javaプログラムの起動時間は例えばC++のプログラムより長くなることが多い。
分割検証と呼ばれる技術がJavaプラットフォームのJ2MEで導入され、Java version 6からJava仮想マシンで利用されるようになった。これはバイトコードの検証を二つのフェーズに分割する。
設計時 - ソースコードからバイトコードにクラスをコンパイルする際
実行時 - クラスをロードする際
こうした手法は、Javaコンパイラがクラスのコードの流れを解析し、コンパイルされたメソッドのバイトコードにクラスのフロー情報の概要を注釈(アノテート)することで動作する。実行時の検証を劇的に単純化するわけではないが、若干の処理を省略することができる。
Javaは言語レベルでマルチスレッドに対応している。マルチスレッドとは、下記のようなことを可能にする技術である。
並行コンピューティング - 例えばプログラムがバックグラウンドでタスクを実行中であってもユーザーがGUIを操作できるようにすることで、応答性やユーザーに与える印象を改善する
並列コンピューティング - 例えばマルチコアプロセッサのアーキテクチャを活かして、依存関係のない複数の作業を異なるコアで同時に実行し、処理時間を削減する
しかし、マルチスレッドを使用するプログラムは、スレッド間で共有されるオブジェクトやメソッド、コードブロックに開発者が特別な注意を払う必要がある。またオブジェクトやコードブロックをロックすることは、それに伴うOSの性質によって時間のかかる操作である(並行性制御やロックの粒度を参照)。
Javaライブラリにはどのメソッドが複数のスレッドから使用されるか分からないため、マルチスレッド環境で使用される標準的なライブラリは常にコードブロックのロックを行っている。
Java 6以前では、複数の異なるスレッドが同時にオブジェクトを変更するリスクがない場合でも、仮想マシンはオブジェクトやコードブロックのロックをプログラムの要求にしたがって行っていた(ロックの実装を参照)。例えば、ローカル変数のjava/utilがあり、それに対する add 操作を行う際、それが確実にローカルでしか使用されずロックが不要である状況でも、同時に他のスレッドから変更されないようロックを行っていた。
Java 6では、コードブロックやロックは必要なときだけロックされるようになり 、上記の例では仮想マシンはVectorオブジェクトのロックを行わない。
バージョン 6 Update 14で、Javaは実験的ながらエスケープ解析をサポートするようになった。
Java 6以前では、「クライアント」仮想マシンにおけるレジスタ割り付けはかなり初歩的なもので(ブロックを超えてレジスタが生存できない)、これは例えばx86のようなレジスタが少ないアーキテクチャで問題となる。ある操作に必要なレジスタが足りなくなると、コンパイラはレジスタからメモリ(ないしはメモリからレジスタ)に値をコピーするが、メモリは通常レジスタより低速なので、通常より時間がかかる。なお「サーバ」仮想マシンではグラフ彩色によるレジスタ割り付けを行うため、こうした問題は生じない。
レジスタ割り付けの最適化はサンのJDK 6で導入された。これは同じレジスタを、可能な場合コードブロックをまたがって使用することでメモリアクセスを減らすもので、報告によればいくつかのベンチマークで約60%の性能向上が得られた。
オラクルJVMにおけるクラスデータの共有 (class data sharing: CDS) とは、Javaアプリケーションの起動時間を短くし、同時にメモリ使用量を削減する仕組みである。JREがインストールされると、インストーラーはシステムJARファイル(Java のクラスライブラリを全て含むJARファイルで、rt.jarと呼ばれる)からいくつかのクラスを特別な内部表現形式でロードし、この内部表現を「共有書庫」ファイルとして書き出す。それ以降のJVMの呼び出し時には、共有書庫ファイルはメモリマッピングされ、これによってクラスをロードする時間を短縮し、複数 JVM プロセスがこれらのクラスのメタデータを共有できるようになる。
起動時間の短縮は、特に小さなプログラムで効果が著しい。
上記以外にも、オラクルのJavaは各バージョンでJava APIの性能向上を多数盛り込んでいる。
仮想マシンレベルの向上:
最初のJITコンパイル(シマンテックのJITコンパイラによる)
仮想マシンレベルの向上:
仮想マシンレベルの向上:
サン・マイクロシステムズが1.3から1.4での性能向上をまとめたを参照
仮想マシンレベルの向上:
クラスデータの共有参照。Sun がバージョン 1.4 から 5.0 での性能向上をまとめている。
仮想マシンレベルの向上:
その他の向上:
Java OpenGLおよびJava 2Dパイプラインの性能向上
Java 2Dの性能はJava 6で大きく向上した。
Java Quick Starterにより、OS起動時にJRE のデータをディスクキャッシュにロードしておくことでアプリケーションの起動時間を短縮する。
プラットフォームの一部で、アプリケーションを実行する際に必要な部分もWebからダウンロードされるようになった。JRE全体のサイズは12MBになり、典型的なSwingアプリケーションは4MBしか使用しない。残りはバックグラウンドでダウンロードされる。
Direct3Dが標準で使用されるようになり、Windows上でのグラフィック性能が向上した。複雑なJava 2Dの操作を高速化するためGPUのシェーダーを使用するようになった。
今後の性能改善は、Java 6のupdateかJava 7で計画されている。
JVMでの動的プログラミング言語のサポート。Multi Language Virtual Machineのプロトタイプ開発に基づく。
並列性を提供するライブラリの改善。マルチコアプロセッサ上での並列処理を行う
多段コンパイルと呼ばれる手法で、Java仮想マシンがクライアントとサーバという二種類のJITコンパイルの両方を、同じセッションで使用できるようにする。
クライアントのJITコンパイルは、起動時に使用される(起動時と小規模なアプリケーションに適しているため)
サーバーのJITコンパイルは、長時間動作するアプリケーションに使用される(クライアントより性能がよいため)
既存の並列化されたガベージコレクタ (CMS collector; Concurrent Mark-Sweep collector) を、停止時間が一定であることを保証した新しい G1 (Garbage First) コレクタに置き換える。
Javaプログラムは通常仮想マシンによって実行時にJITコンパイルされるが、
C/C++のように事前コンパイルすることも可能である。JITコンパイルされた場合、その性能は一般的に
CやC++などのコンパイル言語と比べて性能は劣る。ただ通常のタスクでは大きな性能の低下はない。
C#などの他のJITコンパイルを行う言語と同様の性能を示す。
性能の良いネイティブコードへのコンパイラ(JITあるいはAOT)を持たないPerl、Ruby、PHP、Pythonなどの言語より、大幅に高い性能を示す。
Javaプログラムの平均的な性能は徐々に向上しており、Javaの性能はCやC++に匹敵するほどになっており、Javaが低速な場合もあるが、高速な場合もある。2009年3月の時点で、Java は においてC/C++より5-15%遅い。ベンチマークは小規模で数値演算中心の性能を測定するとも言われる。これは、おそらくCに有利に働く。実際のプログラムでは、JavaがCを上回ったり性能の差はなかったりする。例として、Jake2(Quake 2クローンで、GPLのCコードをJavaに変換して作成)のベンチマークがあり、Java 5.0のバージョンは、同じハードウェア構成でCの性能を上回る。データがどのように計測されたか明確ではないが(例えば、オリジナルのQuack 2の実行ファイルが1997年にコンパイルされたものであれば、現在のコンパイラではよりよい最適化を行うことができる)、Javaの同じソースコードがVMを更新するだけで大きく性能向上しており、これは100%静的にコンパイルする方法では達成できない。
また、Javaや類似の言語では可能な最適化で、C/C++では実施できないものもある。
C形式のポインタは、ポインタをサポートする言語での最適化を困難にする。
コードがプログラムの実行前にコンパイルされるため、#適応的コンパイルは完全にコンパイルされたコードでは実施できず、アーキテクチャの機能や、コードパスを用いた最適化の恩恵を受けられない。いくつかのベンチマークの結果は、C/C++の性能はプロセッサアーキテクチャに対応したコンパイルオプション(たとえばSSE2の利用など)に強く依存しており、JavaプログラムはJITコンパイルにより対象のアーキテクチャに適応できることを示している。
エスケープ解析の手法は、オブジェクトがどこで使用されるかをコンパイラが知ることができないため、たとえばC++では使用できない(また、ポインタの使用が原因でもある)。
しかし、JavaとC/C++でのベンチマークによる比較は実施する作業に大きく依存する。例えば、Java 5.0と比較すると、
32 / 64ビットの数値演算、ファイル入出力、例外処理 ではC/C++と同等の性能を示す。
コレクション、オブジェクトの生成、解放、メソッド呼び出しの性能 では、Javaの方がC++より性能が高い。
配列 の操作はC/C++の方が高速である。
三角関数 の性能はC/C++の方が高い。
Javaアプリケーションの起動時は、膨大な数のクラス(プラットフォームのクラスライブラリののクラスを含む)を使用前にロードしなければならないため、CやC++よりもかなり時間がかかることが多い。
起動時間の大半はJVMの初期化やクラスのロードそのものではなく、I/Oを伴う操作によるものと2019-09 (rt.jarのクラスデータファイルは40MBあり、JVMは多数のデータをこの巨大なファイルをシークして取り出さなければならない)。いくつかの実験により、バイトコード検証分割の方法を用いるとクラスのロードは約40%向上するが、大規模なプログラムの起動時間は5%しか向上しないことがわかった。
改善幅は小さいものの、単純な操作を実行しすぐ終了するような小さなプログラムでは、Javaプラットフォームのデータローディングはプログラムの操作の数倍の負荷であるため、向上が目に見えやすい。
Java SE 6 Update 10より、オラクルのJREにはQuick Starterが同梱されるようになり、OSの起動時にクラスのデータをロードしておくことで、ディスクではなくディスクキャッシュからデータを読み出すことができるようになる。
Excelsior JETでは、この問題に対して別の方向からアプローチしている。JETのStartup Optimizerはアプリケーションの起動時にディスクから読み出すデータを削減し、さらにシーケンシャルに読み出せるよう配置する。
Javaのメモリ使用量はC/C++より大きい。
32ビット環境のJavaでは各オブジェクトに最低8バイト、各配列に12バイトのオーバーヘッドが存在する(64ビットの環境では倍)。また、オブジェクトのサイズが8の倍数でない場合は、8の倍数に切り上げられる。そのため、4バイトの整数を格納するオブジェクトは32ビット環境で16バイト消費する。ただし、C++も仮想関数テーブルを持つ型は、各オブジェクトに32ビット環境で4バイト、64ビット環境で8バイトのポインタを余分に割り当てる。また、多重継承や仮想継承をするとメンバー関数ポインタのサイズが増大する処理系もある。
クラスライブラリが(最低でもプログラムが使用する分は)実行前にロードされていなければならない。
Javaのバイナリと、ネイティブにJITコンパイルしたものの両方がメモリ上に存在する。
仮想マシン自体がメモリを消費する。
Javaの三角関数の性能は、Cと比べて悪い。Javaが数値演算の結果に(使用するハードウェアとも合致しない場合もある)厳密な仕様を定義しているためである。
x87での絶対値\pi/4以上の値に対するサイン、コサインの演算結果は、\piの値に近似値を用いるため正確ではない。JVMの実装ではソフトウェアで正確な演算を行わなければならず、その領域では大きな性能低下を引き起こす。
Java Native Interfaceはオーバーヘッドが大きく、JVM上で動作するコードとネイティブコードの境界を行き来するコストが高くなっている。
Swingはウィジェットの描画をPure Javaで記述されたJava 2D APIに任せているため、ネイティブのウィジェット・ツールキットと比較して低速であるとされてきた。しかし、SwingとOSのネイティブGUIライブラリに描画処理を任せるStandard Widget Toolkitをベンチマークで比較しても、片方が明確に速いわけではなく、結果はコンテキストや環境に大きく依存した。
いくつかの独立した研究によれば、高性能計算 (HPC) における Javaの性能は、演算中心のベンチマークでFORTRANと同等であるが、JVMはグリッドネットワーク上の通信が多くなると、スケーラビリティに問題があるようである。
しかし、Javaで記述された高性能計算のアプリケーションがベンチマークで最高の成績を出したことがある。2008年、Javaで記述されたHPCのプロジェクト Apache Hadoop が、テラバイト級の整数のソートで最高速の結果を出した。