目錄
- 1.入門
- 2 javap 工具
- 3 圖解方法執行流程
- 3.1.原始 java 代碼
- 3.2.編譯后的位元組碼檔案
- 3.3.常量池載入運行時常量池
- 3.4.方法位元組碼載入方法區
- 3.5.main 執行緒開始運行,分配堆疊幀記憶體
- 3.6.執行引擎開始執行位元組碼
- 4 練習 - 分析 i++
- 5.條件判斷
- 6.回圈控制指令
- 7 練習 - 判斷結果
- 8 構造方法
- 9 方法呼叫
- 10.多型的原理
- 11.例外處理
- 11.1.try-catch
- 11.2.多個single-catch
- 11.3.finally
- 11.4.finally面試題
- 11.4.1.finally中的return
- 11.4.2.被吞掉的例外
- 11.4.3.finally不帶return
- 12.synchronized
1.入門
接著上一節類檔案結構,研究一下兩組位元組碼指令,一個是public cn.itcast.jvm.t5.HelloWorld(); 構造方法的位元組碼指令
2a b7 00 01 b1
它實際上對應位元組碼的指令,java虛擬機內部有解釋器,解釋器會識別這些平臺無關的位元組碼指令,把它們最終解釋為機器碼,然后執行,
那么怎么知道機器碼對應的位元組碼指令呢,
請參考
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5
查找0x2a

