Javaコラム Javaエンジニアのためのお役立ちコラム

IT

Javaバイトコードとは?Javaバイトコードを理解する方法をくわしく紹介します!

2021年03月24日
SE
Javaバイトコードとはどのようなようなものなのでしょうか。
PM
JVMの機械語です。Java言語など(Kotlin、Scala、Groovy・・)で書かれたプログラムはまずJavaバイトコードにコンパイルされJVM(Java仮想マシン )上で動作します。

Javaバイトコードとは?


Java言語など(Kotlin、Scala、Groovy・・)で書かれたプログラムはまずJavaバイトコードにコンパイルされJVM(Java仮想マシン )上で動作します。実機の機械語に対してJavaバイトコードはJVMの機械語です。いずれもバイナリで人間が解読するのは困難です。

通常の実行ファイルはCPUやOSごとに異なりますが、JavaバイトコードはJVMが動作していればCPU、OSなどによらず動作させることができます。

インタープリタ言語との違いは

Python、Ruby、PHPのようなインタープリタ言語の処理系でもソースコードは同様にまずバイトコードに変換され、それを元に処理系やOSで用意された機械語にコンパイルされた関数を1回ずつ呼び出します。

この方式はAOTコンパイル(予め機械語にコンパイルされたプログラムを実行する)やJITコンパイル(バイトコードやソースコードなどの入力を実行時に直接、機械語にコンパイルする)と比べて実行速度は格段に遅くなります。

クライアント向けのJVM

JVMはクライアント向けとサーバ向けの二種類が用意されていて、インタープリタと実行時(JIT)コンパイルを併用しています。

まずインタープリタで実行しながら実行時プロファイルを作成しその情報を元に最適化された機械語にコンパイルし、また実行回数が少ない部分はメモリを節約するためインタープリタのままで実行します。

クライアント向けのJVMではJavaバイトコードをすばやく機械語に実行時コンパイルして実行されます。またメモリの消費を少なく抑えるように実行されます。

サーバ向けJVM

サーバ向けのJVMではインタープリタを走らせながらより詳しく実行時プロファイルを取得します。その分実行後間もなくは低速ですが、より高度に最適化され状況によっては最適化されたC言語などAOT方式を上回る性能を発揮します。

さらに2種類のJVMを組み合わせまずクライアントJVMで実行しコンパイルされて高速に実行されます。サーバJVMはインタープリタで実行するより早く実行時プロファイルを取得できるのでより短時間で最適化された機械語を実行することができます。

JITコンパイルとは

同じJavaバイトコードを使うJavaとGroovyなどJVMで実行する動的型付け言語では、データの型を実行時に決めるための処理が必要になるためJavaに比べると実行速度は遅くなります。

動的型付け言語は機械語にコンパイルされた後も、変化しうる変数やメソッドの型を決めるための機械語が余分に必要になるため、JITコンパイラにより最適化されてコンパイルされてもJavaで書かれたプログラムと同等の実行速度にはなりません。

AndroidアプリでのJavaバイトコード

Androidアプリの開発もJavaバイトコードを使います。AndroidアプリではJavaバイトコードをDalvikという仮想機械語にコンパイルして、そこからさらにAndroid端末の機械語にコンパイルします。基本的にはC言語と同じ事前コンパイル方式(AOT)です。

JavaバイトコードとDalvikは相互に変換可能ですがJVMとDalvik仮想機械は構造が異なります。初期のAndroidではDalvikをJITコンパイルして機械語を生成していましたが、性能や電力消費などの問題に対処するため現在の方式になりました。

Android端末の仮想環境

AndroidはLinuxカーネルを使用していますがアプリはART(Android Runtime)という仮想マシンのような環境で動作します。

ARTは機械語で実行されますが、カーネル側から見るとARTもJVMと同じようなアプリケーションでスレッドや低レベルのメモリの管理をカーネルに依存しています。

インストールや更新で機械語にコンパイルする時間が問題となり、インストール直後から動作するためにJITコンパイラも搭載されています。

なぜJavaバイトコードは必要なのか?

バイトコードはソースコードからコンピュータが効率よくプログラムを実行できる形にコンパイルされます。他のインタープリタ型の言語では内部だけで使用する言語とJavaのようにバイトコードを外部に書き出すことのできる言語があります。

小規模なプログラムではあまり気になりませんが、大規模なプログラムではバイトコードにコンパイルする時間を節約することができます。

実行に必要な情報を記録する

