1. 概要

プログラミング言語は、抽象化のレベルに基づいて分類されます。 高水準言語(Java、Python、JavaScript、C ++、Go)、低水準(アセンブラー)、そして最後に機械語を区別します。

Javaのようなすべての高級言語コードは、実行のためにマシンネイティブコードに変換する必要があります。この変換プロセスは、コンパイルまたは解釈のいずれかです。 ただし、3番目のオプションもあります。 両方のアプローチを利用しようとする組み合わせ。

このチュートリアルでは、Javaコードが複数のプラットフォームでコンパイルおよび実行される方法について説明します。 JavaとJVMの設計の詳細をいくつか見ていきます。 これらは、Javaがコンパイルされているか、インタプリタされているか、または両方のハイブリッドであるかを判断するのに役立ちます。

2. コンパイル済みvs。 解釈

コンパイルされたプログラミング言語とインタプリタされたプログラミング言語の基本的な違いを調べることから始めましょう。

2.1. コンパイル言語

コンパイルされた言語(C ++、Go)は、コンパイラプログラムによってマシンネイティブコードに直接変換されます。

実行前に明示的なビルドステップが必要です。 そのため、コードを変更するたびにプログラムを再構築する必要があります。

コンパイルされた言語は、解釈された言語よりも高速で効率的である傾向がありますただし、生成されたマシンコードはプラットフォーム固有です。

2.2. 解釈される言語

一方、インタプリタ言語(Python、JavaScript)では、ビルド手順はありません。 代わりに、インタプリタはプログラムの実行中にプログラムのソースコードを操作します。

インタプリタ言語は、かつてコンパイル言語よりも大幅に遅いと考えられていました。 ただし、ジャストインタイム(JIT)コンパイルの開発により、パフォーマンスのギャップは縮小しています。 ただし、JITコンパイラは、プログラムの実行時に、コードをインタープリター型言語からマシンネイティブコードに変換することに注意してください。

さらに、Windows、Linux、Macなどの複数のプラットフォーム解釈された言語コードを実行できます。 解釈されたコードは、特定のタイプのCPUアーキテクチャとは親和性がありません。

3. ライトワンスランエニウェア

JavaとJVMは、移植性を念頭に置いて設計されました。 したがって、今日最も人気のあるプラットフォームはJavaコードを実行できます。

これは、Javaが純粋にインタープリター型言語であるというヒントのように聞こえるかもしれません。 ただし、実行前に、 Javaソースコードはバイトコードにコンパイルする必要があります。 バイトコードは、JVMにネイティブな特別な機械語です。 JVMは、実行時にこのコードを解釈して実行します。

プログラムやライブラリではなく、Javaをサポートするプラットフォームごとに構築およびカスタマイズされたのはJVMです。

最新のJVMにはJITコンパイラもあります。 これは、JVMが実行時にコードを最適化して、コンパイルされた言語と同様のパフォーマンス上の利点を得ることを意味します。

4. Javaコンパイラ

javacコマンドラインツールは、Javaソースコードをプラットフォームに依存しないバイトコードを含むJavaクラスファイルにコンパイルします。

$ javac HelloWorld.java

ソースコードファイルには.javaサフィックスが付いていますが、バイトコードを含むクラスファイルは。classサフィックスで生成されます。

5. Java仮想マシン

コンパイルされたクラスファイル(バイトコード)は、 Java仮想マシン(JVM)によって実行できます。

$ java HelloWorld
Hello Java!

それでは、JVMアーキテクチャについて詳しく見ていきましょう。 私たちの目標は、実行時にバイトコードがマシンネイティブコードに変換される方法を決定することです。

5.1. アーキテクチャの概要

JVMは、次の5つのサブシステムで構成されています。

  • ClassLoader
  • JVMメモリ
  • 実行エンジン
  • ネイティブメソッドインターフェイスと
  • ネイティブメソッドライブラリ

5.2. ClassLoader

JVMは、 ClassLoader サブシステムを利用して、コンパイルされたクラスファイルをJVMメモリに取り込みます。

ロードに加えて、ClassLoaderはリンクと初期化も実行します。 これには以下が含まれます:

  • セキュリティ違反がないかバイトコードを確認する
  • 静的変数にメモリを割り当てる
  • シンボリックメモリ参照を元の参照に置き換える
  • 静的変数への元の値の割り当て
  • すべての静的コードブロックを実行する

5.3. 実行エンジン

実行エンジンサブシステムは、バイトコードの読み取り、マシンネイティブコードへの変換、および実行を担当します。

インタプリタとコンパイラの両方を含む、3つの主要なコンポーネントが実行を担当します。

  • JVMはプラットフォームに依存しないため、インタープリターを使用してバイトコードを実行します
  • JITコンパイラは、メソッド呼び出しを繰り返すためにバイトコードをネイティブコードにコンパイルすることでパフォーマンスを向上させます
  • ガベージコレクターは、参照されていないすべてのオブジェクトを収集して削除します

