web-dev-qa-db-ja.com

ソースコードをJavaバイトコードに変換する使用法は何ですか?

アーキテクチャごとに異なるJVMが必要な場合、この概念の導入の背後にあるロジックが何であるか理解できません。他の言語ではマシンごとに異なるコンパイラが必要ですが、Javaでは異なるJVMが必要なので、JVMの概念やこの追加の手順を導入する背後にあるロジックは何ですか??

37
Pranjal Kumar

論理は、JVMバイトコードはJavaソースコードよりもはるかに単純です。

コンパイラーは、非常に抽象的なレベルで、解析、セマンティック分析、コード生成の3つの基本的な部分を持つと考えることができます。

解析は、コードを読み取り、それをコンパイラのメモリ内のツリー表現に変換することで構成されます。セマンティック分析は、このツリーを分析し、それが何を意味するのかを理解し、すべての高レベルの構造を低レベルの構造に単純化する部分です。そしてコード生成は、単純化されたツリーを受け取り、それをフラットな出力に書き出します。

バイトコードファイルを使用すると、再帰的な(ツリー構造の)ソース言語ではなく、JITが使用するのと同じフラットバイトストリーム形式で書き込まれるため、解析フェーズが大幅に簡略化されます。また、セマンティック分析の重い部分の多くは、Java(または他の言語)コンパイラ)によってすでに実行されています。そのため、コードをストリーム読み取りするだけで、最小限の操作を実行できます。解析と最小限のセマンティック分析を行い、コード生成を実行します。

これにより、JITが実行する必要のあるタスクが大幅に簡素化され、実行速度が大幅に向上する一方で、単一ソースのクロスプラットフォームコードを理論的に記述できるようにする高レベルのメタデータとセマンティック情報が保持されます。

79
Mason Wheeler

さまざまな種類の中間表現は、いくつかの理由により、コンパイラ/ランタイム設計でますます一般的になっています。

Javaの場合、最初の最大の理由はおそらくportability:Javaは、最初は「Write Once、Run Anywhere」として広く販売されていました。これは、ソースコードと異なるコンパイラを使用して異なるプラットフォームをターゲットにする場合、これにはいくつかの欠点があります。

  • コンパイラは、言語の便利な構文をすべて理解する必要がある複雑なツールです。バイトコードは人間が読み取れるソースよりもマシン実行可能コードに近いため、より単純な言語にすることができます。これの意味は:
    • コンパイルは、バイトコードの実行に比べて遅い場合があります
    • 異なるプラットフォームをターゲットとするコンパイラーは、異なる動作を生成するか、言語の変更に対応できない可能性があります
    • 新しいプラットフォーム用のコンパイラを作成することは、そのプラットフォーム用のVM(またはバイトコードからネイティブコンパイラ)を作成することよりもはるかに困難です。
  • ソースコードを配布することは必ずしも望ましいことではありません。バイトコードは、リバースエンジニアリングに対する保護を提供します(ただし、意図的に難読化しない限り、逆コンパイルはかなり簡単です)

中間表現の他の利点は次のとおりです。

  • 最適化、パターンをバイトコードで見つけて同等のものにコンパイルするか、プログラムの実行時に特殊なケース用に最適化することもできます( "JIT"、または "ジャストインタイム"コンパイラを使用)
  • 相互運用性同じVM内の複数の言語間。これはJVM(Scalaなど)で一般的になり、.netフレームワークの明確な目的です
27
IMSoP

なぜソースコードだけを配布しないのかと不思議に思われるかもしれません。その質問を振り返ってみましょう:なぜマシンコードを単に配布しないのですか?