Javaバイトコードにはソースコードから読み取ったプログラム内のメソッドや変数の型やクラスの情報が含まれています。動的なJavaバイトコードを利用した言語では変数やメソッドの型情報は実行時に解決します。

Javaバイトコードはソースコードアノテーションやジェネリックの情報が削除されますが、実行に必要なソースコードの全ての情報を含んでいます。

Jarファイルとは

Jarファイルとは実行時のエントリーポイントなどメタ情報を記述したMETA-INF/MANIFEST.MFファイルを頂点にしてJavaバイトコードとプログラムが使用する画像などのリソースを1つのファイルに圧縮したものです。

JavaではアプリケーションはJarファイルとして外部に配布されます。JVMはアプリケーション本体や実行に必要なライブラリを複数のJarファイルの形で読み込みます。

Javaバイトコードを理解する方法

Javaバイトコードの内容はプログラムを作る上で理解しておくべきものではありませんが、実行される命令の数を意識でき、ハードウェア、ソフトウェア双方に理解が深まります。

これから作成するアセンブラはX64などの実際のアセンブラにも似ているので(スタックマシンとレジスタマシンで構造は異なります)アセンブラやCLR、LLVMなどの仮想化技術を使用した他の処理系を扱う際にも参考になります。

Javaバイトコードの内容を理解する

適当なJavaプログラムを作って(example.java)バイトコンパイルしてみます。この説明ではJDK11を使用します。

拡張子.classのファイルが生成されます。

テキストエディタで開くと、一部パスやオブジェクトの名前らしいものがありますが、大半は機械語です。(暗号化されているわけではありません。)

逆アセンブラを使う

ここままでは読みにくいので逆アセンブラをします。
$ javap -v example.class #結果が出力されます

今度はJar(example.jar)ファイルに含まれる多数のclassファイルをまとめて逆アセンブラします。

Javaバイトコードの冒頭を見てみる

内容を見てみます。クラス1つに1つのclassファイルが割り当てられます。バイトコードでは構造体の形をとっています。こちらの表に対応する箇所を説明します。javapコマンドでは一部表示されない値があります。

“magic”は最初のclassfileからCompiled fromまででclassファイルのメタ情報やチェックサムが書かれています。

“minor_version”、”major_version”が次に表示されています。使用しているJDKのバージョンによってこの値は異なります。

クラスの属性と継承

javapで表示される順番はバージョンによって異なりますが、”access_flags”にはクラス(インターフェイスやEnumなども)のアクセス権や継承されているか、などの情報が書かれています。

“this_class” 、”super_class” はクラス名と継承しているクラスの名前が参照されていて、次に説明するConstant pool に名前が書かれています。何も継承していない場合”super_class”はObjectです。

メソッドとフィールド変数そしてインターフェイス

“interfaces_count”はそれぞれ実装しているインターフェイスの数です。”fields_count”は クラスやインターフェイス内で定義されたフィールド変数の数です。”methods_count”は定義されたメソッドの数です。

interface[]、fields[]、methods[]はそれぞれ定義された内容を格納した配列で要素数は・_countで示した数です。こちらは後に説明します。

Constant poolとは

Constant poolにはプログラムに存在するオブジェクトの属するクラスやインターフェイスやメソッドやフィールド変数の名前が配列として格納されて、classファイル内部で参照されています。一方局所変数の名前はこの中にはありません。

またプログラム内で使用されている値の定義されている変数の値も格納されています。doubleやStringは格納されていますが、intは格納されていません。

配列やオブジェクトはどのようにエントリーされるのか

配列でもint型では格納されず、doubleでは要素の値が格納されています。一般的にサイズの小さいbyte、char、int型は後述するmethod配列内のメソッドの定義の中で使用するたびに新しく作られています。

一般のクラスのインスタンスではインスタンスの属するクラス名だけがConstant poolに格納されています。

javapでは表示されていませんがConstant poolの要素数はconstant_pool_countで定義されています。

配列に何が格納されているか

interfaces配列は実装しているインターフェイスのConstant pool内でのインデックスが格納されています。javapでは表示されません。

field配列には定義されたフィールド変数の型とアクセス権やstaticなどの属性が格納されています。

method配列はコンストラクタを含むメソッドの内容が格納されているJavaバイトコードの目玉です。javapではJVMのオペコードの列と引数で表示されています。

method_infoの冒頭を読んで見る

まず冒頭にメソッドの定義が表示されます。

JVMのアセンブラ

次に

のようなJVMのアセンブラが続きます。これは整数を足し算して結果を返すだけのJVMのアセンブラコードです。iload_1,iload_2, iadd, ireturnはこれから説明するJVMのオペコードです。 詳細な記述例は公式サイトを参考にして下さい。

