Edited at

今さらだけど・・・Javaは2度「コンパイル」されているのを知らなかった!(><)

2019/8/18 引用・参考資料の追加に伴い脚注を部分改訂

2018/10/14 部分改訂

==========================================================================


0.はじめに

どうも。ご無沙汰しています。zd6ir7です。前回の投稿「AWS Cloud9のJavaEE環境でWebアプリを動かしてみた」から時間が空いてしまいましたが、この夏ちょっと恥ずかしいと感じたことがありこのテーマで投稿したいと思います。例のとおり、今回もわかりやすさ追求のため、詳細を省略しているところがある点ご了承願います。


1.プログラムが動くときの仕組み

本題を話す前に、まずはプログラムが動くときの仕組み(流れ)について触れる必要があります。この世の中のありとあらゆるプログラムは、Visual Basicであれ、C++であれ、ディスクからメモリにロードされ、最終的にCPUによって解釈、実行されます。

CPUによって実行されるためには、プログラムがCPUによって解釈される言語に変換できなければなりません。その「言語」が「マシン語」(※1)と呼ばれるもので、0と1のみから構成される言語になります。なぜ0と1からしか構成されないのか?と言うと、CPUを形成する電子部品(IC)のピン1本が電気を通すか通さないかの2つの状態しか採りえないためです。このICの特性から、CPUはあらゆる情報を0と1から構成される2進数で取り扱っているわけです(※2)。


2.「コンパイル」の本質

このように、CPUが解釈できるよう、プログラムのソースコードを、0か1のみ構成されるマシン語にのファイルに変換することを「コンパイル」と言い、変換するためのツールを「コンパイラ」と呼んでいます。コンパイラによってコンパイルされたマシン語のファイルはメモリにロードされ最終的にCPUによって実行されるわけです(※3)。

「引用・参考資料」の3.~5.によると、コンパイルという観点で見ると、世の中のプログラミング言語は以下の2種類に分けることができるようです。


  1. コンパイラ型言語

    ソースコードをいったんマシン語のファイルに変換してから、CPUにプログラムを実行させる言語。代表的な言語にFortran、C、COBOL等がある。


  2. インタプリタ型言語

    ソースコードをマシン語のファイルに変換させることなく、そのままCPUにてプログラムを実行させる言語。代表的な言語にPerl、Ruby、PHP等がある。


2.については、少し補足が必要です。CPUはマシン語しかわからないため、例えばPythonのコードを実行させようにもいずれにしてもマシン語に変換しなければなりません。ここで「インタプリタ」が登場するわけですが、このインタプリタが、この例ではPythonですが、Pythonのコードを1行ずつ


  • 読み取って

  • マシン語に変換して

  • CPUに実行させる
    ということをコードの終わりまで繰り返し行うことによってプログラムを実行させているわけです。


ここではっきりとするのは、当たり前ですが、プログラムは必ずマシン語に変換させなければならない、という事実です。ソースコードを「コンパイル」という手段によって別ファイルに変換してCPUに実行させるのか、変換はせずインタプリタを使って直接CPUに実行させるのかの違いに過ぎないわけです(※4)。


3.Javaは2度「コンパイル」される!?

さて、ここからJavaの登場です。Javaはソースコードからどのようなプロセスを経て実行にまでに至るのでしょうか?

上の図で簡単な流れを示しましたが、結論から言うと、Javaは2回「コンパイル」されて実行されます。他のプログラミング言語とは特殊なプロセスを経るわけですが、以下詳細を示します。


  1. ソースコードからclassファイルへの変換(第1回目の「コンパイル」)

    Javaコンパイラ(Javacツール)はソースコードを、バイトコード即ち、0と1からのみからなる8ビットのバイトストリームで構成された、classファイルに「コンパイル」する(※5)。




  2. classファイルから実行へ(第2回目の「コンパイル」)

    Java Virtual Machine(以下、JVM)がこのclassファイルを読み込み、実行段階に入ると(※6)、


    • まずはJVM内のインタプリタによってバイトコードを1行ずつ解釈してJVMにて実行する。

    • インタプリタによる実行によって実行対象のコードに関する情報が蓄積されると、同じくJVM内のJIT(Just-In-Time)コンパイラが、頻繁に呼び出されるメソッドやループ処理等が短時間で終えられるよう最適化を施したうえで、実行対象のバイトコードをまとまった単位(※7)でマシン語に「コンパイル」して実行する。マシン語はアセンブリ言語の体系、具体的には「[インデックス] [命令] [オペランド]」の形態をとっている(※8)。



JVM内にはインタプリタと(JIT)コンパイラが同居しており、1行1行読んでマシン語に変換するのか、まとまった単位で「コンパイル」するのか、の2つの機能の組み合わせによってJavaプログラムが実行されているのがわかると思います。JVMには様々なオプションがあり、上記のように最初にインタプリタに実行させてからJITコンパイラを呼ぶ方法もあれば、いきなりJITコンパイラにコンパイルさせ方法もありますし、はたまた両者を適度に組み合わせるといったオプションも用意されています(※9)。パフォーマンス要件に応じていずれかを選択するイメージになります。オプションの詳細は「引用・参考資料」の3.をご参照ください。


4.おわりに

いかがでしたでしょうか?Javaが2度コンパイルされて実行されることがイメージいただけたのではないかと思います。これまでの話をまとめると、


  • どんなプログラミング言語であっても、それがインタプリタ型だろうがコンパイル型だろうが、CPUによって解釈・実行できるよう、プログラムは最終的に0と1からのみ構成されるマシン語に変換されなければならない。

  • Javaは他のプログラミング言語と異なり、2回「コンパイル」して実行するという形態をとる。1回目の「コンパイル」ではソースコードはclassファイルに変換され、2回目の「コンパイル」ではclassファイルは最終的にマシン語に変換される。2回目の際、「インタプリタ」による逐次実行で得られた情報をもとに最適化が加えられている。

