打卡學習JVM,第六天
本人學習過程中所整理的代碼,源碼地址
- Java字節碼文件剖析
測試用例:
public class MyTest2 {
String str = "Welcome";
private int x = 5;
public static Integer in = 10;
public static void main(String[] args) {
MyTest2 myTest2 = new MyTest2();
myTest2.setX(0);
in = 20;
}
private void setX(int x) {
this.x = x;
}
}
- 使用java -verbose命令分析一個字節碼文件時,將會分析該字節碼文件的魔數、版本號、常量池、類信息、類的構造方法、類中的方法信息、類變量與成員變量等信息
Classfile /G:/GitHub/jvm-study/build/classes/java/main/bytecode/MyTest2.class
Last modified 2020-3-19; size 814 bytes
MD5 checksum baffcdf4a49b347cecf3122ddf8455e2
Compiled from "MyTest2.java"
public class bytecode.MyTest2
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #10.#34 // java/lang/Object."<init>":()V
#2 = String #35 // Welcome
#3 = Fieldref #5.#36 // bytecode/MyTest2.str:Ljava/lang/String;
#4 = Fieldref #5.#37 // bytecode/MyTest2.x:I
#5 = Class #38 // bytecode/MyTest2
#6 = Methodref #5.#34 // bytecode/MyTest2."<init>":()V
#7 = Methodref #5.#39 // bytecode/MyTest2.setX:(I)V
#8 = Methodref #40.#41 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
#9 = Fieldref #5.#42 // bytecode/MyTest2.in:Ljava/lang/Integer;
#10 = Class #43 // java/lang/Object
#11 = Utf8 str
#12 = Utf8 Ljava/lang/String;
#13 = Utf8 x
#14 = Utf8 I
#15 = Utf8 in
#16 = Utf8 Ljava/lang/Integer;
#17 = Utf8 <init>
#18 = Utf8 ()V
#19 = Utf8 Code
#20 = Utf8 LineNumberTable
#21 = Utf8 LocalVariableTable
#22 = Utf8 this
#23 = Utf8 Lbytecode/MyTest2;
#24 = Utf8 main
#25 = Utf8 ([Ljava/lang/String;)V
#26 = Utf8 args
#27 = Utf8 [Ljava/lang/String;
#28 = Utf8 myTest2
#29 = Utf8 setX
#30 = Utf8 (I)V
#31 = Utf8 <clinit>
#32 = Utf8 SourceFile
#33 = Utf8 MyTest2.java
#34 = NameAndType #17:#18 // "<init>":()V
#35 = Utf8 Welcome
#36 = NameAndType #11:#12 // str:Ljava/lang/String;
#37 = NameAndType #13:#14 // x:I
#38 = Utf8 bytecode/MyTest2
#39 = NameAndType #29:#30 // setX:(I)V
#40 = Class #44 // java/lang/Integer
#41 = NameAndType #45:#46 // valueOf:(I)Ljava/lang/Integer;
#42 = NameAndType #15:#16 // in:Ljava/lang/Integer;
#43 = Utf8 java/lang/Object
#44 = Utf8 java/lang/Integer
#45 = Utf8 valueOf
#46 = Utf8 (I)Ljava/lang/Integer;
{
java.lang.String str;
descriptor: Ljava/lang/String;
flags:
private int x;
descriptor: I
flags: ACC_PRIVATE
public static java.lang.Integer in;
descriptor: Ljava/lang/Integer;
flags: ACC_PUBLIC, ACC_STATIC
public bytecode.MyTest2();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: ldc #2 // String Welcome
7: putfield #3 // Field str:Ljava/lang/String;
10: aload_0
11: iconst_5
12: putfield #4 // Field x:I
15: return
LineNumberTable:
line 7: 0
line 9: 4
line 11: 10
LocalVariableTable:
Start Length Slot Name Signature
0 16 0 this Lbytecode/MyTest2;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #5 // class bytecode/MyTest2
3: dup
4: invokespecial #6 // Method "<init>":()V
7: astore_1
8: aload_1
9: iconst_0
10: invokespecial #7 // Method setX:(I)V
13: bipush 20
15: invokestatic #8 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
18: putstatic #9 // Field in:Ljava/lang/Integer;
21: return
LineNumberTable:
line 16: 0
line 18: 8
line 20: 13
line 21: 21
LocalVariableTable:
Start Length Slot Name Signature
0 22 0 args [Ljava/lang/String;
8 14 1 myTest2 Lbytecode/MyTest2;
private void setX(int);
descriptor: (I)V
flags: ACC_PRIVATE
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: iload_1
2: putfield #4 // Field x:I
5: return
LineNumberTable:
line 24: 0
line 25: 5
LocalVariableTable:
Start Length Slot Name Signature
0 6 0 this Lbytecode/MyTest2;
0 6 1 x I
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: bipush 10
2: invokestatic #8 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
5: putstatic #9 // Field in:Ljava/lang/Integer;
8: return
LineNumberTable:
line 13: 0
}
SourceFile: "MyTest2.java"
- 魔數:所有的.class字節碼文件的前4個字節都是魔數,魔數值爲固定值:0xCAFEBABE
- 魔數之後的4個字節爲版本信息,其中前兩個字節表示minor version(次版本號),後兩個字節表示major version(主版本號)
- 常量池(constant pool):緊接着主版本號之後的就是常量池入口。一個Java類中定義的很多信息都是由常量池來維護和描述的,可以將常量池看作是class文件的資源倉庫,比如說Java類中定義的方法與變量信息都是存儲在常量池中。常量池中主要存儲兩類常量:字面量與符號引用。字面量如文本字符串,Java中聲明爲final的常量值等; 符號引用如類和接口的全侷限定名,字段的名稱和描述符,方法的名稱和描述符等。
- 常量池的總體結構:Java類所對應的常量池主要由常量池數量與常量池數組(常量表)這兩部分共同構成。常量池緊跟在主版本後面,佔據2個字節;常量池數組則緊跟在常量池數量之後。常量池數組與一般的數組不同的是,常量池數組中不同的元素的類型、結構都是不同的;但是每一種元素的第一個數據都是一個u1類型,該字節是個標誌位,佔據1個字節。JVM在解析常量池時,會根據這個u1類型來獲取元素的具體類型。**值得注意的是,常量池數組中元素的個數=常量池數-1(索引從1開始),目的是滿足某些常量池索引值的數據在特定情況下需要表達不引用任何一個常量池的含義;**根本原因在於,索引爲0也是一個常量(保留常量),只不過它不位於常量表中,這個常量就對應null值,所以常量池的索引從1而非0開始。
- 在JVM規範中,每個變量/字段都有描述信息,描述信息主要的作用是描述字段中的數據類型、方法的參數列表(包括數量、類型與順序)與返回值。根據描述符規則,基本數據類型和代表無返回值的void類型都用一個大寫字符來表示,對象類型則使用字符L加對象的全限定名稱來表示。爲了壓縮字節碼文件的體積,對於基本數據類型,JVM都只是用一個大寫字母來表示,如下所示,如B-byte,C-char,D-double,F-float,I-int,J-long,S-short,Z-boolean,V-void,L-對象類型,如Ljava/lang/String;
- 對於數組類型,每一個維度使用一個前置的[來表示
- 用描述符描述方法時,按照先參數列表,後返回值的順序來描述。參數列表按照參數的嚴格順序放在一組()之內,如方法:String getRealnamebyIdAndNickname(int id,String name)的描述符爲:(I, Ljava/lang/String;) Ljava/lang/String
Java字節碼整體結構
Class字節碼中有兩種數據類型:
(1)字節數據直接量:這是基本的數據類型。共細分爲u1、u2、u4、u8四種,分別代表連續的1個字節、2個字節、4個字節、8個字節組成的整體數據。
(2)表/數組:表是由多個基本數據或其他表,按照既定順序組成的大的數據集合。表是有結構的,它的結構體:組成表的成分所在的位置和順序都是已經嚴格定義好的。
- 訪問標識符(Access Flags)
訪問標誌信息包括了該class文件是類還是接口,是否被定義成public,是否是abstract,如果是類,是否被定義成final
- 字段表(Fields)
字段表用於描述類和接口中聲明的變量。這裏的字段包含了類級別變量和實例變量,但是不包括方法內部聲明的局部變量
- 方法表(Methods)
方法的屬性結構:
方法中的每個屬性都是一個attribute_info結構:
- JVM預定義了部分attribute,但是編譯器自己也可以實現自己的attribute寫入class文件裏,供運行時使用
- 不同的attribute通過attribute_name_index來區分
//attribute_info格式:
attribute_info{
//attribute_name_index值爲code,則爲Code結構Code的作用是保存該方法的結構,所對應的的字節碼
u2 attribute_name_index;
//表示attribute所包含的字節數,不包含attribute_name_index和attribute_length
u4 attribute_length;
u1 info[attribute_length]
}
- 通過Java字節碼分析this關鍵字
- 對於Java類中的每一個實例方法(非static方法),在編譯後所生成的字節碼當中方法參數的數量總是會比源代碼種方法參數的數量多一個(this),它位於方法的第一個參數位置處;這樣,我們就可以在Java的實例方法中使用this去訪問當前對象的屬性以及其它方法
- 這個操作是在編譯期間完成的,即由javac編譯器在編譯的時候將對this的訪問轉化爲對一個普通實例方法參數的訪問,接下來在運行期間,由JVM在調用實例方法時,自動向實例方法傳入該this參數。所以,在實例方法的局部變量表中,至少會有一個指向當前對象的局部變量
- Java字節碼對於異常的處理方式
public class MyTest3 {
public void test() {
try {
FileInputStream fis = new FileInputStream("test.txt");
ServerSocket serverSocket = new ServerSocket(9999);
serverSocket.accept();
} catch (FileNotFoundException e) {
} catch (IOException e) {
} catch (Exception e) {
} finally {
System.out.println("finally");
}
}
}
- 統一採用異常表的方式來對一場進行處理
- 在jdk1.4.2之前的版本中,並不是使用異常表的方式來對異常進行處理的,而是採用特定的指令這種方式
- 當異常處理存在finally語句塊中,現代化的JVM採取的處理方法是將finally語句塊的字節碼拼接到每一個catch塊後面,換句話說,代碼中存在多少個catch塊,就會在每一個catch塊後面重複多少個finally語句塊的字節碼
0 new #2 <java/io/FileInputStream>
3 dup
4 ldc #3 <test.txt>
6 invokespecial #4 <java/io/FileInputStream.<init>>
9 astore_1
10 new #5 <java/net/ServerSocket>
13 dup
14 sipush 9999
17 invokespecial #6 <java/net/ServerSocket.<init>>
20 astore_2
21 aload_2
22 invokevirtual #7 <java/net/ServerSocket.accept>
25 pop
26 getstatic #8 <java/lang/System.out>
29 ldc #9 <finally>
31 invokevirtual #10 <java/io/PrintStream.println>
34 goto 92 (+58)
37 astore_1
38 aload_1
39 invokevirtual #12 <java/io/FileNotFoundException.printStackTrace>
42 getstatic #8 <java/lang/System.out>
45 ldc #9 <finally>
47 invokevirtual #10 <java/io/PrintStream.println>
50 goto 92 (+42)
53 astore_1
54 aload_1
55 invokevirtual #14 <java/io/IOException.printStackTrace>
58 getstatic #8 <java/lang/System.out>
61 ldc #9 <finally>
63 invokevirtual #10 <java/io/PrintStream.println> //第一次出現
66 goto 92 (+26)
69 astore_1
70 getstatic #8 <java/lang/System.out>
73 ldc #9 <finally>
75 invokevirtual #10 <java/io/PrintStream.println> //第二次出現
78 goto 92 (+14)
81 astore_3
82 getstatic #8 <java/lang/System.out>
85 ldc #9 <finally>
87 invokevirtual #10 <java/io/PrintStream.println> //第三次出現
90 aload_3
91 athrow
92 return