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

- システム
エンジニア - Javaバイトコードとはどのようなようなものなのでしょうか。
- プロジェクト
マネージャー - 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を使用します。
1 |
$ javac example.java # $ java example.javaではバイトコンパイルした後実行されます。 |
拡張子.classのファイルが生成されます。
テキストエディタで開くと、一部パスやオブジェクトの名前らしいものがありますが、大半は機械語です。(暗号化されているわけではありません。)
逆アセンブラを使う
ここままでは読みにくいので逆アセンブラをします。
$ javap -v example.class #結果が出力されます
今度はJar(example.jar)ファイルに含まれる多数のclassファイルをまとめて逆アセンブラします。
1 |
$ zipinfo -1 example.jar \*.class| sed 's/\.class//' | xargs javap -classpath example.jar -verbose > example.jasm #example.jasmという名前のファイルに結果が表示される |
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の冒頭を読んで見る
まず冒頭にメソッドの定義が表示されます。
1 2 3 4 5 6 7 8 9 |
public int add(int, int);#返り値と引数の型、public,privateなどの属性が表示されます。 descriptor: (II)I#(Constant poolに格納された引数と返り値のデータ型のエイリアスが入ります) flags: (0x0001) ACC_PUBLIC#(最初の行と同様にクラスの属性が表示されます) Code: stack=2, locals=3, args_size=3 # stackはメソッドの実行に必要なスタックの深さです。 # localsはローカル変数テーブルに予約する必要があるローカル変数スロットの数です。 # この処理で参照するパラメータの数です。 |
JVMのアセンブラ
次に
1 2 3 4 5 |
0: iload_1 #public int add(int x, int y) { 1: iload_2 #return x + y; 2: iadd #} 3: ireturn |
のような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)を逆コンパイルします。
1 2 3 4 5 6 7 |
$ curl -O “https://github.com/kwart/jd-cli/releases/download/jd-cmd-1.1.0.Final/jd-cli-1.1.0.Final-dist.zip” #jd-cliをダウウンロードする $ unzip unzip jd-cli-1.1.0.Final-dist.zip -d jd-cli #解凍する Jarファイルを逆コンパイルする $ java jd-cli/bin/jd-cli example.jar -od (ソースコードを格納するディレクトリ)#復元されたソースコードがディレクトリ以下に作られます classファイル(example.class)を逆コンパイルする $ java jd-cli/bin/jd-cli example.class > example.src #example.srcにソースコードが作られます。 |
- システム
エンジニア - Javaバイトコードを理解するためには、JVMについての知識も必要なのですね。
- プロジェクト
マネージャー - JavaバイトコードはJavaの中でも少々複雑なコードなので、この記事を参考に学んでいきましょう。
最後に
この記事ではJavaバイトコードがどのように使用され、どのように記述されているか、またJVMがそれをどのように解釈するかを中心に説明してきました。
JITコンパイルや仮想マシンを用いたコンパイル技術は近年の主流となりつつあります。従って、他の処理系の動作を理解する上でもJVMの概観を把握することは重要です。この記事を読んで、Javaバイトコードを理解し活用していきましょう。
FEnetJava・Javaコラムは株式会社オープンアップシステムが運営しています。
株式会社オープンアップシステムはこんな会社です
秋葉原オフィスには株式会社オープンアップシステムをはじめグループのIT企業が集結!
数多くのエンジニアが集まります。

-
スマホアプリから業務系システムまで
スマホアプリから業務系システムまで開発案件多数。システムエンジニア・プログラマーとしての多彩なキャリアパスがあります。
-
充実した研修制度
毎年、IT技術のトレンドや社員の要望に合わせて、カリキュラムを刷新し展開しています。社内講師の丁寧なサポートを受けながら、自分のペースで学ぶことができます。
-
資格取得を応援
スキルアップしたい社員を応援するために資格取得一時金制度を設けています。受験料(実費)と合わせて資格レベルに合わせた最大10万円の一時金も支給しています。
-
東証プライム上場企業グループ
オープンアップシステムは東証プライム上場「株式会社オープンアップグループ」のグループ企業です。
安定した経営基盤とグループ間のスムーズな連携でコロナ禍でも安定した雇用を実現させています。
株式会社オープンアップシステムに興味を持った方へ
株式会社オープンアップシステムでは、開発系エンジニア・プログラマを募集しています。
年収をアップしたい!スキルアップしたい!大手の上流案件にチャレンジしたい!
まずは話だけでも聞いてみたい場合もOK。お気軽にご登録ください。


Java新着案件New Job
官公庁向け業務システム開発/Java/東京都千代田区/【WEB面談可】/テレワーク
月給39万~44万円東京都千代田区(永田町駅)販売管理システム開発/Java/東京都中央区/【WEB面談可】/テレワーク
月給49万~55万円東京都中央区(京橋駅)生命保険会社向けシステム開発/Java/東京都千代田区/【WEB面談可】/テレワーク
月給42万~48万円東京都千代田区(大手町駅)社会保険システムのパッケージ開発/Java/東京都港区/【WEB面談可】/テレワーク
月給42万~48万円東京都港区(新橋駅)金融機関向けシステム更改/Java/東京都江東区/【WEB面談可】/テレワーク
月給46万~51万円東京都江東区(豊洲駅)大手通信会社者向けWebシステム改修/Java/東京都港区/【WEB面談可】/テレワーク
月給42万~48万円東京都港区(品川駅)