となります。Javaの「コンパイル」と言うと、どちらの「コンパイル」なのかで混乱するかもしれません。その際、本稿が読者の皆様の理解に役立てられればと思います。一方で重要ことは、Javaを含めどんなプログラミング言語でも共通する、プログラムが動くときの仕組みを理解しておくことではないかと思います。それが、Javaの「コンパイル」について聞かれても混乱を最小限に止めることができますし、ひいては何が本質かを見極めることができるためです。


5.余談

ここまで読んでくださりありがとうございました。それにしても今回ほど原稿を作るのに苦労するものはなかったです。これまでの「先行研究」が少ないことから、何度も壁にぶち当たっては乗り越えての繰り返しで・・・そのせいもあってお見苦しいところあるかもしれませんが、随時改善してまいります。また次回お会いしましょう。それにしても脚注が大きくなってしまった・・・


脚注

(※1)「機械語」または「ネイティブ・コード」とも言います。以降本稿では「マシン語」で通します。

(※2)「引用・参考資料」の1.にCPUの仕組み、なぜ2進数で取り扱わないといけないのか等の詳細な解説がありますので、そちらを適宜ご参照願います。また「引用・参考資料」の2.もコンピュータが動く仕組みが分かりやすく記載されていますので、是非併せてご参照ください。

(※3)細かいですが、「コンパイル」という用語の定義に関して言うと、文献やWebサイトによって単にマシン語に変換するだけなのか、マシン語を何らかの形式に変換するのか等、微妙に違っているようです。本稿では、本来いろいろなプログラミング言語でコンパイラが果たしてきた別ファイル形式に変換するという役割、並びにインタプリタ型言語の比較の点からマシン語の「ファイル」に変換することを、コンパイルと定義しています。

(※4)「引用・参考資料」の3.や5.にあるとおり、コンパイラ型言語だと、コンパイラによってソースコードを分析・最適化することにより効率的なコードが実行される一方で、インタプリタ型言語だと1行1行実行されるため、コンパイラ型言語より速度が遅くなる欠点があります。余談ですが、以下に両言語の特徴をまとめてみました。

コンパイラ型言語
インタプリタ型言語

性能(実行速度)
相対的に速い
相対的に遅い

プログラミング負荷
相対的に大きい
相対的に小さい(軽量言語)

移植性
低い
高い

種類
Fortran、COBOL、C、C++等
Perl、Ruby、PHP、Python等

当初C#をコンパイラ型言語にしていましたが、外しています。C#は「.NET の概念と構造」のとおり、共通中間言語(CIL)に変換されランタイムによって実行されるためであり、ピンポイントで位置づけられないためです。

Pythonはインタプリタ型言語にしていますが、「Pythonのpycファイルを作成する方法【初心者向け】」のとおり、pycファイルに「コンパイル」されてから実行されるため、ピンポイントでそれぞれコンパイラ型言語の要素もあります。

(※5)classファイルの詳細な構成は「引用・参考資料」の6.、7.をご参照ください。

(※6)classファイルはJVMでいきなり実行されるわけではなく、「クラスローダーサブシステム」によって読み込み処理がなされ、「実行時データ領域」に情報が割り当てられてから、「実行時エンジン」内のインタプリタ、JITコンパイラによって実行されます。詳細は「引用・参考資料」の8.、9.、11.をご参照ください。

(※7)残念ながら第2回目の「コンパイル」について、1度中間的なファイルに変換するのか等、具体的にどういった形式で落とすのか、本稿で挙げた「引用・参考資料」の他、各種サイト、文献等確認しましたが見つけられませんでした。JVMの詳細仕様に関わるため公開ができないためと思われますが、筆者の推測として、1つ以上のclassとか、バイトコードの何バイトとか特定の単位で、中間的かつ一時的なマシン語の「ファイル」に変換してメモリにロードしているものと考えられます。

(※8)特定のclassファイルに対してjavapコマンドを実行すると、アセンブリ言語の体系となっていることが確認できます。javapコマンドの詳細は「引用・参考資料」の6.、10.を参照ください。ちなみにアセンブリ言語についての解説は「引用・参考資料」の1.、2.をご参照ください。

(※9)JITコンパイラは、オプションを「-Djava.compiler=NONE」と設定することにより、無効化することができます。JIT の使用不可化を参照ください。


引用・参考資料


  1. 矢沢久雄著/日経ソフトウェア監修『プログラムはなぜ動くのか~知っておきたいプログラムの基礎知識~』 日経BP社

  2. 矢沢久雄著/日経ソフトウェア監修『コンピュータはなぜ動くのか~知っておきたいハードウエア&ソフトウエアの基礎知識~』 日経BP社

  3. Scott Oaks著、Acroquest Technology株式会社監訳、寺田佳央監訳、牧野聡訳 『Javaパフォーマンス』 オライリー・ジャパン

  4. TAKUYA-110さん著『プログラミング基礎』 Qiitaブログ

  5. 辻真吾著『Pythonスタートブック』 技術評論社

  6. y torazuka著 『初心者向けJavaクラスファイルの読み方』SlideShare


  7. Java Language and Virtual Machine Specifications Oracle Docs

  8. JavaプログラムJVM


  9. The JVM Architecture Explained - An overview of the different components of the JVM, along with a very useful diagram by Jackson Joseraj Java Zone


  10. javap - Java クラスファイル逆アセンブラ Oracle Docs


  11. Preface to java virtual machine and architecture by admin

1.と2.は、「個人的に役立つ技術本」にて、3.は「CPU使用率100%は悪なのか?」でも触れていますので、ぜひ確認してみてください。