明らかに、ここでの答えは、Javaは設計上、コードが実行されるマシンが何であるかを知っているとは想定していないということです。デスクトップ、スーパーコンピュータ、電話、またはその中間にあるものであれば何でも構いません。 JavaローカルJVMコンパイラがその機能を実行する余地を残します。これには、コードの移植性を高めることに加えて、コンパイラがマシンを利用するなどのことを実行できるという素晴らしい利点があります。特定の最適化(存在する場合)、または存在しない場合でも少なくとも機能するコードを生成します。 [〜#〜] sse [〜#〜] のようなものまたはハードウェアアクセラレーションは、マシンでのみ使用できますそれらをサポートします。

この観点から見ると、生のソースコードよりもバイトコードを使用する理由はより明確です。生の機械語にできるだけ近づけることで、次のような機械コードの利点の一部または全部を実現できます。

  • 一部のコンパイルと分析はすでに行われているため、起動時間が短縮されます。
  • セキュリティ。バイトコード形式には、配布ファイルに署名するための組み込みのメカニズムがあるため(ソースは慣例によりこれを行うことができますが、これを実現するメカニズムは、バイトコードのように組み込まれていません)。

高速実行については触れていません。ソースコードとバイトコードはどちらも、実際に実行するために、同じマシンコードに完全にコンパイルするか、または(理論的には)コンパイルできます。

さらに、バイトコードにより、マシンコードよりもいくつかの改善が可能になります。もちろん、前に述べたプラットフォームの独立性とハードウェア固有の最適化はありますが、JVMコンパイラにサービスを提供して、古いコードから新しい実行パスを生成するようなものもあります。これは、セキュリティの問題にパッチを適用したり、新しい最適化が発見された場合、または新しいハードウェア命令を利用したりするためです。実際には、バグが発生する可能性があるため、このように大きな変更が見られることはまれですが、それは可能であり、常に小さな方法で発生するものです。

8
Joel Coehoorn

ここでは、少なくとも2つの異なる質問が考えられます。 Java基本的にはジャンルの単なる例です。もう1つはJavaが使用する特定のバイトコードに固有です。

コンパイラー全般

まず、一般的な質問について考えてみましょう。なぜコンパイラーは、特定のプロセッサーで実行するためにソースコードをコンパイルするプロセスで中間表現を使用するのでしょうか。

複雑さの軽減

その答えの1つはかなり単純です。O(N * M)問題をO(N + M)問題に変換します。

N個のソース言語とM個のターゲットが与えられ、各コンパイラが完全に独立している場合、すべてのソース言語をすべてのターゲットに変換するためにN * Mコンパイラが必要です(「ターゲット」は、プロセッサとOS)。

ただし、これらすべてのコンパイラが共通の中間表現に同意する場合は、ソース言語を中間表現に変換するNコンパイラフロントエンドと、中間表現を特定のターゲットに適したものに変換するMコンパイラバックエンドを使用できます。

問題のセグメンテーション

さらに良いことに、それは問題を2つの多かれ少なかれ排他的なドメインに分離します。言語設計、構文解析などを知っている/気にする人はコンパイラのフロントエンドに集中できますが、命令セットやプロセッサ設計などを知っている人はバックエンドに集中できます。

したがって、たとえば、LLVMのようなものを考えると、さまざまな異なる言語用のフロントエンドがたくさんあります。また、多くの異なるプロセッサ用のバックエンドもあります。言語の男は自分の言語の新しいフロントエンドを書くことができ、多くのターゲットをすばやくサポートできます。プロセッサの男は、言語設計や解析などを行わずに、ターゲットの新しいバックエンドを作成できます。

コンパイラをフロントエンドとバックエンドに分離し、2つの間で通信するための中間表現を使用することは、Javaでは元々ありません。それは長い間かなり一般的な慣行でした(とにかく、Javaが登場する前から)。

分布モデル

Java=がこの点で何か新しいものを追加した程度に、それは分散モデルにありました。特に、コンパイラが内部的に長い間フロントエンドとバックエンドの部分に分離されていたとしても通常、これらは単一の製品として配布されていました。たとえば、Microsoft Cコンパイラを購入した場合、内部には「C1」と「C2」があり、それぞれフロントエンドとバックエンドでしたが、購入したのは、両方を含む「Microsoft C」だけでした(2つの間の操作を調整する「コンパイラドライバ」を使用)。コンパイラは2つの部分で構築されていますが、コンパイラを使用する通常の開発者にとっては、それはただ1つソースコードからオブジェクトコードに変換され、間に何も表示されません。

その代わり、JavaはフロントエンドをJava開発キットで配布し、バックエンドをJava仮想マシンで配布しました。すべてのJavaユーザーは、使用しているシステムをターゲットとするコンパイラバックエンドを備えていました。Java開発者は中間形式でコードを配布しているため、ユーザーがコードをロードすると、JVMは特定のマシンで実行する必要があります。

先例

この配布モデルもまったく新しいものではなかったことに注意してください。たとえば、UCSD Pシステムは同様に機能しました。コンパイラのフロントエンドがPコードを生成し、Pシステムの各コピーには、その特定のターゲットでPコードを実行するために必要なことを実行する仮想マシンが含まれていました1

Javaバイトコード

JavaバイトコードはPコードに非常に似ています。これは基本的にかなり単純なマシンの指示です。そのマシンは既存のマシンを抽象化することを目的としているため、ほとんどすべての特定のターゲットにすばやく変換するのはかなり簡単です。元々の意図はP-Systemが行ったようにバイトコードを解釈することだったので、翻訳のしやすさは早い段階で重要でした(そしてもちろん、それがまさに初期の実装が機能した方法です)。

強み

Javaバイトコードは、コンパイラのフロントエンドで簡単に作成できます。 (たとえば)式を表すかなり典型的なツリーがある場合、通常はツリーをたどり、各ノードで見つけたものから直接コードをかなり簡単に生成できます。

Javaバイトコードは非常にコンパクトです。ほとんどの場合、ほとんどの典型的なプロセッサ(特にSPARCなどのほとんどのRISCプロセッサ)のソースコードまたはマシンコードよりもはるかにコンパクトです。 SunはJavaの設計時に販売しました。Javaの主な目的の1つは、アプレット(実行前にダウンロードされるWebページに埋め込まれたコード)をサポートすることでした。ほとんどの人が電話回線を介して1秒あたり約28.8キロビットでモデム経由で私たちにアクセスしたとき(もちろん、古い、遅いモデムを使用している人はまだかなりいます)。

弱点

Javaバイトコードの主な弱点は、それらが特に表現力がないことです。それらはJavaに存在する概念をかなりうまく表現できますが、同様に、ほとんどのマシンでバイトコードを実行するのは簡単ですが、特定のマシンを最大限に活用する方法では、はるかに困難です。

たとえば、Javaバイトコードを本当に最適化したい場合は、基本的にリバースエンジニアリングを行って、それらを表現のようなマシンコードから逆変換し、SSAに戻すというのはかなりルーチンです指示(または類似の何か)2。次に、SSA命令を操作して最適化を行い、そこから、本当に気になるアーキテクチャをターゲットとするものに変換します。ただし、このかなり複雑なプロセスを使用しても、Javaにとっては重要ではない)概念の一部は、ソース言語から最適な(近くでさえも)実行されるマシンコードに変換するのが難しいため、表現するのが非常に困難です。ほとんどの典型的なマシンで。