上の例では左側の数字はメソッドの始まりから何バイト目かを示します。オペコードの意味を理解するためにJVMの大まかな処理について説明します。

スタックマシンについて

JVMはスタックマシンと呼ばれ命令の実行はスタックを介して行われます。通常我々が使用しているx64やarmなどはレジスタを介して演算を行うレジスタマシンとは異なります。

レジスタマシンでは複数個のハードウェアに配置が依存するレジスタをオペランドとして指定しなければならないのに対して、JVMでの演算は次に実行する命令を指すPCカウンタとスタックへのPushとPopの操作で完結するためハードウェアによらずアセンブラ言語を書くことができます。

JVMの内部構造

JVMは起動するとロードされているクラスのConstant Pool、クラス、メソッドなどの(Javapで表示されるような)情報をJVMのヒープ領域の一部であるMethod Areaと呼ばれる領域に書き込みます。JVM内部のメモリの配置は、公式サイト2.5~2.6を参考にすると良いでしょう。

またインスタンスや配列が生成されるとヒープ領域に格納され、参照されないインスタンスはGCによって削除されます。MethodAreaの情報は参照されなくてもGCによって削除されることはありません。またヒープ領域、MethodAreaは全てのスレッドから参照されます。

スタック領域とは

JVMにはスレッドごとにJVMスタックが割り当てられメソッドが呼ばれるごとに新しいフレームがPushされます。フレーム内は局所変数を格納する配列と、メソッド内の計算のために使用されるオペランドスタックで構成されます。またPCカウンタがスレッド毎に生成されます。

その他C言語などで書かれたネイティブメソッドを実行するためのNative Method StacksもJVMスタックとは異なる領域に確保されています。なおネイティブメソッドの実行の際はPCカウンタの値は定義されません。

Javaオペコードで書かれたメソッド

上記のJVMのアセンブリコードは、addメソッドが呼ばれるとJVMスタックにフレームを生成し、iload1、iload2でフレーム内の局所変数を格納する配列の1番目と2番目から引数を取り出し、オペランドスタックに順次Pushします。

そしてiaddでPopした変数yの値を変数xに加算します。そしてireturnでオペランドスタックの値をPopして、JVMスタックの下の呼び出し元のフレームに値を返します。

全てのJVMのオペコードの詳細を知りたい方は公式サイトで調べてみて下さい。

プログラムの実行

最終的にこのような形式で書かれたバイトコードを、インタープリタで実行しながら頻繁に実行される部分を実機の機械語に最適化してコンパイルして実行します。

通常のインタープリタ言語とは異なりJavaバイトコードは型が決まっていますので、実行時にインタープリタ内部のメモリに割り当てる変数の型を決定する時間が無くなるのでインタープリタで実行した場合も動的なインタープリタ言語より高速になります。

classファイルをソースコードに逆コンパイルする

Javaバイトコードはジェネリック表記など一部コンパイル時に無くなる情報もありますが、実行に必要な大部分の情報が格納されているためソースコードを復元させることができます。サードパーティー製の逆コンパイルするためのツールを紹介します。

「JADX」はGUIとCUI版が用意されています。扱えるファイルの種類が多くclass, jarに加えapkファイルも逆コンパイル可能です。「jd-cli」 は簡単にclassファイルやJarファイルの逆コンパイルができます。

また、「JD-GUI」はGUIの操作で簡単に逆コンパイルができるようになっています。

jd-cliを使って逆コンパイルをしてみる

jd-cliは簡単にclassファイルもJarファイルも逆コンパイルできます。ここではJarファイル(example.jar)を逆コンパイルします。

SE
Javaバイトコードを理解するためには、JVMについての知識も必要なのですね。
PM
JavaバイトコードはJavaの中でも少々複雑なコードなので、この記事を参考に学んでいきましょう。

最後に

この記事ではJavaバイトコードがどのように使用され、どのように記述されているか、またJVMがそれをどのように解釈するかを中心に説明してきました。

JITコンパイルや仮想マシンを用いたコンパイル技術は近年の主流となりつつあります。従って、他の処理系の動作を理解する上でもJVMの概観を把握することは重要です。この記事を読んで、Javaバイトコードを理解し活用していきましょう。


Javaでのキャリアアップをお考えの方は、現在募集中の求人情報をご覧ください。

また、直接のエントリーも受け付けております。

エントリー(応募フォーム)

Search

Popular

recommended

Categories

Tags