隨着Java語言的不斷的發展,Java的應用場景慢慢被擴大,各類優雅解決問題的技術也不斷衍生,如AOP技術,清晰理解Java運行原理就顯得頗有必要,本篇文章重點講解Java字節碼相關知識。java
Java文件經過編譯器生成的是class字節碼文件,字節碼文件也有文件本身的格式,這裏不詳細展開,直接經過Java本身帶的工具查看一下。 首先咱們的測試類文件以下:segmentfault
public class Person {
public String name;
public int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
複製代碼
定義了一個Person類,裏面有name和age的屬性,編譯後生成Person.class文件,直接使用Java工具dump這個class文件,dump命令以下:數組
javap -v -p Person.class
複製代碼
dump生成的內容以下:bash
public class com.sec.resourceparse.Person
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #5.#27 // java/lang/Object."<init>":()V
#2 = Fieldref #4.#28 // com/sec/resourceparse/Person.name:Ljava/lang/String;
#3 = Fieldref #4.#29 // com/sec/resourceparse/Person.age:I
#4 = Class #30 // com/sec/resourceparse/Person
#5 = Class #31 // java/lang/Object
#6 = Utf8 name
#7 = Utf8 Ljava/lang/String;
#8 = Utf8 age
#9 = Utf8 I
#10 = Utf8 <init>
#11 = Utf8 ()V
#12 = Utf8 Code
#13 = Utf8 LineNumberTable
#14 = Utf8 LocalVariableTable
#15 = Utf8 this
#16 = Utf8 Lcom/sec/resourceparse/Person;
#17 = Utf8 getName
#18 = Utf8 ()Ljava/lang/String;
#19 = Utf8 setName
#20 = Utf8 (Ljava/lang/String;)V
#21 = Utf8 getAge
#22 = Utf8 ()I
#23 = Utf8 setAge
#24 = Utf8 (I)V
#25 = Utf8 SourceFile
#26 = Utf8 Person.java
#27 = NameAndType #10:#11 // "<init>":()V
#28 = NameAndType #6:#7 // name:Ljava/lang/String;
#29 = NameAndType #8:#9 // age:I
#30 = Utf8 com/sec/resourceparse/Person
#31 = Utf8 java/lang/Object
{
public java.lang.String name;
descriptor: Ljava/lang/String;
flags: ACC_PUBLIC
public int age;
descriptor: I
flags: ACC_PUBLIC
public com.sec.resourceparse.Person();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/sec/resourceparse/Person;
public java.lang.String getName();
descriptor: ()Ljava/lang/String;
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field name:Ljava/lang/String;
4: areturn
LineNumberTable:
line 9: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/sec/resourceparse/Person;
public void setName(java.lang.String);
descriptor: (Ljava/lang/String;)V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: putfield #2 // Field name:Ljava/lang/String;
5: return
LineNumberTable:
line 13: 0
line 14: 5
LocalVariableTable:
Start Length Slot Name Signature
0 6 0 this Lcom/sec/resourceparse/Person;
0 6 1 name Ljava/lang/String;
複製代碼
這裏截取了部份內容,先簡單看一下,首先是類信息的介紹:工具
public class com.sec.resourceparse.Person
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER
複製代碼
類名,編譯的JDK版本,以及訪問修飾符
而後字符串池:測試
Constant pool:
#1 = Methodref #5.#27 // java/lang/Object."<init>":()V
#2 = Fieldref #4.#28 // com/sec/resourceparse/Person.name:Ljava/lang/String;
#3 = Fieldref #4.#29 // com/sec/resourceparse/Person.age:I
#4 = Class #30 // com/sec/resourceparse/Person
#5 = Class #31 // java/lang/Object
#6 = Utf8 name
#7 = Utf8 Ljava/lang/String;
複製代碼
這裏包含整個類裏面的字符串,包含聲明的類信息,屬性等
最後是方法的信息:ui
public java.lang.String getName();
descriptor: ()Ljava/lang/String;
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field name:Ljava/lang/String;
4: areturn
LineNumberTable:
line 9: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/sec/resourceparse/Person;
複製代碼
這裏主要是方法名,訪問修飾符,以及操做棧執行流程信息
看完整個類的class文件,下面介紹字節碼相關的基礎知識。this
上述字節碼中類,屬性以及方法中均有flag信息,這個就是修飾符,在字節碼中類訪問修飾符及對應值以下所示:spa
標誌符名稱 | 標誌符值 | 釋義 |
---|---|---|
ACC_PUBLIC | 0x0001 | Public 類型 |
ACC_FINAL | 0x0010 | Final類型 |
ACC_SUPER | 0x0020 | 是否容許使用invokespecial字節碼指令的新語義 |
ACC_INTERFACE | 0x0200 | 接口修飾符 |
ACC_ABSTRACT | 0x0400 | abstract修飾符 |
ACC_SYNTHETIC | 0x1000 | 標誌這個類並不是由用戶代碼生成 |
ACC_ANNOTATION | 0x2000 | 註解修飾符 |
**ACC_ENUM | 0x400 | 枚舉修飾符 |
上面介紹的是類的訪問修飾符,那麼屬性以及方法的也是相似的,只是相對而言比較簡單,這裏就不繼續展開了。.net
JAVA中有基本類型,數組,以及對象,字節碼中對類型的表示有所區別,對照表以下所示:
類型 | 字節碼錶示 | 釋義 |
---|---|---|
byte | B | 字節 |
boolean | Z | bool |
char | C | 字符 |
short | S | 短整型 |
int | I | 整型 |
float | F | 浮點數 |
long | J | 長整型 |
double | D | 浮點數 |
void | V | 空返回值 |
類 | Ljava/lang/Object; | 對象類型 |
數組 | [] | [ |
其中類是以L開頭,中間是類路徑,最後以;結尾,上面的數組是單個數組,要結合其餘類型一塊兒使用,如int[]的字節碼是[I,int[][]的字節碼是[[I.
上面已經介紹了訪問修飾符以及JAVA字節碼中類型對照,下面講解一下方法的解析,拿上面的方法舉例,以下所示:
public java.lang.String getName();
descriptor: ()Ljava/lang/String;
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field name:Ljava/lang/String;
4: areturn
LineNumberTable:
line 9: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/sec/resourceparse/Person;
複製代碼
這裏簡單解釋一下,類方法最少有一個參數,這個參數就是類對象自己,至關於this關鍵字,並且下標是0。
上面已經介紹了字節碼相關的基礎知識,可是沒有詳細說明字節碼指令相關內容,本節就重點介紹字節碼指令內容,字節碼指令主要分爲以下幾類:
上面已經基本介紹完字節碼全部的內容了,這裏實戰講解方法操做流程。先記住下面這個點:
JAVA方法執行都是基於棧進行的,方法調用指令調用後都會出棧,若是方法有返回值,則將返回值壓棧
先分析一個簡單的:
public java.lang.String getName();
descriptor: ()Ljava/lang/String;
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field name:Ljava/lang/String;
4: areturn
LineNumberTable:
line 9: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/sec/resourceparse/Person;
複製代碼
1.aload_0:這裏是將第0個參數,壓棧,參數的類型是對象(前面分析過是this)
再介紹一個稍微複雜一點的列子:
public class Manager {
public static void main(String [] args) {
String resPath = "/Users/Desktop/resources.arsc";
FileInputStream ins = null;
ByteArrayOutputStream ous = null;
try {
ins = new FileInputStream(new File(resPath));
ous = new ByteArrayOutputStream();
int length = -1;
byte data[] = new byte[4 * 1024];
while ((length = ins.read(data)) != -1) {
ous.write(data, 0, length);
}
byte[] resData = ous.toByteArray();
ParseUtils.parseRes(resData);
} catch (Exception e) {
e.printStackTrace();
}
}
}
複製代碼
對應的字節碼以下所示:
public com.sec.resourceparse.Manager();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 7: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/sec/resourceparse/Manager;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=5, locals=7, args_size=1
0: ldc #2 // String /Users/Desktop/resources.arsc
2: astore_1
3: aconst_null
4: astore_2
5: aconst_null
6: astore_3
7: new #3 // class java/io/FileInputStream
10: dup
11: new #4 // class java/io/File
14: dup
15: aload_1
16: invokespecial #5 // Method java/io/File."<init>":(Ljava/lang/String;)V
19: invokespecial #6 // Method java/io/FileInputStream."<init>":(Ljava/io/File;)V
22: astore_2
23: new #7 // class java/io/ByteArrayOutputStream
26: dup
27: invokespecial #8 // Method java/io/ByteArrayOutputStream."<init>":()V
30: astore_3
31: iconst_m1
32: istore 4
34: sipush 4096
37: newarray byte
39: astore 5
41: aload_2
42: aload 5
44: invokevirtual #9 // Method java/io/FileInputStream.read:([B)I
47: dup
48: istore 4
50: iconst_m1
51: if_icmpeq 66
54: aload_3
55: aload 5
57: iconst_0
58: iload 4
60: invokevirtual #10 // Method java/io/ByteArrayOutputStream.write:([BII)V
63: goto 41
66: aload_3
67: invokevirtual #11 // Method java/io/ByteArrayOutputStream.toByteArray:()[B
70: astore 6
72: aload 6
74: invokestatic #12 // Method com/sec/resourceparse/ParseUtils.parseRes:([B)V
77: goto 87
80: astore 4
82: aload 4
84: invokevirtual #14 // Method java/lang/Exception.printStackTrace:()V
87: return
複製代碼
這個Manager中只聲明瞭一個static的main方法,可是字節碼中有一個init的方法,其實就是默認的無參構造方法,先看一下這個方法的字節碼:
public com.sec.resourceparse.Manager();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 7: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/sec/resourceparse/Manager;
複製代碼
1.aload_0:將this對象壓入棧中
2.invokespecial:調用棧頂對象的特殊方法init方法,因爲init的返回值類型爲V,因此調用後棧頂就爲空
3.return:因爲棧頂沒有值,因此直接執行return指令就能夠了
再重點看下另一個方法:
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=5, locals=7, args_size=1
0: ldc #2 // String /Users/Desktop/resources.arsc
2: astore_1
3: aconst_null
4: astore_2
5: aconst_null
6: astore_3
7: new #3 // class java/io/FileInputStream
10: dup
11: new #4 // class java/io/File
14: dup
15: aload_1
16: invokespecial #5 // Method java/io/File."<init>":(Ljava/lang/String;)V
19: invokespecial #6 // Method java/io/FileInputStream."<init>":(Ljava/io/File;)V
22: astore_2
23: new #7 // class java/io/ByteArrayOutputStream
26: dup
27: invokespecial #8 // Method java/io/ByteArrayOutputStream."<init>":()V
30: astore_3
31: iconst_m1
32: istore 4
34: sipush 4096
37: newarray byte
39: astore 5
41: aload_2
42: aload 5
44: invokevirtual #9 // Method java/io/FileInputStream.read:([B)I
47: dup
48: istore 4
50: iconst_m1
51: if_icmpeq 66
54: aload_3
55: aload 5
57: iconst_0
58: iload 4
60: invokevirtual #10 // Method java/io/ByteArrayOutputStream.write:([BII)V
63: goto 41
66: aload_3
67: invokevirtual #11 // Method java/io/ByteArrayOutputStream.toByteArray:()[B
70: astore 6
72: aload 6
74: invokestatic #12 // Method com/sec/resourceparse/ParseUtils.parseRes:([B)V
77: goto 87
80: astore 4
82: aload 4
84: invokevirtual #14 // Method java/lang/Exception.printStackTrace:()V
87: return
複製代碼
方法解釋:
堆棧操做解釋:
0:壓棧一個String類型的對象,值爲:"/Users/Desktop/resources.arsc"
上面操做結束後,方法棧和局部變量以下所示:
7-30:
31-42
上面邏輯基本就這樣分析,這個方法比較長,就不繼續向下分析,都是同樣的步驟
操做棧流程的關鍵: 全部的操做都伴隨着壓棧和出棧的邏輯,如方法調用,使用到的在棧中的類和參數會被出棧,若是方法有返回值,則將返回值壓棧。
字節碼知識仍是比較重要的,理解字節碼知識能清晰的理解JVM運行機制,同時爲後面AOP直接操做字節碼打下基礎。
參考:
segmentfault.com/a/119000000… my.oschina.net/ta8210/blog…