概要

一般的に中間表現を使用する理由について質問する場合、2つの主な要因は次のとおりです。

  1. O(N * M)問題をO(N + M)問題に減らし、
  2. 問題をより扱いやすい部分に分割します。

Javaバイトコードの詳細について、そしてなぜ彼らが他の表現の代わりにこの特定の表現を選んだのかについて尋ねているなら、私は答えが大部分は元の意図に戻ってくると思います当時のウェブの制限により、次の優先事項につながります。

  1. コンパクトな表現。
  2. すばやく簡単にデコードして実行できます。
  3. ほとんどの一般的なマシンにすばやく簡単に実装できます。

多くの言語を表現したり、多種多様なターゲットで最適に実行したりできることは、優先度がはるかに低い(それらがまったく優先度と見なされた場合)。


  1. では、なぜPシステムはほとんど忘れられているのでしょうか?主に価格設定の状況。 PシステムはApple II's、Commodore SuperPetsなど)でかなり上手く売れました。IBMPCが出たとき、PシステムはサポートされたOSでしたが、MS-DOSのコストは(ほとんどの人の観点から) 、本質的に無料で投入されました)、そしてそれはMicrosoftとIBM(とりわけ)が書いたものなので、すぐにより多くのプログラムが利用可能になりました。
  2. たとえば、これは Soot の動作方法です。
8
Jerry Coffin

元々、JVMは純粋なインタプリタでした。そして、あなたが解釈している言語が可能な限りsimpleであるなら、あなたは最高のパフォーマンスのインタプリタを手に入れます。それがバイトコードの目標でした。ランタイム環境に効率的に解釈可能な入力を提供することです。この単一の決定により、そのパフォーマンスから判断して、Javaインタープリター言語よりもコンパイル言語に近くなりました。

その後、JVMの解釈のパフォーマンスが依然として低下していることが明らかになったとき、人々はパフォーマンスの高いジャストインタイムコンパイラを作成するための努力に投資しました。これにより、CやC++などのより高速な言語とのギャップが多少なくなりました。 (ただし、一部のJava固有の速度の問題が残っているため、おそらくJava実行されたCコードと同じように動作する環境)ことはありません。)

もちろん、ジャストインタイムコンパイルの手法が手元にあるので、couldは実際にソースコードを配布し、ジャストインタイムコンパイルでマシンコードにコンパイルします。ただし、これにより、コードの関連するすべての部分がコンパイルされるまで、起動パフォーマンスが大幅に低下します。バイトコードは同等のJavaコードよりも解析が非常に簡単であるため、ここでも重要な助けになります。

つまり、バイトコードからマシンコードへのコンパイルは、元のコードをマシンコードへと適切なタイミングで解釈するよりも高速です。しかし、アプリケーションをクロスプラットフォームにするためには、変更を加えたり、準備(コンパイル)したりせずに、すべてのプラットフォームで元のコードを使用したいので、解釈が必要です。したがって、最初にjavacがソースをバイトコードにコンパイルしてから、このバイトコードを任意の場所で実行でき、Java仮想マシンからマシンコードへの変換が速くなります。答え:時間を節約できます。

0
Sergey Orlov

他の人が指摘した利点に加えて、バイトコードははるかに小さいので、配布および更新が容易であり、ターゲット環境で占有するスペースが少なくなります。これは、スペースに制約のある環境では特に重要です。

また、著作権で保護されたソースコードの保護も容易になります。