0x2a aload_0
b7 invokespecial
b1 return
- 2a => aload_0 加載 slot 0 的區域變數,即 this,做為下面的 invokespecial 構造方法呼叫的引數 (把區域變數表中0號槽位的變數加載到運算元堆疊上)
- b7 => invokespecial 預備呼叫構造方法,哪個方法呢?(準備進行方法的呼叫)
- 00 01 參考常量池中 #1 項,即【 Method java/lang/Object." ": () V 】
- b1 表示回傳
所以這個是通過this呼叫了父類的無參構造方法,最后b1是方法執行了要回傳,
另一個是 public static void main(java.lang.String[]); 主方法的位元組碼指令
b2 00 02 12 03 b6 00 04 b1
- b2 => getstatic 用來加載靜態變數,哪個靜態變數呢?
- 00 02 參考常量池中 #2 項,即【Field java/lang/System.out:Ljava/io/PrintStream;】
- 12 => ldc 加載引數,哪個引數呢?
- 03 參考常量池中 #3 項,即 【String hello world】
- b6 => invokevirtual 預備呼叫成員方法,哪個方法呢?
- 00 04 參考常量池中 #4 項,即【Method java/io/PrintStream.println:(Ljava/lang/String;)V】
- b1 表示回傳
注意,這里位元組碼是先準備引數,再呼叫方法,
2 javap 工具
自己分析類檔案結構太麻煩了,Oracle 提供了 javap 工具來反編譯 class 檔案
使用IDEA反編譯
F:\IDEA\projects\jvm>javap -v F:\IDEA\projects\jvm\out\production\untitled\cn\yj\jvm\HelloWorld.class
Classfile /F:/IDEA/projects/jvm/out/production/untitled/cn/yj/jvm/HelloWorld.class
Last modified 2021-2-2; size 553 bytes
MD5 checksum 6b7033e0eab7845f9c8aa7b8e1f2d44f
Compiled from "HelloWorld.java"
public class cn.yj.jvm.HelloWorld
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#20 // java/lang/Object."<init>":()V
#2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #23 // hello world
#4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #26 // cn/yj/jvm/HelloWorld
#6 = Class #27 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcn/yj/jvm/HelloWorld;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 args
#17 = Utf8 [Ljava/lang/String;
#18 = Utf8 SourceFile
#19 = Utf8 HelloWorld.java
#20 = NameAndType #7:#8 // "<init>":()V
#21 = Class #28 // java/lang/System
#22 = NameAndType #29:#30 // out:Ljava/io/PrintStream;
#23 = Utf8 hello world
#24 = Class #31 // java/io/PrintStream
#25 = NameAndType #32:#33 // println:(Ljava/lang/String;)V
#26 = Utf8 cn/yj/jvm/HelloWorld
#27 = Utf8 java/lang/Object
#28 = Utf8 java/lang/System
#29 = Utf8 out
#30 = Utf8 Ljava/io/PrintStream;
#31 = Utf8 java/io/PrintStream
#32 = Utf8 println
#33 = Utf8 (Ljava/lang/String;)V
{
public cn.yj.jvm.HelloWorld();
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 2: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcn/yj/jvm/HelloWorld;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String hello world
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 4: 0
line 5: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
}
SourceFile: "HelloWorld.java"
3 圖解方法執行流程
3.1.原始 java 代碼
package cn.yj.jvm;
/** * 演示 位元組碼指令 和 運算元堆疊、常量池的關系 */
public class Demo3_1 {
public static void main(String[] args)
{
int a = 10;
int b = Short.MAX_VALUE + 1;
int c = a + b;
System.out.println(c);
}
}
3.2.編譯后的位元組碼檔案
F:\IDEA\projects\jvm>javap -v F:\IDEA\projects\jvm\out\production\untitled\cn\yj\jvm\Demo3_1.class
Classfile /F:/IDEA/projects/jvm/out/production/untitled/cn/yj/jvm/Demo3_1.class
Last modified 2021-2-2; size 603 bytes
MD5 checksum 9bdbe178a29e07556915f368dbf7def1
Compiled from "Demo3_1.java"
public class cn.yj.jvm.Demo3_1
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #7.#25 // java/lang/Object."<init>":()V
#2 = Class #26 // java/lang/Short
#3 = Integer 32768
#4 = Fieldref #27.#28 // java/lang/System.out:Ljava/io/PrintStream;
#5 = Methodref #29.#30 // java/io/PrintStream.println:(I)V
#6 = Class #31 // cn/yj/jvm/Demo3_1
#7 = Class #32 // java/lang/Object
#8 = Utf8 <init>
#9 = Utf8 ()V
#10 = Utf8 Code
#11 = Utf8 LineNumberTable
#12 = Utf8 LocalVariableTable
#13 = Utf8 this
#14 = Utf8 Lcn/yj/jvm/Demo3_1;
#15 = Utf8 main
#16 = Utf8 ([Ljava/lang/String;)V
#17 = Utf8 args
#18 = Utf8 [Ljava/lang/String;
#19 = Utf8 a
#20 = Utf8 I
#21 = Utf8 b
#22 = Utf8 c
#23 = Utf8 SourceFile
#24 = Utf8 Demo3_1.java
#25 = NameAndType #8:#9 // "<init>":()V
#26 = Utf8 java/lang/Short
#27 = Class #33 // java/lang/System
#28 = NameAndType #34:#35 // out:Ljava/io/PrintStream;
#29 = Class #36 // java/io/PrintStream
#30 = NameAndType #37:#38 // println:(I)V
#31 = Utf8 cn/yj/jvm/Demo3_1
#32 = Utf8 java/lang/Object
#33 = Utf8 java/lang/System
#34 = Utf8 out
#35 = Utf8 Ljava/io/PrintStream;
#36 = Utf8 java/io/PrintStream
#37 = Utf8 println
#38 = Utf8 (I)V
{
public cn.yj.jvm.Demo3_1();
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 4: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcn/yj/jvm/Demo3_1;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: bipush 10
2: istore_1
3: ldc #3 // int 32768
5: istore_2
6: iload_1
7: iload_2
8: iadd
9: istore_3
10: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
13: iload_3
14: invokevirtual #5 // Method java/io/PrintStream.println:(I)V
17: return
LineNumberTable:
line 7: 0
line 8: 3
line 9: 6
line 10: 10
line 11: 17
LocalVariableTable:
Start Length Slot Name Signature
0 18 0 args [Ljava/lang/String;
3 15 1 a I
6 12 2 b I
10 8 3 c I
}
SourceFile: "Demo3_1.java"
3.3.常量池載入運行時常量池
當我們java代碼被執行時,它會由java虛擬機的類加載器把我們main方法所在的類進行類加載的操作,類加載實際上把這些位元組的class的資料讀取到記憶體里,常量池的資料被放入運行時常量池,運行時常量池屬于方法區的組成部分,只是因為相對比較特殊,其實就是把class檔案中的資料存入到運行時常量池的地方,將來找其中一些常量池資訊就到運行時常量池中找,
如圖只列出了3,4,5幾項,第3項是原碼中的int b = Short.MAX_VALUE + 1;而int a=10;比較小的數字與方法位元組碼存盤在一起,不存在常量池中,一旦超過了Short整數的最大值的范圍,就存到常量池中,
常量池也屬于方法區,只不過這里單獨提出來了

3.4.方法位元組碼載入方法區

3.5.main 執行緒開始運行,分配堆疊幀記憶體
(stack=2,locals=4) 對應運算元堆疊有2個空間(每個空間4個位元組),區域變數表中有4個槽位

3.6.執行引擎開始執行位元組碼
bipush 10
- 將一個 byte 壓入運算元堆疊(其長度會補齊 4 個位元組),類似的指令還有
- sipush 將一個 short 壓入運算元堆疊(其長度會補齊 4 個位元組)
- ldc 將一個 int 壓入運算元堆疊
- ldc2_w 將一個 long 壓入運算元堆疊(分兩次壓入,因為 long 是 8 個位元組)
- 這里小的數字都是和位元組碼指令存在一起,超過 short 范圍的數字存入了常量池

istore 1
將運算元堆疊堆疊頂元素彈出,放入區域變數表的slot 1中0
對應代碼中的
a = 10


ldc #3
讀取運行時常量池中#3,即32768(超過short最大值范圍的數會被放到運行時常量池中),將其加載到運算元堆疊中
注意 Short.MAX_VALUE 是 32767,所以 32768 = Short.MAX_VALUE + 1 實際是在編譯期間計算好的

istore 2
將運算元堆疊中的元素彈出,放到區域變數表的2號位置


iload_1 iload_2
將區域變數表中1號位置和2號位置的元素放入運算元堆疊中
因為只能在運算元堆疊中執行運算操作


iadd
將運算元堆疊中的兩個元素彈出堆疊并相加,結果在壓入運算元堆疊中


istore 3
將運算元堆疊中的元素彈出,放入區域變數表的3號位置


getstatic #4
在運行時常量池中找到#4,發現是一個物件
在堆記憶體中找到該物件,并將其參考放入運算元堆疊中


iload 3
將區域變數表中3號位置的元素壓入運算元堆疊中


invokevirtual 5
找到常量池 #5 項,定位到方法區 java/io/PrintStream.println:(I)V 方法
生成新的堆疊幀(分配 locals、stack等)
傳遞引數,執行新堆疊幀中的位元組碼

執行完畢,彈出堆疊幀
清除 main 運算元堆疊內容

return
完成 main 方法呼叫,彈出 main 堆疊幀
程式結束
4 練習 - 分析 i++
目的:從位元組碼角度分析 a++ 相關題目
原始碼:
package cn.yj.jvm;
/** * 從位元組碼角度分析 a++ 相關題目 */ public class Demo3_2 {
public static void main(String[] args) {
int a = 10;
int b = a++ + ++a + a--;
System.out.println(a);
System.out.println(b);
}
}
位元組碼:
F:\IDEA\projects\jvm>javap -v F:\IDEA\projects\jvm\out\production\untitled\cn\yj\jvm\Demo3_2.class
Classfile /F:/IDEA/projects/jvm/out/production/untitled/cn/yj/jvm/Demo3_2.class
Last modified 2021-2-3; size 578 bytes
MD5 checksum c5e9d3ebbd57d36a03305a1c3a5d9b4c
Compiled from "Demo3_2.java"
public class cn.yj.jvm.Demo3_2
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #5.#22 // java/lang/Object."<init>":()V
#2 = Fieldref #23.#24 // java/lang/System.out:Ljava/io/PrintStream;
#3 = Methodref #25.#26 // java/io/PrintStream.println:(I)V
#4 = Class #27 // cn/yj/jvm/Demo3_2
#5 = Class #28 // java/lang/Object
#6 = Utf8 <init>
#7 = Utf8 ()V
#8 = Utf8 Code
#9 = Utf8 LineNumberTable
#10 = Utf8 LocalVariableTable
#11 = Utf8 this
#12 = Utf8 Lcn/yj/jvm/Demo3_2;
#13 = Utf8 main
#14 = Utf8 ([Ljava/lang/String;)V
#15 = Utf8 args
#16 = Utf8 [Ljava/lang/String;
#17 = Utf8 a
#18 = Utf8 I
#19 = Utf8 b
#20 = Utf8 SourceFile
#21 = Utf8 Demo3_2.java
#22 = NameAndType #6:#7 // "<init>":()V
#23 = Class #29 // java/lang/System
#24 = NameAndType #30:#31 // out:Ljava/io/PrintStream;
#25 = Class #32 // java/io/PrintStream
#26 = NameAndType #33:#34 // println:(I)V
#27 = Utf8 cn/yj/jvm/Demo3_2
#28 = Utf8 java/lang/Object
#29 = Utf8 java/lang/System
#30 = Utf8 out
#31 = Utf8 Ljava/io/PrintStream;
#32 = Utf8 java/io/PrintStream
#33 = Utf8 println
#34 = Utf8 (I)V
{
public cn.yj.jvm.Demo3_2();
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 Lcn/yj/jvm/Demo3_2;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: bipush 10
2: istore_1
3: iload_1
4: iinc 1, 1
7: iinc 1, 1
10: iload_1
11: iadd
12: iload_1
13: iinc 1, -1
16: iadd
17: istore_2
18: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
21: iload_1
22: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
25: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
28: iload_2
29: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
32: return
LineNumberTable:
line 6: 0
line 7: 3
line 8: 18
line 9: 25
line 10: 32
LocalVariableTable:
Start Length Slot Name Signature
0 33 0 args [Ljava/lang/String;
3 30 1 a I
18 15 2 b I
}
SourceFile: "Demo3_2.java"
分析:
注意 iinc 指令是直接在區域變數 slot 上進行運算
a++ 和 ++a 的區別是先執行 iload 還是 先執行 iinc

5.條件判斷
| 指令 | 助記符 | 含義 |
|---|---|---|
| 0x99 | ifeq | 判斷是否 == 0 |
| 0x9a | ifne | 判斷是否 != 0 |
| 0x9b | i?t | 判斷是否 < 0 |
| 0x9c | ifge | 判斷是否 >= 0 |
| 0x9d | ifgt | 判斷是否 > 0 |
| 0x9e | i?e | 判斷是否 <= 0 |
| 0x9f | if_icmpeq | 兩個int是否 == |
| 0xa0 | if_icmpne | 兩個int是否 != |
| 0xa1 | if_icmplt | 兩個int是否 < |
| 0xa2 | if_icmpge | 兩個int是否 >= |
| 0xa3 | if_icmpgt | 兩個int是否 > |
| 0xa4 | if_icmple | 兩個int是否 <= |
| 0xa5 | if_acmpeq | 兩個參考是否 == |
| 0xa6 | if_acmpne | 兩個參考是否 != |
| 0xc6 | ifnull | 判斷是否 == null |
| 0xc7 | ifnonnull | 判斷是否 != null |
幾點說明:
byte,short,char 都會按 int 比較,因為運算元堆疊都是 4 位元組 goto 用來進行跳轉到指定行號的位元組碼
位元組碼:
0: iconst_0
1: istore_1
2: iload_1
3: ifne 12
6: bipush 10
8: istore_1
9: goto 15
12: bipush 20
14: istore_1
15: return
注意,比較小的數用iconst來表示,ifne 12,判斷運算元中的堆疊是不是不等于0,如果不等于0就會跳轉到12行,如果不成立,就會執行后面的代碼,接著往下走,goto 15是直接跳轉到15行,
思考
細心的同學應當注意到,以上比較指令中沒有 long,?oat,double 的比較,那么它們要比較怎 么辦?
參考 https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.lcmp
6.回圈控制指令
其實回圈控制還是前面介紹的那些指令,例如 while 回圈:
public class Demo3_4 {
public static void main(String[] args) {
int a = 0;
while (a < 10) {
a++;
}
}
}
位元組碼是:
0: iconst_0
1: istore_1
2: iload_1
3: bipush 10
5: if_icmpge 14
8: iinc 1, 1
11: goto 2
14: return
public class Demo3_5 {
public static void main(String[] args) {
int a = 0;
do {
a++;
}while (a < 10);}
}
}
后再看看 for 回圈:
public class Demo3_6 {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
}
}
}
0: iconst_0
1: istore_1
2: iload_1
3: bipush 10
5: if_icmpge 14
8: iinc 1, 1
11: goto 2
14: return
注意 比較 while 和 for 的位元組碼,你發現它們是一模一樣的,殊途也能同歸
7 練習 - 判斷結果
請從位元組碼角度分析,下列代碼運行的結果:
public class Demo3_6_1 {
public static void main(String[] args) {
int i = 0;
int x = 0;
while (i < 10) {
x = x++;
i++;
}
System.out.println(x);
}
}
x=x++;
x++的程序對應兩條位元組碼指令
iload_x
iinc x,1
初始x(0)
iload是把區域變數表中的0讀進運算元堆疊,讀完以后,我們iinc進行自增,自增的結果是區域變數表中的x變為1,它然后又執行了賦值操作,把運算元堆疊中的0取出來,再覆寫掉區域變數中的x.等第一次回圈之后,區域變數表中的x仍然是0,即使再回圈多少次,值仍然為0,
8 構造方法
public class Demo3_8_1 {
static int i = 10;
static {
i = 20;
}
static {
i = 30;
}
public static void main(String[] args) {
System.out.println(Demo3_8_1.i);
}
}
編譯器會按從上至下的順序,收集所有 static 靜態代碼塊和靜態成員賦值的代碼,合并為一個特殊的方法 < cinit> ()V :
stack=1, locals=0, args_size=0
0: bipush 10
2: putstatic #2 // Field i:I
5: bipush 20
7: putstatic #2 // Field i:I
10: bipush 30
12: putstatic #2 // Field i:I
15: return
最后賦值的是30,所以結果是30
init()V
public class Demo4 {
private String a = "s1";
{
b = 20;
}
private int b = 10;
{
a = "s2";
}
public Demo4(String a, int b) {
this.a = a;
this.b = b;
}
public static void main(String[] args) {
Demo4 d = new Demo4("s3", 30);
System.out.println(d.a);
System.out.println(d.b);
}
}
編譯器會按從上至下的順序,收集所有 {} 代碼塊和成員變數賦值的代碼,形成新的構造方法,但原始構造方法內的代碼總是在后
Code:
stack=2, locals=3, args_size=3
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: ldc #2 // String s1
7: putfield #3 // Field a:Ljava/lang/String;
10: aload_0
11: bipush 20
13: putfield #4 // Field b:I
16: aload_0
17: bipush 10
19: putfield #4 // Field b:I
22: aload_0
23: ldc #5 // String s2
25: putfield #3 // Field a:Ljava/lang/String;
//原始構造方法在最后執行
28: aload_0
29: aload_1
30: putfield #3 // Field a:Ljava/lang/String;
33: aload_0
34: iload_2
35: putfield #4 // Field b:I
38: return

