Javaバイトコードの基礎

Java開発者は通常、仮想マシンで実行されているバイトコードについて知る必要はありませんが、最新のフレームワーク、コンパイラー、さらにはJavaツールを開発する人は、バイトコードを理解する必要があります。独自の目的のため。 ASM、cglib、Javassistなどの特別なライブラリがバイトコードの使用に役立つという事実にもかかわらず、これらのライブラリを効果的に使用するには基本を理解する必要があります。
この記事では、このトピックをさらに掘り下げる上で構築できる非常に基本的なことを説明します(約Per。)。

簡単な例から始めましょう。つまり、1つのフィールドとそのゲッターとセッターを持つPOJOです。
public class Foo { private String bar; public String getBar(){ return bar; } public void setBar(String bar) { this.bar = bar; } } 

javac Foo.javaコマンドを使用してクラスをコンパイルすると、バイトコードを含むFoo.classファイルが表示されます。 HEXエディターでのコンテンツの外観は次のとおりです。

画像

16進数(バイト)の各ペアは、オペコード(ニーモニック)に変換されます。 これをバイナリ形式で読み取ろうとするのは残酷です。 ニーモニック表現に移りましょう。

javap -c Fooコマンドはバイトコードを出力します。
 public class Foo extends java.lang.Object { public Foo(); Code: 0: aload_0 1: invokespecial #1; //Method java/lang/Object."<init>":()V 4: return public java.lang.String getBar(); Code: 0: aload_0 1: getfield #2; //Field bar:Ljava/lang/String; 4: areturn public void setBar(java.lang.String); Code: 0: aload_0 1: aload_1 2: putfield #2; //Field bar:Ljava/lang/String; 5: return } 


クラスは非常に単純なので、ソースコードと生成されたバイトコードの関係を簡単に確認できます。 まず、クラスのバイトコードバージョンでは、コンパイラーがデフォルトのコンストラクター(JVM仕様で記述されている)を呼び出していることがわかります。

さらに、バイトコード命令(aload_0とaload_1があります)を調べると、それらの一部にはaload_0やistore_2のようなプレフィックスがあることがわかります。 これは、命令が動作するデータのタイプを指します。 接頭辞「a」は、オペコードがオブジェクトへの参照を制御することを意味します。 「I」はそれぞれ整数を制御します。

ここで興味深い点は、一部の命令が実際にクラス定数のプールを参照するタイプ#1および#2の奇妙なオペランドで動作することです。 クラスファイルを詳しく見てみましょう。 javap -c -s -verbose(-sは署名を表示し、-verboseは詳細な出力を表示)を実行します
 Compiled from "Foo.java" public class Foo extends java.lang.Object SourceFile: "Foo.java" minor version: 0 major version: 50 Constant pool: const #1 = Method #4.#17; // java/lang/Object."":()V const #2 = Field #3.#18; // Foo.bar:Ljava/lang/String; const #3 = class #19; // Foo const #4 = class #20; // java/lang/Object const #5 = Asciz bar; const #6 = Asciz Ljava/lang/String;; const #7 = Asciz ; const #8 = Asciz ()V; const #9 = Asciz Code; const #10 = Asciz LineNumberTable; const #11 = Asciz getBar; const #12 = Asciz ()Ljava/lang/String;; const #13 = Asciz setBar; const #14 = Asciz (Ljava/lang/String;)V; const #15 = Asciz SourceFile; const #16 = Asciz Foo.java; const #17 = NameAndType #7:#8;// "":()V const #18 = NameAndType #5:#6;// bar:Ljava/lang/String; const #19 = Asciz Foo; const #20 = Asciz java/lang/Object; { public Foo(); Signature: ()V Code: Stack=1, Locals=1, Args_size=1 0: aload_0 1: invokespecial #1; //Method java/lang/Object."":()V 4: return LineNumberTable: line 1: 0 public java.lang.String getBar(); Signature: ()Ljava/lang/String; Code: Stack=1, Locals=1, Args_size=1 0: aload_0 1: getfield #2; //Field bar:Ljava/lang/String; 4: areturn LineNumberTable: line 5: 0 public void setBar(java.lang.String); Signature: (Ljava/lang/String;)V Code: Stack=2, Locals=2, Args_size=2 0: aload_0 1: aload_1 2: putfield #2; //Field bar:Ljava/lang/String; 5: return LineNumberTable: line 8: 0 line 9: 5 } 

ここで、それらがどのような奇妙なオペランドであるかがわかります。 たとえば、#2:

const#2 =フィールド#3。#18; // Foo.bar:Ljava/lang/String;

以下を参照します:

const#3 =クラス#19; // foo
const#18 = NameAndType#5:#6; // bar:Ljava / lang / String;

などなど。

各オペレーションコードには番号(0:aload_0)のラベルが付いていることに注意してください。 これは、フレーム内の命令の位置を示しています。これが何を意味するのかをさらに説明します。

バイトコードの仕組みを理解するには、実行モデルを見てください。 JVMは、スタックベースの実行モデルを使用します。 各スレッドには、フレームを含むJVMスタックがあります。 たとえば、デバッガでアプリケーションを実行すると、次のフレームが表示されます。
画像

メソッドが呼び出されるたびに、新しいフレームが作成されます。 フレームは、オペランドスタック、ローカル変数の配列、および実行されたメソッドのクラスの定数のプールへのリンクで構成されます。
画像

ローカル変数の配列のサイズは、ローカル変数とメソッドのパラメーターの数とサイズに応じて、コンパイル時に決定されます。 オペランドのスタック-スタック内の値を書き込みおよび削除するためのLIFOスタック。 サイズもコンパイル時に決定されます。 いくつかのオペコードはスタックに値を追加し、他のオペコードはスタックからオペランドを取得し、その状態を変更してスタックに返します。 オペランドスタックは、(戻り値)メソッドによって返される値を取得するためにも使用されます。
 public String getBar(){ return bar; } public java.lang.String getBar(); Code: 0: aload_0 1: getfield #2; //Field bar:Ljava/lang/String; 4: areturn 

このメソッドのバイトコードは3つのオペコードで構成されています。 最初のオペコードaload_0は、ローカル変数テーブルからインデックス0の値をスタックにプッシュします。 コンストラクターとインスタンスメソッドのローカル変数のテーブルにあるthis参照には、常に0のインデックスがあります。次のオペコードgetfieldは、オブジェクトフィールドを取得します。 最後のステートメントareturnは、メソッドから参照を返します。

各メソッドには、対応するバイトコード配列があります。 16進エディタで.classファイルの内容を見ると、バイトコード配列に次の値が表示されます。

画像

したがって、getBarメソッドのバイトコードは2A B4 00 02 B0です。 2Aはaload_0を指し、B0は戻りを指します。 メソッドのバイトコードに3つの命令があり、バイト配列に5つの要素があるのは奇妙に思えるかもしれません。 これは、getfield(B4)が2つのパラメーター(00 02)を必要とし、配列の位置2と3を占有するため、配列の5つの要素を占有するためです。 戻り命令は4桁シフトされます。
ローカル変数テーブル

ローカル変数で何が起こるかを説明するために、別の例を使用します。
 public class Example { public int plus(int a){ int b = 1; return a + b; } } 

ここには2つのローカル変数があります-メソッドパラメーターとローカル変数int b。 バイトコードは次のようになります。
 public int plus(int); Code: Stack=2, Locals=3, Args_size=2 0: iconst_1 1: istore_2 2: iload_1 3: iload_2 4: iadd 5: ireturn LineNumberTable: line 5: 0 line 6: 2 

LocalVariableTable:
開始の長さのスロット名の署名
0 6 0このLExample;
0 6 1 a I
2 4 2 b I

このメソッドは、iconst_1で定数1をロードし、istore_2でローカル変数2に入れます。 現在、ローカル変数テーブルでは、スロット2が予想どおり変数bで占有されています。 次に、iload_1は値をスタックにロードし、iload_2はbの値をロードします。 iaddは、スタックから2つのオペランドをポップし、それらを追加して、メソッドの値を返します。
例外処理

try-catch-finallyコンストラクトなどの例外処理の場合にバイトコードを取得する方法の興味深い例。
 public class ExceptionExample { public void foo(){ try { tryMethod(); } catch (Exception e) { catchMethod(); }finally{ finallyMethod(); } } private void tryMethod() throws Exception{} private void catchMethod() {} private void finallyMethod(){} } 

foo()メソッドのバイトコード:
 public void foo(); Code: 0: aload_0 1: invokespecial #2; //Method tryMethod:()V 4: aload_0 5: invokespecial #3; //Method finallyMethod:()V 8: goto 30 11: astore_1 12: aload_0 13: invokespecial #5; //Method catchMethod:()V 16: aload_0 17: invokespecial #3; //Method finallyMethod:()V 20: goto 30 23: astore_2 24: aload_0 25: invokespecial #3; //Method finallyMethod:()V 28: aload_2 29: athrow 30: return Exception table: from to target type 0 4 11 Class java/lang/Exception 0 4 23 any 11 16 23 any 23 24 23 any 

コンパイラーは、try-catch-finally内で可能なすべてのスクリプトのコードを生成します。finallyMethod()ブロックは3回呼び出されます(!)。 tryブロックは、tryが存在しないようにコンパイルされ、最終的にマージされました:
0:aload_0
1:invokespecial#2; //メソッドtryMethod :()V
4:aload_0
5:特別な#3を呼び出します。 //メソッドfinallyMethod :()V
ブロックが実行されると、goto命令はreturnオペコードで30番目の位置に実行をスローします。

tryMethodが例外をスローした場合、例外テーブルから最初の適切な(内部)例外ハンドラーが選択されます。 例外の表から、例外キャッチの位置は11であることがわかります。

0 4 11クラスjava / lang /例外

これにより、catchMethod()およびfinallyMethod()に実行がスローされます。

11:astore_1
12:aload_0
13:invokespecial#5; // catchMethodメソッド:()V
16:aload_0
17:invokespecial#3; // finallyMethodメソッド:()V

実行中に別の例外がスローされた場合、例外テーブルの位置は23になります。

0 4 23任意
11 16 23任意
23 24 23任意

23から始まる手順:

23:astore_2
24:aload_0
25:invokespecial#3; //メソッドfinallyMethod :()V
28:aload_2
29:投げる
30:戻り

そのため、finallyMethod()はaload_2とathrowで未処理の例外をスローして実行されます。

おわりに

これらは、JVMバイトコード領域からのほんのわずかなポイントです。 ほとんどは、developerWorks Peter Haggarの記事Javaバイトコードからのものでした。バイトコードを理解することは、より良いプログラマーになります。 この記事は少し時代遅れですが、それでも関連性があります。 BCELユーザーガイドには、バイトコードの基本についての適切な説明が含まれているので、興味のある人には読んでおくことをお勧めします。 さらに、仮想マシンの仕様も有用な情報源になりますが、理解するのに役立つグラフィック素材がないため、読みやすくありません。

一般に、バイトコードの仕組みを理解することは、特にフレームワーク、JVM言語コンパイラ、またはその他のユーティリティを検討している人にとって、Javaプログラミングの知識を深める上で重要なポイントだと思います。

Source: https://habr.com/ru/post/J111456/


All Articles