実行エンジンは、 Native Method Interface(JNI)を使用して、ネイティブライブラリとアプリケーションを呼び出します。

5.4. ジャストインタイムコンパイラ

インタプリタの主な欠点は、メソッドが呼び出されるたびに解釈が必要になることです。これは、コンパイルされたネイティブコードよりも遅くなる可能性があります。 Javaは、この問題を克服するためにJITコンパイラを利用します。

JITコンパイラは、インタプリタを完全に置き換えるわけではありません。 実行エンジンは引き続きそれを使用します。 ただし、JVMは、メソッドが呼び出される頻度に基づいてJITコンパイラーを使用します。

JITコンパイラは、メソッドのバイトコード全体をマシンネイティブコードにコンパイルします 、直接再利用できるように標準のコンパイラと同様に、中間コードの生成、最適化、そしてマシンネイティブコードの生成があります。

プロファイラーは、ホットスポットの検索を担当するJITコンパイラーの特別なコンポーネントです。 JVMは、実行時に収集されたプロファイリング情報に基づいて、JITでコンパイルするコードを決定します。

この効果の1つは、Javaプログラムが、数サイクルの実行後にジョブの実行を高速化できることです。 JVMがホットスポットを学習すると、ネイティブコードを作成して、処理を高速化できます。

6. パフォーマンスの比較

JITコンパイルがJavaの実行時パフォーマンスをどのように改善するかを見てみましょう。

6.1. フィボナッチパフォーマンステスト

単純な再帰的方法を使用して、n番目のフィボナッチ数を計算します。

private static int fibonacci(int index) {
    if (index <= 1) {
        return index;
    }
    return fibonacci(index-1) + fibonacci(index-2);
}

繰り返されるメソッド呼び出しのパフォーマンス上の利点を測定するために、フィボナッチメソッドを100回実行します。

for (int i = 0; i < 100; i++) {
    long startTime = System.nanoTime();
    int result = fibonacci(12);
    long totalTime = System.nanoTime() - startTime;
    System.out.println(totalTime);
}

まず、Javaコードを通常どおりにコンパイルして実行します。

$ java Fibonacci.java

次に、JITコンパイラを無効にして同じコードを実行します。

$ java -Djava.compiler=NONE Fibonacci.java

最後に、比較のために、C++とJavaScriptで同じアルゴリズムを実装して実行します。

6.2. パフォーマンステストの結果

フィボナッチ再帰テストを実行した後、ナノ秒単位で測定された平均パフォーマンスを見てみましょう。

  • JITコンパイラを使用したJava– 2726 ns –最速
  • JITコンパイラなしのJava– 17965 ns –559% s下位
  • O2最適化なしのC++– 9435 ns –246% sより低い
  • O2最適化を備えたC++– 3639 ns –33% sより低い
  • JavaScript – 22998 ns –743% sより低い

この例では、 Javaのパフォーマンスは、JITコンパイラを使用すると500%以上向上しています。 ただし、JITコンパイラが起動するには数回の実行が必要です。

興味深いことに、O2最適化フラグを有効にしてC ++をコンパイルした場合でも、JavaのパフォーマンスはC ++コードより33%優れていました。 予想どおり、 C ++は、Javaがまだ解釈されていたとき、最初の数回の実行ではるかに優れたパフォーマンスを示しました。

Javaは、同じくJITコンパイラを使用するNodeで実行される同等のJavaScriptコードよりも優れたパフォーマンスを発揮しました。 結果は、700%以上優れたパフォーマンスを示しています。 主な理由は、JavaのJITコンパイラがはるかに高速に起動することです

7. 考慮事項

技術的には、静的プログラミング言語コードをマシンコードに直接コンパイルすることが可能です。 プログラミングコードを段階的に解釈することも可能です。

他の多くの最新のプログラミング言語と同様に、Javaはコンパイラとインタプリタの組み合わせを使用します。 目標は、両方の長所を活用し、高性能とプラットフォームに依存しない実行を実現することです。

この記事では、HotSpotでの動作の説明に焦点を当てました。 HotSpotは、OracleによるデフォルトのオープンソースJVM実装です。 Graal VM もHotSpotに基づいているため、同じ原則が適用されます。

現在最も人気のあるJVM実装は、インタプリタとJITコンパイラの組み合わせを使用しています。ただし、一部の実装では異なるアプローチを使用している可能性があります。

8. 結論

この記事では、JavaとJVMの内部について説明しました。 私たちの目標は、Javaがコンパイル言語かインタプリタ言語かを判断することでした。 JavaコンパイラとJVM実行エンジンの内部について調べました。

それに基づいて、私たちは次のように結論付けました Javaは、両方のアプローチを組み合わせて使用します。 

Javaで記述したソースコードは、ビルドプロセス中に最初にバイトコードにコンパイルされます。 次に、JVMは生成されたバイトコードを解釈して実行します。 ただし、JVMは、パフォーマンスを向上させるために、実行時にJITコンパイラーも使用します。

いつものように、ソースコードはGitHubから入手できます。