執行順序:靜態代碼塊->非靜態代碼塊->類的構造方法
9 方法呼叫
public class Demo5 {
public Demo5() {
}
private void test1() {
}
private final void test2() {
}
public void test3() {
}
public static void test4() {
}
public static void main(String[] args) {
Demo5 demo5 = new Demo5();
demo5.test1();
demo5.test2();
demo5.test3();
Demo5.test4();
}
}
不同方法在呼叫時,對應的虛擬機指令有所區別
私有、構造、被final修飾的方法,在呼叫時都使用invokespecial指令
普通成員方法在呼叫時,使用invokespecial指令,因為編譯期間無法確定該方法的內容,只有在運行期間才能確定
靜態方法在呼叫時使用invokestatic指令
invokespecial在呼叫時無法確定是呼叫哪個物件的方法,也許是父類的,也許是子類的,invokespecial稱之為動態系結,在運行的時候確定呼叫哪個物件的方法,invokestatic靜態系結直接就能找到方法的入口地址了,
Code:
stack=2, locals=2, args_size=1
0: new #2 // class com/nyima/JVM/day5/Demo5
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokespecial #4 // Method test1:()V
12: aload_1
13: invokespecial #5 // Method test2:()V
16: aload_1
17: invokevirtual #6 // Method test3:()V
20: invokestatic #7 // Method test4:()V
23: return
new 是創建【物件】,給物件分配堆記憶體,執行成功會將【物件參考】壓入運算元堆疊
dup 是復制運算元堆疊堆疊頂的內容,本例即為【物件參考】,為什么需要兩份參考呢,一個是要配合 invokespecial 呼叫該物件的構造方法 “init”😦)V (會消耗掉堆疊頂一個參考),另一個要 配合 astore_1 賦值給區域變數
終方法(?nal),私有方法(private),構造方法都是由 invokespecial 指令來呼叫,屬于靜態系結
普通成員方法是由 invokevirtual 呼叫,屬于動態系結,即支持多型 成員方法與靜態方法呼叫的另一個區別是,執行方法前是否需要【物件參考】
靜態方法屬于靜態系結,不需要物件來呼叫,
10.多型的原理
/**
* 演示多型原理,注意加上下面的 JVM 引數,禁用指標壓縮
* -XX:-UseCompressedOops -XX:-UseCompressedClassPointers
*/
public class Demo3_10 {
public static void test(Animal animal) {
animal.eat();
System.out.println(animal.toString());
}
public static void main(String[] args) throws IOException {
test(new Cat());
test(new Dog());
System.in.read();
}
}
abstract class Animal {
public abstract void eat();
@Override
public String toString() {
return "我是" + this.getClass().getSimpleName();
}
}
class Dog extends Animal {
@Override
public void eat() {
System.out.println("啃骨頭");
}
}
class Cat extends Animal {
@Override
public void eat() {
System.out.println("吃魚");
}
}
因為普通成員方法需要在運行時才能確定具體的內容,所以虛擬機需要呼叫invokevirtual指令
在執行invokevirtual指令時,經歷了以下幾個步驟
先通過堆疊幀中物件的參考找到物件
分析物件頭,找到物件實際的Class
Class結構中有vtable
查詢vtable找到方法的具體地址
執行方法的位元組碼
11.例外處理
11.1.try-catch
public class Demo1 {
public static void main(String[] args) {
int i = 0;
try {
i = 10;
}catch (Exception e) {
i = 20;
}
}
}
對應位元組碼指令
Code:
stack=1, locals=3, args_size=1
0: iconst_0
1: istore_1
2: bipush 10
4: istore_1
5: goto 12
8: astore_2
9: bipush 20
11: istore_1
12: return
//多出來一個例外表
Exception table:
from to target type
2 5 8 Class java/lang/Exception
可以看到多出來一個 Exception table 的結構,[from, to) 是前閉后開(也就是檢測2~4行)的檢測范圍,
一旦這個范圍內的位元組碼執行出現例外,則通過 type 匹配例外型別,如果一致,進入 target 所指示行號
8行的位元組碼指令 astore_2 是將例外物件參考存入區域變數表的2號位置(為e)
11.2.多個single-catch
public class Demo1 {
public static void main(String[] args) {
int i = 0;
try {
i = 10;
}catch (ArithmeticException e) {
i = 20;
}catch (Exception e) {
i = 30;
}
}
}
對應位元組碼
Code:
stack=1, locals=3, args_size=1
0: iconst_0
1: istore_1
2: bipush 10
4: istore_1
5: goto 19
8: astore_2
9: bipush 20
11: istore_1
12: goto 19
15: astore_2
16: bipush 30
18: istore_1
19: return
Exception table:
from to target type
2 5 8 Class java/lang/ArithmeticException
2 5 15 Class java/lang/Exception
因為例外出現時,只能進入 Exception table 中一個分支,所以區域變數表 slot 2 位置被共用
11.3.finally
public class Demo2 {
public static void main(String[] args) {
int i = 0;
try {
i = 10;
} catch (Exception e) {
i = 20;
} finally {
i = 30;
}
}
}
對應位元組碼
Code:
stack=1, locals=4, args_size=1
0: iconst_0
1: istore_1
//try塊
2: bipush 10
4: istore_1
//try塊執行完后,會執行finally
5: bipush 30
7: istore_1
8: goto 27
//catch塊
11: astore_2 //例外資訊放入區域變數表的2號槽位
12: bipush 20
14: istore_1
//catch塊執行完后,會執行finally
15: bipush 30
17: istore_1
18: goto 27
//出現例外,但未被Exception捕獲,會拋出其他例外,這時也需要執行finally塊中的代碼
21: astore_3
22: bipush 30
24: istore_1
25: aload_3
26: athrow //拋出例外
27: return
Exception table:
from to target type
2 5 11 Class java/lang/Exception
2 5 21 any
11 15 21 any
可以看到 ?nally 中的代碼被復制了 3 份,分別放入 try 流程,catch 流程以及 catch剩余的例外型別流程,即catch塊捕獲不到的例外finally也要執行,
注意:雖然從位元組碼指令看來,每個塊中都有finally塊,但是finally塊中的代碼只會被執行一次
11.4.finally面試題
11.4.1.finally中的return
下面的程式運行結果
public class Demo3 {
public static void main(String[] args) {
int i = Demo3.test();
//結果為20
System.out.println(i);
}
public static int test() {
int i;
try {
i = 10;
return i;
} finally {
i = 20;
return i;
}
}
}
對應位元組碼
Code:
stack=1, locals=3, args_size=0
0: bipush 10
2: istore_0
3: iload_0
4: istore_1 //暫存回傳值
5: bipush 20
7: istore_0
8: iload_0
9: ireturn //ireturn會回傳運算元堆疊頂的整型值20
//如果出現例外,還是會執行finally塊中的內容,沒有拋出例外
10: astore_2
11: bipush 20
13: istore_0
14: iload_0
15: ireturn //這里沒有athrow了,也就是如果在finally塊中如果有回傳操作的話,且try塊中出現例外,會吞掉例外!
Exception table:
from to target type
0 5 10 any
由于 ?nally 中的 ireturn 被插入了所有可能的流程,因此回傳結果肯定以?nally的為準
至于位元組碼中第 2 行,似乎沒啥用,且留個伏筆,看下個例子
跟上例中的 ?nally 相比,發現沒有 athrow 了,這告訴我們:如果在 ?nally 中出現了 return,會吞掉例外,這樣就不知道發生了例外
所以不要在finally中進行回傳操作
11.4.2.被吞掉的例外
public class Demo3 {
public static void main(String[] args) {
int i = Demo3.test();
//最終結果為20
System.out.println(i);
}
public static int test() {
int i;
try {
i = 10;
//這里應該會拋出例外
i = i/0;
return i;
} finally {
i = 20;
return i;
}
}
}
會發現列印結果為20,并未拋出例外
11.4.3.finally不帶return
public class Demo4 {
public static void main(String[] args) {
int i = Demo4.test();
System.out.println(i);
}
public static int test() {
int i = 10;
try {
return i;
} finally {
i = 20;
}
}
}
結果是10
對應位元組碼
Code:
stack=1, locals=3, args_size=0
0: bipush 10
2: istore_0 //賦值給i 10
3: iload_0 //加載到運算元堆疊頂
4: istore_1 //加載到區域變數表的1號位置
5: bipush 20
7: istore_0 //賦值給i 20
8: iload_1 //加載區域變數表1號位置的數10到運算元堆疊
9: ireturn //回傳運算元堆疊頂元素 10
10: astore_2
11: bipush 20
13: istore_0
14: aload_2 //加載例外
15: athrow //拋出例外
Exception table:
from to target type
3 5 10 any
如果在try陳述句return了,finally中雖然對i做了變化,但是不會影響到回傳結果的,因為它在return之前先做了一個暫存,然后再執行了finally中的代碼,然后再把暫存的值取回來,最后回傳的是return時暫存的值,
12.synchronized
public class Demo5 {
public static void main(String[] args) {
int i = 10;
Lock lock = new Lock();
synchronized (lock) {
System.out.println(i);
}
}
}
對應位元組碼
Code:
stack=2, locals=5, args_size=1
0: bipush 10
2: istore_1
3: new #2 // class com/nyima/JVM/day06/Lock
6: dup //復制一份,放到運算元堆疊頂,用于建構式消耗
7: invokespecial #3 // Method com/nyima/JVM/day06/Lock."<init>":()V
10: astore_2 //剩下的一份放到區域變數表的2號位置
11: aload_2 //加載到運算元堆疊
12: dup //復制一份,放到運算元堆疊,用于加鎖時消耗
13: astore_3 //將運算元堆疊頂元素彈出,暫存到區域變數表的三號槽位,這時運算元堆疊中有一份物件的參考
14: monitorenter //加鎖
//鎖住后代碼塊中的操作
15: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
18: iload_1
19: invokevirtual #5 // Method java/io/PrintStream.println:(I)V
//加載區域變數表中三號槽位物件的參考,用于解鎖
22: aload_3
23: monitorexit //解鎖
24: goto 34
//例外操作
27: astore 4
29: aload_3
30: monitorexit //解鎖
31: aload 4
33: athrow
34: return
//可以看出,無論何時出現例外,都會跳轉到27行,將例外放入區域變數中,并進行解鎖操作,然后加載例外并拋出例外,
Exception table:
from to target type
15 24 27 any
27 31 27 any
不管是方法正常執行,還是中間出現了例外,還總是會進入到25行加載剛才暫存的lock物件的參考來配合monitorexit進行一個解鎖操作
注意
方法級別的 synchronized 不會在位元組碼指令中有所體現
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/256360.html
標籤:java
上一篇:Java基礎之陣列
下一篇:Java基礎知識難點總結
