Java のパフォーマンスは、ある種の黒魔術として知られています。その理由の 1 つは、Java プラットフォームが非常に複雑であり、多くの場合、問題を特定するのが難しいことです。ただし、応用統計や経験的推論に頼るのではなく、知恵と経験に基づいて Java のパフォーマンスを研究するという歴史的な傾向もあります。この記事では、最もばかばかしいテクノロジー通説のいくつかが誤りであることを暴きたいと思っています。
1. Java は遅い
Java のパフォーマンスについては多くの誤解がありますが、これは最も時代遅れであり、おそらく最も明らかです。
1990 年代から 2000 年代初頭にかけて、Java が時々遅くなったのは事実です。
しかし、それ以来 10 年以上にわたって仮想マシンと JIT テクノロジが向上し、Java の全体的なパフォーマンスは現在非常に優れています。
6 つの独立した Web パフォーマンス ベンチマークにおいて、Java フレームワークは 24 テスト中 22 でトップ 4 にランクされました。
JVM はパフォーマンス プロファイリングを使用して、一般的に使用されるコード パスのみを最適化しますが、最適化の効果は明らかです。多くの場合、JIT コンパイルされた Java コードは C++ と同じくらい高速であり、そのような状況はますます増えています。
それにもかかわらず、Java プラットフォームは遅いと考える人がいます。これは、Java プラットフォームの初期バージョンを経験した人々の歴史的な偏見に由来している可能性があります。
結論を出す前に、客観性を保ち、最新のパフォーマンス結果を評価することをお勧めします。
2. Java コードの 1 行を単独で表示できます
次の短いコード行を考えてみましょう:
MyObject obj = new MyObject();
Java 開発者にとって、このコード行が必ずオブジェクトを割り当て、適切なコンストラクターを呼び出します。
これに基づいてパフォーマンスの境界を導き出すことができるかもしれません。このコード行によって確実に一定量の作業が実行されると想定し、この推定に基づいてパフォーマンスへの影響を計算してみます。
実は、この理解は間違っています。それは、どんな仕事であっても、どんな状況下でも実行されるという先入観を与えます。
実際、javac コンパイラーと JIT コンパイラーは両方とも、デッドコードを最適化して取り除くことができます。 JIT コンパイラーに関する限り、プロファイリング データに基づいて、予測を通じてコードを最適化することもできます。この場合、このコード行はまったく実行されないため、パフォーマンスには影響しません。
さらに、JRockit などの一部の JVM では、JIT コンパイラがオブジェクトの操作を分解することもできるため、コード パスがまだ有効な場合でも割り当て操作を回避できます。
ここでの教訓は、Java のパフォーマンスの問題を扱うときはコンテキストが非常に重要であり、時期尚早な最適化は直感に反する結果を生み出す可能性があるということです。したがって、時期尚早に最適化しないことが最善です。代わりに、常にコードを構築し、パフォーマンス チューニング手法を使用してパフォーマンスのホット スポットを特定し、改善します。
3. マイクロベンチマークはご想像どおりです
上で見たように、コードの小さな部分を調べることは、アプリケーションの全体的なパフォーマンスを分析するほど正確ではありません。
にもかかわらず、開発者はマイクロベンチマークを書くのが大好きです。基盤となるプラットフォームの一部をいじるのはとても楽しいようです。
Richard Feynman はかつてこう言いました。「自分を騙さないでください。最も騙されやすいのはあなたです。」 この文は、Java マイクロベンチマークの作成を説明するのに最適です。
適切なマイクロベンチマークを作成することは非常に困難です。 Java プラットフォームは非常に複雑であり、多くのマイクロベンチマークは、Java プラットフォームの一時的な影響やその他の予期しない側面しか測定できません。
たとえば、経験がない場合、作成するマイクロベンチマークは時間やガベージ コレクションを測定するだけで、実際の影響要因を捉えていないことがよくあります。
実際のニーズがある開発者と開発チームのみがマイクロベンチマークを作成する必要があります。これらのベンチマークは完全に公開され (ソース コードを含む)、再現可能であり、ピアレビューとさらなる精査の対象となる必要があります。
Java プラットフォームの多くの最適化では、統計的な実行と単一の実行が結果に大きな影響を与えることが示されています。正確で信頼できる答えを得るには、単一のベンチマークを複数回実行し、結果を集計する必要があります。
読者がマイクロベンチマークを作成する必要性を感じている場合は、Georges、Buytaert、および Eeckhout による論文「統計的に厳密な Java パフォーマンス評価」が良いスタートとなります。適切な統計分析がなければ、私たちは簡単に誤解されてしまいます。
その周囲には、よく開発されたツールとコミュニティがたくさんあります (Google の Caliper など)。本当にマイクロベンチマークを作成する必要がある場合は、自分で作成しないでください。必要なのは同僚の意見や経験です。
4. パフォーマンスの問題の最も一般的な原因はアルゴリズムの遅さです
開発者 (および一般の人々) の間には、自分が制御するシステムの一部が重要であると考えるという、非常に一般的な認知エラーがあります。
この認識上の誤りは、Java のパフォーマンスについて議論するときにも反映されます。Java 開発者は、アルゴリズムの品質がパフォーマンスの問題の主な原因であると信じています。開発者はコードについて考えるので、自然と独自のアルゴリズムについて考える傾向があります。
実際、一連の現実のパフォーマンス問題に対処するときに、アルゴリズムの設計が根本的な問題であると人々が気づく確率は 10% 未満です。
逆に、ガベージ コレクション、データベース アクセス、構成エラーは、アルゴリズムよりもアプリケーションの速度低下の原因となる可能性が高くなります。
ほとんどのアプリケーションは比較的少量のデータを処理するため、メインのアルゴリズムが効率的でなくても、通常は深刻なパフォーマンスの問題を引き起こしません。確かに、私たちのアルゴリズムは最適ではありませんが、それでも、このアルゴリズムによって引き起こされるパフォーマンスの問題は比較的小さく、アプリケーション スタックの他の部分によってさらに多くのパフォーマンスの問題が発生します。
したがって、私たちの最善のアドバイスは、実際の運用データを使用して、パフォーマンスの問題の本当の原因を明らかにすることです。パフォーマンス データを測定します。推測しないでください。
5. キャッシュはすべての問題を解決できる
「コンピューターサイエンスにおけるすべての問題は、中間層を導入することで解決できる。」
デビッド・ウィーラーのプログラマーのモットー (インターネット上では、少なくとも他の 2 台のコンピューターによるものと考えられています)科学者)は、特に Web 開発者の間で非常に一般的です。
既存のアーキテクチャが完全に理解されておらず、分析が行き詰まっている場合、多くの場合、「キャッシュですべての問題を解決できる」という誤った考えが醜い頭をもたげます。
開発者の意見では、恐ろしい既存のシステムに対処する代わりに、前面にキャッシュのレイヤーを追加して既存のシステムを隠し、最善の結果を期待する方がよいと考えています。間違いなく、このアプローチではアーキテクチャ全体がより複雑になるだけであり、引き継ぐ次の開発者がシステムの現在の状態を理解しようとすると、状況はさらに悪化するでしょう。
大規模で設計が不十分なシステムは、全体的な設計が欠如していることが多く、一度に 1 行のコードと 1 つのサブシステムが記述されます。ただし、多くの場合、アーキテクチャを単純化してリファクタリングするとパフォーマンスが向上し、ほとんどの場合、理解しやすくなります。
そのため、本当にキャッシュを追加する必要があるかどうかを評価するときは、まず、キャッシュ層によってもたらされる本当の価値を証明するために、いくつかの基本的な使用統計 (ヒット率やミス率など) を収集する計画を立てる必要があります。
6. すべてのアプリケーションは、Stop-The-World 問題に注意する必要があります
Java プラットフォームには、ガベージ コレクションを実行するために、すべてのアプリケーション スレッドを定期的に一時停止する必要があるという変更できない事実があります。これは、実際の証拠がない場合でも、Java の重大な欠点として引用されることがあります。
実証研究によると、デジタル データ (価格変動など) が 200 ミリ秒に 1 回よりも頻繁に変化すると、人々はそれを正常に認識できなくなります。
アプリは主に人間が使用することを目的としているため、200 ミリ秒以下の Stop-The-World (STW) は通常影響を及ぼさないという有用な経験則があります。一部のアプリケーション (ストリーミング メディアなど) には、より高い要件が必要な場合がありますが、多くの GUI アプリケーションではそれは必要ありません。
一部のアプリケーション (低レイテンシー取引やマシン制御システムなど) は 200 ミリ秒の一時停止を許容できません。このタイプのアプリケーションを作成していない限り、ユーザーがガベージ コレクターの影響を感じることはほとんどありません。
アプリケーション スレッドの数が物理コアの数を超えるシステムでは、オペレーティング システムが CPU へのタイムシェアリング アクセスを制御する必要があることに注意してください。 Stop-The-World というと恐ろしく聞こえますが、実際には、どのアプリケーション (JVM であっても他のアプリケーションであっても) は、希少なコンピューティング リソースをめぐる競合の問題に直面する必要があります。
測定しない場合、JVM がアプリケーションのパフォーマンスにさらにどのような影響を与えるかは不明です。
とにかく、GC ログをオンにして、一時停止時間が実際にアプリケーションに影響を与えているかどうかを確認してください。手動またはスクリプトやツールを使用してログを分析し、一時停止時間を決定します。次に、それらが実際にアプリケーションに問題を引き起こすかどうかを判断します。最も重要なことは、「ユーザーは実際に不満を抱いているのか?」という重要な質問を自分自身に問いかけることです。
7. 手書きオブジェクト プールは、大規模なクラスのアプリケーションに適しています
Stop-The-World の一時停止がある程度悪いものであることを考慮すると、アプリケーション開発チームの一般的な反応は、Java ヒープ内に独自のメモリ管理テクノロジを実装することです。これは多くの場合、オブジェクト プール (または完全な参照カウント) を実装し、ドメイン オブジェクトを使用するコードを関与させる必要があるということになります。
このテクニックは、ほとんどの場合、誤解を招きます。これは、オブジェクトの割り当てに非常にコストがかかり、オブジェクトの変更がはるかに安価だった過去の理解に基づいています。今では状況はまったく異なります。
現在のハードウェアは割り当てにおいて非常に効率的であり、最新のデスクトップまたはサーバー ハードウェアでは、メモリ帯域幅は少なくとも 2 ~ 3 GB です。これは大きな数であり、アプリケーションが特別に作成されない限り、このような広い帯域幅を最大限に活用するのは簡単ではありません。
一般に、オブジェクト プーリングを正しく実装することは非常に難しく (特に複数のスレッドが動作している場合)、オブジェクト プーリングにはいくつかのマイナス要件もあり、この手法が一般的に良い選択とは言えません。オブジェクト プールのコードはオブジェクト プールを理解し、それを正しく処理できなければなりません
どのコードがオブジェクト プールを認識し、どのコードがオブジェクト プールを認識していないか?境界は誰もが知っており、ドキュメントに記述されている必要があります
これら追加の複雑さ 常に更新し、定期的にレビューしてください
そのうちの 1 つが満たされていない場合、問題が静かに発生するリスク (C でのポインターの再利用と同様) が再発します
つまり、受け入れられないのは GC の一時停止だけであり、調整と再構築が必要です。 オブジェクト プーリングは、ストールを許容レベルまで低減できない場合にのみ使用できます。
8. ガベージ コレクションでは、Parallel Old よりも CMS が常に優れた選択肢です。
Oracle JDK は、デフォルトで、古い世代を収集するために並列 Stop-The-World コレクター、つまり Parallel Old コレクターを使用します。
Concurrent-Mark-Sweet (CMS) は、ほとんどのガベージ コレクション サイクル中にアプリケーション スレッドが実行を継続できるようにする代替手段ですが、コストがかかり、いくつかの注意点があります。
アプリケーション スレッドをガベージ コレクション スレッドと一緒に実行できるようにすると、必然的に問題が発生します。アプリケーション スレッドがオブジェクト グラフを変更し、オブジェクトの実行可能性に影響を与える可能性があります。この状況は事後に解決する必要があるため、CMS には実際には 2 つの STW フェーズ (通常は非常に短い) があります。
これにはいくつかの影響があります。
すべてのアプリケーション スレッドは安全なポイントに移動する必要があり、各 Full GC 中に 2 回一時停止されます。
ガベージ コレクションはアプリケーションと同時に実行されますが、アプリケーションのスループットは低下します。削減 (通常は 50%);
ガベージ コレクションに CMS を使用する場合、JVM によって使用されるブックキーピング情報 (および CPU サイクル) は他の並列コレクターよりもはるかに多くなります。
これらのコストがお金に見合う価値があるかどうかは、アプリケーションによって異なります。しかし、無料のランチはありません。 CMS コレクターの設計は賞賛に値しますが、万能薬ではありません。
したがって、CMS が正しいガベージ コレクション戦略であることを確認する前に、まず、Parallel Old の STW 一時停止が実際には受け入れられず、調整できないことを確認する必要があります。最後に、すべてのメトリクスは運用システムと同等のシステムから取得する必要があることを強調します。
9. ヒープ サイズを増やすことでメモリの問題を解決できる可能性があります
アプリケーションに問題が発生し、GC の問題が疑われる場合、多くのアプリケーション チームの反応はヒープ サイズを増やすことです。場合によっては、これによりすぐに結果が得られ、より思慮深い解決策を検討する時間を得ることができます。ただし、パフォーマンスの問題の原因が完全に理解されていない場合、この戦略は状況を悪化させる可能性があります。
大量のドメイン オブジェクト (その存続期間は代表的なもので、たとえば 2 ~ 3 秒) を生成する、非常に不十分にコーディングされたアプリケーションを考えてみましょう。割り当て率が十分に高い場合、ガベージ コレクションが頻繁に発生するため、ドメイン オブジェクトは古い世代に昇格されます。ドメイン オブジェクトが古い世代に入るとほぼすぐに、その生存期間は終了し、直接消滅しますが、次のフル GC までリサイクルされません。
アプリケーションのヒープ サイズを増やすと、比較的寿命の短いオブジェクトが入ったり消えたりするために使用されるスペースが増えるだけです。これにより、Stop-The-World の一時停止時間が長くなり、アプリケーションにとっては有益ではありません。
ヒープ サイズを変更したり、他のパラメーターを調整したりする前に、オブジェクトの割り当てと有効期間のダイナミクスを理解する必要があります。パフォーマンス データを測定せずにやみくもに行動すると、状況はさらに悪化するだけです。ここでは、ガベージ コレクターの古い世代のディストリビューションが特に重要です。
結論
Java のパフォーマンス チューニングに関しては、直感が誤解を招くことがよくあります。プラットフォームの動作を視覚化し、理解を高めるには、実験データとツールが必要です。
ガベージ コレクションがその最良の例です。チューニングやチューニングをガイドするデータの生成では、GC サブシステムには無限の可能性がありますが、実稼働アプリケーションでは、ツールを使用せずに生成されたデータの意味を理解するのは困難です。
デフォルトでは、Java プロセス (開発環境と運用環境を含む) を実行するときは、常に少なくとも次のパラメータを使用する必要があります:
-verbose:gc (GC ログを出力)
-Xloggc: (より包括的な GC ログ)
-XX:+PrintGCDetails (より詳細な出力)
-XX:+PrintTenuringDistribution (オブジェクトを古い世代に昇格させるために JVM によって使用される経過時間のしきい値を表示します)
次に、ツールを使用してログを分析します。ここでは手書きのスクリプトを使用できます。グラフを生成するには、GCViewer (オープン ソース) や jClarity Censum などの視覚化ツールを使用することもできます。