類加載與字節(jié)碼技術(shù)
1.類文件結(jié)構(gòu)
根據(jù) JVM 規(guī)范,類文件結(jié)構(gòu)如下
ClassFile {
u4 magic; //魔數(shù)
u2 minor_version; //小版本號(hào)
u2 major_version; //java 主版本號(hào)
u2 constant_pool_count; //常量池
cp_info constant_pool[constant_pool_count-1];
u2 access_flags; //訪問標(biāo)識(shí)與繼承信息
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count; method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
2.字節(jié)碼指令
指令 | 作用 |
---|---|
iconst_1 | int型常量值1進(jìn)棧 |
bipush | 將一個(gè)byte型常量值推送至棧頂 |
iload_1 | 第二個(gè)int型局部變量進(jìn)操作數(shù)棧,從0開始計(jì)數(shù) |
istore_1 | 將操作數(shù)棧棧頂 int 型數(shù)值存入第二個(gè)局部變量郭变,從0開始計(jì)數(shù) |
iadd | 棧頂兩int型數(shù)值相加,并且結(jié)果進(jìn)棧 |
return | 當(dāng)前方法返回void |
getstatic | 獲取指定類的靜態(tài)域,并將其值壓入棧頂 |
putstatic | 為指定的類的靜態(tài)域賦值 |
invokevirtual | 調(diào)用實(shí)例方法 |
invokespecial | 調(diào)用超類構(gòu)造方法、實(shí)例初始化方法萨蚕、私有方法 |
invokestatic | 調(diào)用靜態(tài)方法 |
invokeinterface | 調(diào)用接口方法 |
new | 創(chuàng)建一個(gè)對(duì)象,并且其引用進(jìn)棧 |
newarray | 創(chuàng)建一個(gè)基本類型數(shù)組蹄胰,并且其引用進(jìn)棧 |
iinc 1,1 | 局部變量表的值自加 |
iinc 1,-1 | 局部變量表的值自減 |
2.1 javap 工具
自己分析類文件結(jié)構(gòu)太麻煩了岳遥,Oracle 提供了 javap 工具來反編譯 class 文件 javap -v
javap -v Demo1_22.class
Classfile /D:/IDEAworkplace/jvm/out/production/jvm/cn/itcast/jvm/t1/stringtable/Demo1_22.class
Last modified 2020-1-30; size 534 bytes
MD5 checksum 5c4213b2f1defff2bb24bf7cbd5ff183
Compiled from "Demo1_22.java"
public class cn.itcast.jvm.t1.stringtable.Demo1_22
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
//常量池
Constant pool:
#1 = Methodref #6.#24 // java/lang/Object."<init>":()V
#2 = String #25 // a
#3 = String #26 // b
#4 = String #27 // ab
#5 = Class #28 // cn/itcast/jvm/t1/stringtable/Demo1_22
#6 = Class #29 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcn/itcast/jvm/t1/stringtable/Demo1_22;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 args
#17 = Utf8 [Ljava/lang/String;
#18 = Utf8 s1
#19 = Utf8 Ljava/lang/String;
#20 = Utf8 s2
#21 = Utf8 s3
#22 = Utf8 SourceFile
#23 = Utf8 Demo1_22.java
#24 = NameAndType #7:#8 // "<init>":()V
#25 = Utf8 a
#26 = Utf8 b
#27 = Utf8 ab
#28 = Utf8 cn/itcast/jvm/t1/stringtable/Demo1_22
#29 = Utf8 java/lang/Object
{
//----------------------------------------構(gòu)造方法
public cn.itcast.jvm.t1.stringtable.Demo1_22();
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
//源代碼的第幾行對(duì)應(yīng)字節(jié)碼的第幾行
LineNumberTable:
line 4: 0
//本地變量表
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcn/itcast/jvm/t1/stringtable/Demo1_22;
//-------------------------------
//-------------------------------main方法
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
//執(zhí)行指令代碼
Code:
stack=1, locals=4, args_size=1
0: ldc #2 // String a
2: astore_1
3: ldc #3 // String b
5: astore_2
6: ldc #4 // String ab
8: astore_3
9: return
//源代碼的第幾行對(duì)應(yīng)字節(jié)碼的第幾行
LineNumberTable:
line 11: 0
line 12: 3
line 13: 6
line 26: 9
//局部變量表
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 args [Ljava/lang/String;
3 7 1 s1 Ljava/lang/String;
6 4 2 s2 Ljava/lang/String;
9 1 3 s3 Ljava/lang/String;
}
SourceFile: "Demo1_22.java"
2.2 圖解方法執(zhí)行流程
(1)原始java代碼
/*** 演示 字節(jié)碼指令 和 操作數(shù)棧、常量池的關(guān)系 */
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);
}
}
(2)編譯后的字節(jié)碼文件
D:\IDEAworkplace\jvm\out\production\jvm\cn\itcast\jvm\t3\bytecode> javap -v Demo3_1.class
Classfile /D:/IDEAworkplace/jvm/out/production/jvm/cn/itcast/jvm/t3/bytecode/Demo3_1.class
Last modified 2020-1-28; size 635 bytes
MD5 checksum 1a6413a652bcc5023f130b392deb76a1
Compiled from "Demo3_1.java"
public class cn.itcast.jvm.t3.bytecode.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
a/io/PrintStream.println:(I)V
#6 = Class #31 // cn/itcast/jvm/t3/bytecode/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/itcast/jvm/t3/bytecode/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/itcast/jvm/t3/bytecode/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.itcast.jvm.t3.bytecode.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 6: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcn/itcast/jvm/t3/bytecode/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 8: 0
line 9: 3
line 10: 6
line 11: 10
line 12: 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)常量池載入運(yùn)行時(shí)常量池
(4)方法字節(jié)碼載入方法區(qū)
(5)main線程開始運(yùn)行裕寨,分配棧幀內(nèi)存
(stack=2(棧的深度)浩蓉,locals=4(局部變量表))
(6)執(zhí)行引擎開始執(zhí)行字節(jié)碼
bipush 10
將一個(gè) byte 壓入操作數(shù)棧(操作數(shù)棧默認(rèn)都是4字節(jié))(不夠4字節(jié)派继,其長(zhǎng)度會(huì)補(bǔ)齊 4 個(gè)字節(jié)),類似的指令還有
sipush 將一個(gè) short 壓入操作數(shù)棧(其長(zhǎng)度會(huì)補(bǔ)齊 4 個(gè)字節(jié))
ldc 將一個(gè) int 壓入操作數(shù)棧
ldc2_w 將一個(gè) long 壓入操作數(shù)棧(分兩次壓入捻艳,因?yàn)?long 是 8 個(gè)字節(jié))
這里小的數(shù)字都是和字節(jié)碼指令存在一起驾窟,超過 short 范圍的數(shù)字存入了常量池
istore_1
- 將操作數(shù)棧頂數(shù)據(jù)彈出,存入局部變量表的 slot 1
ldc #3
從常量池加載 #3 數(shù)據(jù)到操作數(shù)棧
注意 Short.MAX_VALUE 是 32767认轨,所以 32768 = Short.MAX_VALUE + 1 實(shí)際是在編譯期間計(jì)算好的
istore_2
iload_1
iload_2
iadd
istore_3
getstatic #4
iload_3
invokevirtual #5
找到常量池 #5 項(xiàng)
定位到方法區(qū) java/io/PrintStream.println:(I)V 方法
生成新的棧幀(分配 locals绅络、stack等)
傳遞參數(shù),執(zhí)行新棧幀中的字節(jié)碼
執(zhí)行完畢嘁字,彈出棧幀
清除 main 操作數(shù)棧內(nèi)容
return
完成 main 方法調(diào)用恩急,彈出 main 棧幀
程序結(jié)束
2.3 練習(xí)-分析 a++
目的:從字節(jié)碼角度分析 a++ 相關(guān)題目
源碼:
/**
* 從字節(jié)碼角度分析 a++ 相關(guān)題目
*/
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);
}
}
字節(jié)碼:
D:\IDEAworkplace\jvm\out\production\jvm\cn\itcast\jvm\t3\bytecode> javap -v Demo3_2.class
Classfile /D:/IDEAworkplace/jvm/out/production/jvm/cn/itcast/jvm/t3/bytecode/Demo3_2.class
Last modified 2020-1-28; size 610 bytes
MD5 checksum 5f6a35e5b9bb88d08249958a8d2ab043
Compiled from "Demo3_2.java"
public class cn.itcast.jvm.t3.bytecode.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/itcast/jvm/t3/bytecode/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/itcast/jvm/t3/bytecode/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/itcast/jvm/t3/bytecode/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.itcast.jvm.t3.bytecode.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 6: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcn/itcast/jvm/t3/bytecode/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 8: 0
line 9: 3
line 10: 18
line 11: 25
line 12: 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 上進(jìn)行運(yùn)算
a++ 和 ++a 的區(qū)別是先執(zhí)行 iload 還是 先執(zhí)行 iinc
a++
++a
add
a--
add
2.4 練習(xí) - 判斷結(jié)果
從字節(jié)碼角度分析,先 istore 0 纪蜒,進(jìn)操作數(shù)棧衷恭,然后,再將x 的局部變量表執(zhí)行 iinc 加1霍掺,然后匾荆,把操作數(shù)棧的數(shù) istore 回去 x 所在的局部變量表伞访,所以绝页,x一直為0
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);
}
}
//結(jié)果 為 0
2.5 方法調(diào)用
public class Demo3_9 {
public Demo3_9() { }
private void test1() { }
private final void test2() { }
public void test3() { }
public static void test4() { }
@Override
public String toString() {
return super.toString();
}
public static void main(String[] args) {
Demo3_9 d = new Demo3_9();
d.test1(); //invokespecial
d.test2(); //invokespecial
d.test3(); //invokevirtual
d.test4(); //invokestatic
Demo3_9.test4(); //invokestatic
d.toString();
}
}
stack=2, locals=2, args_size=1
0: new #3 // class cn/itcast/jvm/t3/bytecode/Demo3_9
3: dup
4: invokespecial #4 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokespecial #5 // Method test1:()V
12: aload_1
13: invokespecial #6 // Method test2:()V
16: aload_1
17: invokevirtual #7 // Method test3:()V
20: aload_1
21: pop //說明通過對(duì)象調(diào)用靜態(tài)方法灭忠,會(huì)彈出棧,直接調(diào)用
22: invokestatic #8 // Method test4:()V
25: invokestatic #8 // Method test4:()V
28: aload_1
29: invokevirtual #9 // Method toString:()Ljava/lang/String;
32: pop
33: return
通過對(duì)象調(diào)用靜態(tài)方法兔魂,實(shí)際上是load 后,又出棧了举娩,說明通過對(duì)象調(diào)用靜態(tài)方法析校,會(huì)彈出棧,直接調(diào)用铜涉。
靜態(tài)方法是不需要對(duì)象的智玻,直接使用
2.6 new 關(guān)鍵字原理
0: new #3 // class cn/itcast/jvm/t3/bytecode/Demo3_9
3: dup
4: invokespecial #4 // Method "<init>":()V
7: astore_1
new 有兩步,第一步是先分配一個(gè)對(duì)象在堆空間需要的內(nèi)存芙代,分配成功吊奢,會(huì)把對(duì)象的引用放入操作數(shù)棧,然后 執(zhí)行 dup 復(fù)制一份對(duì)象引用纹烹? 為什么页滚? 復(fù)制一份對(duì)象,來執(zhí)行init方法铺呵,構(gòu)造方法裹驰,執(zhí)行完后,這個(gè)對(duì)象就出棧了片挂,所以需要復(fù)制一份幻林,然后會(huì)執(zhí)行 store 把此時(shí)棧頂?shù)膶?duì)象引用存儲(chǔ)到局部變量表中的(即賦值給 new 出這個(gè)對(duì)象的引用 )
2.7 多態(tài)原理
在類的鏈接階段贞盯,就確定了虛方法表(vtable) ,確定下一個(gè)類加載時(shí)執(zhí)行方法的先后順序
當(dāng)執(zhí)行一個(gè)對(duì)象的 invokevirtual指令時(shí)沪饺,
- 先通過棧幀中的對(duì)象引用找到對(duì)象
- 分析對(duì)象頭躏敢,找到對(duì)象的實(shí)際class
- class結(jié)構(gòu)中有 vtable ,它在類加載的鏈接階段就已經(jīng)根據(jù)方法的重寫規(guī)則生成好了
- 查表得到方法的具體地址
- 執(zhí)行方法的字節(jié)碼
2.8 異常處理
try - catch
public class Demo3_11_1 {
public static void main(String[] args) {
int i = 0;
try {
i = 10;
} catch (Exception e) {
i = 20;
}
}
}
字節(jié)碼
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
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 //把異常對(duì)象的局部變量存到e中
9: bipush 20
11: istore_1
12: return
//異常表随闽,檢測(cè)第2行到第5行范圍內(nèi)的異常父丰,進(jìn)行異常類型匹配,一旦有異常,則會(huì)到第執(zhí)行第8行
Exception table:
from to target type
2 5 8 Class java/lang/Exception
//局部變量表
LocalVariableTable:
Start Length Slot Name Signature
9 3 2 e Ljava/lang/Exception;
0 13 0 args [Ljava/lang/String;
2 11 1 i I
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 8
locals = [ class "[Ljava/lang/String;", int ]
stack = [ class java/lang/Exception ]
frame_type = 3 /* same */
}
- 可以看到多出來一個(gè) Exception table 的結(jié)構(gòu)掘宪,[from, to) 是前閉后開的檢測(cè)范圍蛾扇,一旦這個(gè)范圍
? 內(nèi)的字節(jié)碼執(zhí)行出現(xiàn)異常,則通過 type 匹配異常類型魏滚,如果一致镀首,進(jìn)入 target 所指示行號(hào)
- 8 行的字節(jié)碼指令 astore_2 是將異常對(duì)象引用存入局部變量表的 slot 2 位置
多個(gè) try-catch塊
public class Demo3_11_2 {
public static void main(String[] args) {
int i = 0;
try {
i = 10;
} catch (ArithmeticException e) {
i = 30;
} catch (NullPointerException e) {
i = 40;
} catch (Exception e) {
i = 50;
}
}
}
字節(jié)碼:
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=3, args_size=1
0: iconst_0
1: istore_1
2: bipush 10
4: istore_1
5: goto 26
8: astore_2
9: bipush 30
11: istore_1
12: goto 26
15: astore_2
16: bipush 40
18: istore_1
19: goto 26
22: astore_2
23: bipush 50
25: istore_1
26: return
Exception table:
from to target type
2 5 8 Class java/lang/ArithmeticException
2 5 15 Class java/lang/NullPointerException
2 5 22 Class java/lang/Exception
LineNumberTable:
LocalVariableTable:
Start Length Slot Name Signature
9 3 2 e Ljava/lang/ArithmeticException;
16 3 2 e Ljava/lang/NullPointerException;
23 3 2 e Ljava/lang/Exception;
0 27 0 args [Ljava/lang/String;
2 25 1 i I
- 因?yàn)楫惓3霈F(xiàn)時(shí),只能進(jìn)入 Exception table 中一個(gè)分支鼠次,所以局部變量表 slot 2 位置被共用
multi-catch的情況
public class Demo3_11_3 {
public static void main(String[] args) {
try {
Method test = Demo3_11_3.class.getMethod("test");
test.invoke(null);
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
}
public static void test() {
System.out.println("ok");
}
}
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=2, args_size=1
0: ldc #2 // class cn/itcast/jvm/t3/bytecode/Demo3_11_3
2: ldc #3 // String test
4: iconst_0
5: anewarray #4 // class java/lang/Class
8: invokevirtual #5 // Method java/lang/Class.getMethod:(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;
11: astore_1
12: aload_1
13: aconst_null
14: iconst_0
15: anewarray #6 // class java/lang/Object
18: invokevirtual #7 // Method java/lang/reflect/Method.invoke:(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;
21: pop
22: goto 30
25: astore_1
26: aload_1
27: invokevirtual #11 // Method java/lang/ReflectiveOperationException.printStackTrace:()V
30: return
Exception table:
from to target type
0 22 25 Class java/lang/NoSuchMethodException
0 22 25 Class java/lang/IllegalAccessException
0 22 25 Class java/lang/reflect/InvocationTargetException
LocalVariableTable:
Start Length Slot Name Signature
12 10 1 test Ljava/lang/reflect/Method;
26 4 1 e Ljava/lang/ReflectiveOperationException;
0 31 0 args [Ljava/lang/String;
StackMapTable: number_of_entries = 2
finally
public class Demo3_11_4 {
public static void main(String[] args) {
int i = 0;
try {
i = 10;
} catch (Exception e) {
i = 20;
} finally {
i = 30;
}
}
}
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=4, args_size=1
0: iconst_0
1: istore_1 // 0 -> i
2: bipush 10 //try---
4: istore_1 // 10 -> i
5: bipush 30 //finally
7: istore_1 //30 -> i
8: goto 27 //return
11: astore_2 //catch Exception -> e
12: bipush 20
14: istore_1
15: bipush 30 //finally
17: istore_1
18: goto 27
21: astore_3
22: bipush 30 //finally
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
LocalVariableTable:
Start Length Slot Name Signature
12 3 2 e Ljava/lang/Exception;
0 28 0 args [Ljava/lang/String;
2 26 1 i I
StackMapTable: number_of_entries = 3
}
可以看到 finally 中的代碼被復(fù)制了 3 份更哄,分別放入 try 流程,catch 流程以及 catch 剩余的異常類型流
程
2.9 finally練習(xí)
題目一:
public class Demo3_12_2 {
public static void main(String[] args) {
int result = test();
System.out.println(result);
}
public static int test() {
try {
return 10;
} finally {
return 20;
}
}
}
//結(jié)果是 20
public static int test();
descriptor: ()I
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=2, args_size=0
0: bipush 10 // <- 10放入棧頂
2: istore_0 // 10 -> slot 0 (從棧頂移除了)
3: bipush 20 // <- 20 放入棧頂
5: ireturn // 返回棧頂 int(20)
6: astore_1 // catch any --> slot 1
7: bipush 20 // <- 20 放入棧頂
9: ireturn //返回棧頂 int(20)
Exception table:
from to target type
0 3 6 any
}
由于 fifinally 中的 ireturn 被插入了所有可能的流程腥寇,因此返回結(jié)果肯定以 finally 的為準(zhǔn)
至于字節(jié)碼中第 2 行成翩,似乎沒啥用,且留個(gè)伏筆赦役,看下個(gè)例子
跟上例中的 finally 相比麻敌,發(fā)現(xiàn)沒有 athrow 了,這告訴我們:如果在 finally 中出現(xiàn)了 return掂摔,會(huì)吞掉異常术羔,最好不要在finally 里面 return
例如:下面的代碼,不會(huì)出現(xiàn)除零異常
public static int test() {
try {
return 10/0;
} finally {
return 20;
}
}
}
題目二:
public class Demo3_12_2 {
public static void main(String[] args) {
int result = test();
System.out.println(result);
}
public static int test() {
int i = 10;
try {
return i;
} finally {
i = 20;
}
}
}
//結(jié)果是 10
public static int test();
descriptor: ()I
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=3, args_size=0
0: bipush 10 // <- 10 放入棧頂
2: istore_0 // 10 -> i
3: iload_0 // <- i(10)
4: istore_1 // 10 -> slot 1 ,暫存至 slot 1, 目的時(shí)為了固定返回值
5: bipush 20 // <- 20 放入棧頂
7: istore_0 // 20 ->i
8: iload_1 // <- slot 1(10) 載入 slot 1 暫存的值
9: ireturn //返回棧頂?shù)?int(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
LocalVariableTable:
Start Length Slot Name Signature
3 13 0 i I
2.10 synchronized
public class Demo3_13 {
public static void main(String[] args) {
Object lock = new Object();
synchronized (lock) {
System.out.println("ok");
}
}
}
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: new #2 // new Object
3: dup //復(fù)制一份對(duì)象引用
4: invokespecial #1 // 執(zhí)行構(gòu)造方法
7: astore_1 //lock引用 -> lock
8: aload_1 //對(duì)象加載到操作數(shù)棧 <-lock (synchronized開始)
9: dup
10: astore_2 //lock 引用 -> slot2
11: monitorenter // monitorenter (lock引用) 加鎖操作
12: getstatic #3 //System.out
15: ldc #4 // String ok
17: invokevirtual #5 // System.out
20: aload_2 // <- slot2 (lock引用)
21: monitorexit // monitorexit 解鎖
22: goto 30
//--------------------出現(xiàn)異常-----
25: astore_3 // any -> slot 3
26: aload_2 //<- slot 2(lock 引用)
27: monitorexit // monitorexit 解鎖
28: aload_3
29: athrow
/------------------
30: return
Exception table:
from to target type
12 22 25 any
25 28 25 any
LocalVariableTable:
Start Length Slot Name Signature
0 31 0 args [Ljava/lang/String;
8 23 1 lock Ljava/lang/Object;
}
3.編譯器處理
所謂的 語法糖 乙漓,其實(shí)就是指 java 編譯器把 *.java 源碼編譯為 *.class 字節(jié)碼的過程中级历,自動(dòng)生成
和轉(zhuǎn)換的一些代碼,主要是為了減輕程序員的負(fù)擔(dān)叭披,算是 java 編譯器給我們的一個(gè)額外福利(給糖吃
嘛)
注意寥殖,以下代碼的分析,借助了 javap 工具趋观,idea 的反編譯功能扛禽,idea 插件 jclasslib 等工具。另外皱坛,
編譯器轉(zhuǎn)換的結(jié)果直接就是 class 字節(jié)碼编曼,只是為了便于閱讀,給出了 幾乎等價(jià) 的 java 源碼方式剩辟,并
不是編譯器還會(huì)轉(zhuǎn)換出中間的 java 源碼掐场,切記往扔。
3.1 默認(rèn)構(gòu)造器
public class Candy1 {
}
public class Candy1 {
// 這個(gè)無參構(gòu)造是編譯器幫助我們加上的
public Candy1() {
super(); // 即調(diào)用父類 Object 的無參構(gòu)造方法,即調(diào)用 java/lang/Object." <init>":()V
}
}
3.2 自動(dòng)拆裝箱
這個(gè)特性是 JDK 5 開始加入的熊户, 代碼片段1 :
public class Candy2 {
public static void main(String[] args) {
Integer x = 1;
int y = x;
}
}
這段代碼在 JDK 5 之前是無法編譯通過的萍膛,必須改寫為 代碼片段2 :
public class Candy2 {
public static void main(String[] args) {
Integer x = Integer.valueOf(1);
int y = x.intValue();
}
}
3.3 泛型集合取值
泛型也是在 JDK 5 開始加入的特性,但 java 在編譯泛型代碼后會(huì)執(zhí)行 泛型擦除 的動(dòng)作嚷堡,即泛型信息
在編譯為字節(jié)碼之后就丟失了蝗罗,實(shí)際的類型都當(dāng)做了 Object 類型來處理:
public class Candy3 {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(10); // 實(shí)際調(diào)用的是 List.add(Object e)
Integer x = list.get(0); // 實(shí)際調(diào)用的是 Object obj = List.get(int index);
}
}
所以在取值時(shí),編譯器真正生成的字節(jié)碼中蝌戒,還要額外做一個(gè)類型轉(zhuǎn)換的操作:
// 需要將 Object 轉(zhuǎn)為
Integer Integer x = (Integer)list.get(0);
如果前面的 x 變量類型修改為 int 基本類型那么最終生成的字節(jié)碼是:
// 需要將 Object 轉(zhuǎn)為 Integer, 并執(zhí)行拆箱操作
int x = ((Integer)list.get(0)).intValue();
擦除的是字節(jié)碼上的泛型信息串塑,可以看到 LocalVariableTypeTable 仍然保留了方法參數(shù)泛型的信息
public static void main(java.lang.String[]) throws java.lang.Exception;
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: new #2 // class java/util/ArrayList
3: dup
4: invokespecial #3 // Method java/util/ArrayList."<init>":()V
7: astore_1
8: aload_1
9: bipush 10
11: invokestatic #4 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
14: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
19: pop
20: aload_1
21: iconst_0
22: invokeinterface #6, 2 // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
27: checkcast #7 // class java/lang/Integer
30: astore_2
31: return
LineNumberTable:
line 13: 0
line 14: 8
line 15: 20
line 31: 31
LocalVariableTable:
Start Length Slot Name Signature
0 32 0 args [Ljava/lang/String;
8 24 1 list Ljava/util/List;
31 1 2 x Ljava/lang/Integer;
//局部變量類型表
LocalVariableTypeTable:
Start Length Slot Name Signature
8 24 1 list Ljava/util/List<Ljava/lang/Integer;>;
Exceptions:
throws java.lang.Exception
}
使用反射,仍然能夠獲得這些信息:
public Set<Integer> test(List<String> list, Map<Integer, Object> map) {
return null;
}
Method test = Candy3.class.getMethod("test", List.class, Map.class);
Type[] types = test.getGenericParameterTypes();
for (Type type : types) {
if (type instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) type;
System.out.println("原始類型 - " + parameterizedType.getRawType());
Type[] arguments = parameterizedType.getActualTypeArguments();
for (int i = 0; i < arguments.length; i++) {
System.out.printf("泛型參數(shù)[%d] - %s\n", i, arguments[i]);
}
}
}
輸出:
原始類型 - interface java.util.List
泛型參數(shù)[0] - class java.lang.String
原始類型 - interface java.util.Map
泛型參數(shù)[0] - class java.lang.Integer
泛型參數(shù)[1] - class java.lang.Object
3.4 可變參數(shù)
可變參數(shù)也是 JDK 5 開始加入的新特性: 例如:
public class Candy4 {
public static void foo(String... args) {
String[] array = args; // 直接賦值
System.out.println(array);
}
public static void main(String[] args) {
foo("hello", "world");
}
}
可變參數(shù) String... args
其實(shí)是一個(gè) String[] args
北苟,從代碼中的賦值語句中就可以看出來桩匪。 同
樣 java 編譯器會(huì)在編譯期間將上述代碼變換為:
public class Candy4 {
public static void foo(String[] args) {
String[] array = args; // 直接賦值
System.out.println(array);
}
public static void main(String[] args) {
foo(new String[]{"hello", "world"});
}
}
注意 如果調(diào)用了 foo() 則等價(jià)代碼為 foo(new String[]{}) ,創(chuàng)建了一個(gè)空的數(shù)組友鼻,而不會(huì)
傳遞 null 進(jìn)去
3.5 foreach 循環(huán)
仍是 JDK 5 開始引入的語法糖傻昙,數(shù)組的循環(huán):
public class Candy5_1 {
public static void main(String[] args) {
int[] array = {1, 2, 3, 4, 5}; // 數(shù)組賦初值的簡(jiǎn)化寫法也是語法糖哦
for (int e : array) {
System.out.println(e);
}
}
}
會(huì)被編譯器轉(zhuǎn)換為:
public class Candy5_1 {
public Candy5_1() {
}
public static void main(String[] args) {
int[] array = new int[]{1, 2, 3, 4, 5};
for(int i = 0; i < array.length; ++i) {
int e = array[i]; System.out.println(e);
}
}
}
而集合循環(huán):
public class Candy5_2 {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
for (Integer i : list) {
System.out.println(i);
}
}
}
實(shí)際被編譯器轉(zhuǎn)換為對(duì)迭代器的調(diào)用:
public class Candy5_2 {
public Candy5_2() { }
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
Iterator iter = list.iterator(); //獲取迭代器
while(iter.hasNext()) {
Integer e = (Integer)iter.next();
System.out.println(e);
}
}
}
注意 foreach 循環(huán)寫法,能夠配合數(shù)組彩扔,以及所有實(shí)現(xiàn)了 Iterable 接口的集合類一起使用妆档,其
中 Iterable 用來獲取集合的迭代器( Iterator )
3.6 switch 字符串
從 JDK 7 開始,switch 可以作用于字符串和枚舉類虫碉,這個(gè)功能其實(shí)也是語法糖过吻,例如:
public class Candy6_1 {
public static void choose(String str) {
switch (str) {
case "hello": {
System.out.println("h");
break;
}
case "world": {
System.out.println("w");
break;
}
}
}
}
public class Candy6_1 {
public Candy6_1() {
}
public static void choose(String str) {
byte x = -1;
switch(str.hashCode()) {
case 99162322: // hello 的 hashCode
if (str.equals("hello")) {
x = 0;
}
break;
case 113318802: // world 的 hashCode
if (str.equals("world")) {
x = 1;
}
}
switch(x) {
case 0:
System.out.println("h");
break;
case 1:
System.out.println("w");
}
}
}
可以看到,執(zhí)行了兩遍 switch蔗衡,第一遍是根據(jù)字符串的 hashCode 和 equals 將字符串的轉(zhuǎn)換為相應(yīng)
byte 類型,第二遍才是利用 byte 執(zhí)行進(jìn)行比較乳绕。
為什么第一遍時(shí)必須既比較 hashCode绞惦,又利用 equals 比較呢?hashCode 是為了提高效率洋措,減少可
能的比較济蝉;而 equals 是為了防止 hashCode 沖突,例如 BM 和 C. 這兩個(gè)字符串的hashCode值都是
2123 菠发,如果有如下代碼:
public class Candy6_2 {
public static void choose(String str) {
switch (str) {
case "BM": {
System.out.println("h");
break;
}
case "C.": {
System.out.println("w");
break;
}
}
}
}
編譯后:
public class Candy6_2 {
public Candy6_2() {
}
public static void choose(String str) {
byte x = -1; switch(str.hashCode()) {
case 2123: // hashCode 值可能相同王滤,需要進(jìn)一步用 equals 比較
if (str.equals("C.")) {
x = 1;
} else if (str.equals("BM")) {
x = 0;
}
default:
switch(x) {
case 0:
System.out.println("h");
break;
case 1:
System.out.println("w");
}
}
}
}
3.7 switch 枚舉類
switch 枚舉的例子,原始代碼:
enum Sex {
MALE, FEMALE
}
public class Candy7 {
public static void foo(Sex sex) {
switch (sex) {
case MALE:
System.out.println("男");
break;
case FEMALE:
System.out.println("女");
break;
}
}
}
轉(zhuǎn)換后:
public class Candy7 {
/**
* 定義一個(gè)合成類(僅 jvm 使用滓鸠,對(duì)我們不可見)
* 用來映射枚舉的 ordinal 與數(shù)組元素的關(guān)系
* 枚舉的 ordinal 表示枚舉對(duì)象的序號(hào)雁乡,從 0 開始
* 即 MALE 的 ordinal()=0,F(xiàn)EMALE 的 ordinal()=1
*/
static class $MAP {
// 數(shù)組大小即為枚舉元素個(gè)數(shù)糜俗,里面存儲(chǔ)case用來對(duì)比的數(shù)字
static int[] map = new int[2];
static {
map[Sex.MALE.ordinal()] = 1;
map[Sex.FEMALE.ordinal()] = 2;
}
}
public static void foo(Sex sex) {
int x = $MAP.map[sex.ordinal()];
switch (x) {
case 1:
System.out.println("男");
break;
case 2:
System.out.println("女");
break;
}
}
}
3.8 枚舉類
JDK 7 新增了枚舉類踱稍,以前面的性別枚舉為例:
enum Sex {
MALE, FEMALE
}
轉(zhuǎn)換后代碼:
public final class Sex extends Enum<Sex> {
public static final Sex MALE;
public static final Sex FEMALE;
private static final Sex[] $VALUES;
static {
MALE = new Sex("MALE", 0);
FEMALE = new Sex("FEMALE", 1);
$VALUES = new Sex[]{MALE, FEMALE};
}
private Sex(String name, int ordinal) {
super(name, ordinal);
}
public static Sex[] values() {
return $VALUES.clone();
}
public static Sex valueOf(String name) {
return Enum.valueOf(Sex.class, name);
}
}
3.9 try-with-resourses
JDK 7 開始新增了對(duì)需要關(guān)閉的資源處理的特殊語法 try-with-resources`:
try(資源變量 = 創(chuàng)建資源對(duì)象){
}
catch( ) {
}
其中資源對(duì)象需要實(shí)現(xiàn) AutoCloseable
接口曲饱,例如 InputStream
、 OutputStream
珠月、
Connection
扩淀、 Statement
、 ResultSet
等接口都實(shí)現(xiàn)了 AutoCloseable
啤挎,使用 try-with-
resources 可以不用寫 finally 語句塊驻谆,編譯器會(huì)幫助生成關(guān)閉資源代碼,例如:
public class Candy9 {
public static void main(String[] args) {
try(InputStream is = new FileInputStream("d:\\1.txt")) {
System.out.println(is);
} catch (IOException e) {
e.printStackTrace();
}
}
}
會(huì)被轉(zhuǎn)換為:
public class Candy9 {
public Candy9() {
}
public static void main(String[] args) {
try {
InputStream is = new FileInputStream("d:\\1.txt");
Throwable t = null;
try {
System.out.println(is);
}
catch (Throwable e1) {
// t 是我們代碼出現(xiàn)的異常
t = e1;
throw e1;
} finally {
// 判斷了資源不為空
if (is != null) {
// 如果我們代碼有異常
if (t != null) {
try {
is.close();
}
catch (Throwable e2) {
// 如果 close 出現(xiàn)異常庆聘,作為被壓制異常添加
t.addSuppressed(e2);
}
} else {
// 如果我們代碼沒有異常胜臊,close 出現(xiàn)的異常就是最后 catch 塊中的 e
is.close();
}
}
}
}
catch (IOException e) {
e.printStackTrace();
}
}
}
為什么要設(shè)計(jì)一個(gè) addSuppressed(Throwable e) (添加被壓制異常)的方法呢?是為了防止異常信
息的丟失(想想 try-with-resources 生成的 fifianlly 中如果拋出了異常):
public class Test6 {
public static void main(String[] args) {
try (MyResource resource = new MyResource()) {
int i = 1/0;
} catch (Exception e) {
e.printStackTrace();
}
}
}
class MyResource implements AutoCloseable {
public void close() throws Exception {
throw new Exception("close 異常");
}
}
輸出:異常都不會(huì)丟
java.lang.ArithmeticException: / by zero
at test.Test6.main(Test6.java:7)
Suppressed: java.lang.Exception: close 異常
at test.MyResource.close(Test6.java:18)
at test.Test6.main(Test6.java:6)
3.10 方法重新時(shí)的橋接方法
我們都知道掏觉,方法重寫時(shí)對(duì)返回值分兩種情況:
父子類的返回值完全一致
子類返回值可以是父類返回值的子類(比較繞口区端,見下面的例子)
class A {
public Number m() {
return 1;
}
}
class B extends A {
@Override // 子類 m 方法的返回值是 Integer 是父類 m 方法返回值 Number 的子類
public Integer m() {
return 2;
}
}
對(duì)于子類,java 編譯器會(huì)做如下處理:
class B extends A {
public Integer m() {
return 2;
}
// 此方法才是真正重寫了父類 public Number m() 方法
public synthetic bridge Number m() {
// 調(diào)用 public Integer m()
return m();
}
}
其中橋接方法比較特殊澳腹,僅對(duì) java 虛擬機(jī)可見织盼,并且與原來的 public Integer m() 沒有命名沖突,可以
用下面反射代碼來驗(yàn)證:
for (Method m : B.class.getDeclaredMethods()) {
System.out.println(m);
}
會(huì)輸出:
public java.lang.Integer test.candy.B.m()
public java.lang.Number test.candy.B.m()
3.11 匿名內(nèi)部類
源代碼:
public class Candy11 {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("ok");
}
};
}
}
轉(zhuǎn)換后的代碼:
// 額外生成的類
final class Candy11$1 implements Runnable {
Candy11$1() {
}
public void run() {
System.out.println("ok");
}
}
public class Candy11 {
public static void main(String[] args) {
Runnable runnable = new Candy11$1();
}
}
引用局部變量的匿名內(nèi)部類酱塔,源代碼:
public class Candy11 {
public static void test(final int x) {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("ok:" + x);
}
};
}
}
轉(zhuǎn)換后代碼:
// 額外生成的類
final class Candy11$1 implements Runnable {
int val$x;
Candy11$1(int x) {
this.val$x = x;
}
public void run() {
System.out.println("ok:" + this.val$x);
}
}
public class Candy11 {
public static void test(final int x) {
Runnable runnable = new Candy11$1(x);
}
}
注意 這同時(shí)解釋了為什么匿名內(nèi)部類引用局部變量時(shí)沥邻,局部變量必須是 final 的:
因?yàn)樵趧?chuàng)建 Candy11 $1 對(duì)象時(shí),將 x 的值賦值給了 Candy11 $1 對(duì)象的 val
屬 性 羊娃, 不 應(yīng) 該 再 發(fā) 生 變 化 了 唐全, 如 果 變 化, 那 么 val$x 屬性沒有機(jī)會(huì)再發(fā)生變化
4.類加載階段
4.1 加載
將類的字節(jié)碼載入方法區(qū)中蕊玷,內(nèi)部采用 C++ 的 instanceKlass 描述 java 類邮利,它的重要 field 有:
_java_mirror 即 java 的類鏡像,例如對(duì) String 來說垃帅,就是 String.class延届,作用是把 Kclass 暴露給 java 使用
_super 即父類
_fields 即成員變量
_methods 即方法
_constants 即常量池
_class_loader 即類加載器
_vtable 虛方法表
_itable 接口方法表
如果這個(gè)類還有父類沒有加載,先加載父類
加載和鏈接可能是交替運(yùn)行的
-
注意
instanceKlass 這樣的【元數(shù)據(jù)】是存儲(chǔ)在方法區(qū)(1.8 后的元空間內(nèi))贸诚,但 _java_mirror是存儲(chǔ)在堆中
可以通過前面介紹的 HSDB 工具查看
4.2 鏈接
驗(yàn)證
驗(yàn)證類是否符合 JVM規(guī)范方庭,安全性檢查
用 UE 等支持二進(jìn)制的編輯器修改 HelloWorld.class 的魔數(shù)( 修改 ca fe ba by ),在控制臺(tái)運(yùn)行
準(zhǔn)備
為 static 變量分配空間酱固,設(shè)置默認(rèn)值
static 變量在 JDK 7 之前存儲(chǔ)于 instanceKlass 末尾械念,從 JDK 7 開始,存儲(chǔ)于 _java_mirror 末尾(在堆)
static 變量分配空間和賦值是兩個(gè)步驟运悲,分配空間在準(zhǔn)備階段完成龄减,賦值在初始化階段完成
如果 static 變量是 final 的基本類型,以及字符串常量扇苞,那么編譯階段值就確定了欺殿,賦值在準(zhǔn)備階段完成
如果 static 變量是 final 的寄纵,但屬于引用類型,那么賦值也會(huì)在初始化階段完成
public class Load8 {
static int a;
static int b = 10;
static final int c = 20;
static final String d = "hello";
static final Object e = new Object();
}
字節(jié)碼
{
static int a;
descriptor: I
flags: ACC_STATIC
static int b;
descriptor: I
flags: ACC_STATIC //準(zhǔn)備階段先分配空間脖苏,然后在初始化階段才賦值
static final int c;
descriptor: I
flags: ACC_STATIC, ACC_FINAL
ConstantValue: int 20 //final 直接賦值
static final java.lang.String d;
descriptor: Ljava/lang/String;
flags: ACC_STATIC, ACC_FINAL
ConstantValue: String hello //finale 直接負(fù)責(zé)
static final java.lang.Object e;
descriptor: Ljava/lang/Object;
flags: ACC_STATIC, ACC_FINAL //引用類型程拭,那么賦值也會(huì)在初始化階段完成
public cn.itcast.jvm.t3.load.Load8();
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/itcast/jvm/t3/load/Load8;
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: bipush 10
2: putstatic #2 // Field b:I
5: new #3 // class java/lang/Object
8: dup
9: invokespecial #1 // Method java/lang/Object."<init>":()V
12: putstatic #4 // Field e:Ljava/lang/Object;
15: return
LineNumberTable:
line 7: 0
line 10: 5
}
解析
將常量池中的符號(hào)引用解析為直接引用
/**
* 解析的含義
*/
public class Load2 {
public static void main(String[] args) throws ClassNotFoundException, IOException { ClassLoader classloader = Load2.class.getClassLoader();
//loadClass 方法不會(huì)導(dǎo)致類的解析和初始化
Class<?> c = classloader.loadClass("cn.itcast.jvm.t3.load.C");
// new C();
//只有通過new 方式,才能實(shí)現(xiàn)對(duì)類的解析
System.in.read();
}
}
class C {
D d = new D();
}
class D {
}
4.3 初始化
<cinit>
()V 方法
初始化即調(diào)用 <cinit>
()V 棍潘,虛擬機(jī)會(huì)保證這個(gè)類的『構(gòu)造方法』的線程安全
發(fā)生的時(shí)機(jī)
概括得說恃鞋,類初始化是【懶惰的】
導(dǎo)致類初始化的情況
main 方法所在的類,總會(huì)被首先初始化
首次訪問這個(gè)類的靜態(tài)變量或靜態(tài)方法時(shí)
子類初始化亦歉,如果父類還沒初始化恤浪,會(huì)引發(fā)
子類訪問父類的靜態(tài)變量,只會(huì)觸發(fā)父類的初始化
Class.forName
new 會(huì)導(dǎo)致初始化
不會(huì)導(dǎo)致類初始化的情況
訪問類的 static final 靜態(tài)常量(基本類型和字符串)不會(huì)觸發(fā)初始化
類對(duì)象.class 不會(huì)觸發(fā)初始化
創(chuàng)建該類的數(shù)組不會(huì)觸發(fā)初始化
類加載器的 loadClass 方法
Class.forName 的參數(shù) 2 為 false 時(shí)
public class Load3 {
static {
System.out.println("main init");
}
public static void main(String[] args) throws ClassNotFoundException, IOException {
// // 1. 靜態(tài)常量不會(huì)觸發(fā)初始化
// System.out.println(B.b);
// // 2. 類對(duì)象.class 不會(huì)觸發(fā)初始化
// System.out.println(B.class);
// // 3. 創(chuàng)建該類的數(shù)組不會(huì)觸發(fā)初始化
// System.out.println(new B[0]);
// 4. 不會(huì)初始化類 B肴楷,但會(huì)加載 B水由、A
ClassLoader cl = Thread.currentThread().getContextClassLoader();
cl.loadClass("cn.itcast.jvm.t3.load.B");
// // 5. 不會(huì)初始化類 B,但會(huì)加載 B赛蔫、A
// ClassLoader c2 = Thread.currentThread().getContextClassLoader();
// Class.forName("cn.itcast.jvm.t3.load.B", false, c2);
System.in.read();
// // 1. 首次訪問這個(gè)類的靜態(tài)變量或靜態(tài)方法時(shí)
// System.out.println(A.a);
// // 2. 子類初始化砂客,如果父類還沒初始化,會(huì)引發(fā)
// System.out.println(B.c);
// // 3. 子類訪問父類靜態(tài)變量呵恢,只觸發(fā)父類初始化
// System.out.println(B.a);
// // 4. 會(huì)初始化類 B鞠值,并先初始化類 A
// Class.forName("cn.itcast.jvm.t3.load.B");
}
}
class A {
static int a = 0;
static {
System.out.println("a init");
}
}
class B extends A {
final static double b = 5.0;
static boolean c = false;
static {
System.out.println("b init");
}
}
4.4 練習(xí)
從字節(jié)碼分析,使用 a渗钉,b彤恶,c 這三個(gè)常量是否會(huì)導(dǎo)致 E 初始化
public class Load4 {
public static void main(String[] args) {
System.out.println(E.a);
System.out.println(E.b);
System.out.println(E.c);
}
}
class E {
public static final int a = 10;
public static final String b = "hello";
public static final Integer c = 20; // Integer.valueOf(20) 會(huì)導(dǎo)致初始化
static {
System.out.println("init E");
}
}
//結(jié)果
10
hello
init E
20
典型應(yīng)用 - 完成懶惰初始化單例模式
public class Load9 {
public static void main(String[] args) {
// Singleton.test(); //結(jié)果,不會(huì)調(diào)用初始化
//結(jié)果時(shí)執(zhí)行了一次初始化鳄橘,第二次調(diào)用類以及初始化声离,直接返回
Singleton.getInstance();
Singleton.getInstance();
}
}
class Singleton {
public static void test() {
System.out.println("test");
}
private Singleton() {}
private static class LazyHolder{
private static final Singleton SINGLETON = new Singleton();
static {
System.out.println("lazy holder init");
}
}
public static Singleton getInstance() {
return LazyHolder.SINGLETON;
}
}
5.類加載器
以 JDK 8 為例:
名稱 | 加載那些類 | 說明 |
---|---|---|
Bootstrap ClassLoader | JAVA_HOME/jre/lib | 無法直接訪問 |
Extension ClassLoader | JAVA_HOME/jre/lib/ext | 上級(jí)為 Bootstrap,顯示為 null |
Application ClassLoader | classpath | 上級(jí)為 Extension |
自定義類加載器 | 自定義 | 上級(jí)為 Application |
5.1 啟動(dòng)類加載器
用Boorstrap 類加載器加載類:
public class F {
static {
System.out.println("bootstrap F init");
}
}
執(zhí)行
public class Load5_1 {
public static void main(String[] args) throws ClassNotFoundException {
Class<?> aClass = Class.forName("cn.itcast.jvm.t3.load.F");
System.out.println(aClass.getClassLoader()); // AppClassLoader ExtClassLoader
}
}
輸出
E:\git\jvm\out\production\jvm>java -Xbootclasspath/a:.
cn.itcast.jvm.t3.load.Load5
bootstrap F init
null //由啟動(dòng)類加載器加載的
-Xbootclasspath 表示設(shè)置 bootclasspath
其中 /a:. 表示將當(dāng)前目錄追加至 bootclasspath 之后
可以用這個(gè)辦法替換核心類
java -Xbootclasspath:<new bootclasspath>
java -Xbootclasspath/a:<追加路徑>
java -Xbootclasspath/p:<追加路徑>
5.2 拓展類加載器
public class G {
static {
System.out.println("classpath G init");
}
}
/**
* 演示 擴(kuò)展類加載器
* 在 C:\Program Files\Java\jdk1.8.0_91 下有一個(gè) my.jar
* 里面也有一個(gè) G 的類瘫怜,觀察到底是哪個(gè)類被加載了
*/
public class Load5_2 {
public static void main(String[] args) throws ClassNotFoundException {
Class<?> aClass = Class.forName("cn.itcast.jvm.t3.load.G");
System.out.println(aClass.getClassLoader());
}
}
輸出
classpath G init
sun.misc.Launcher$AppClassLoader@18b4aac2
寫一個(gè)同名的類
public class G {
static {
System.out.println("ext G init");
}
}
打個(gè) jar 包
E:\git\jvm\out\production\jvm>jar -cvf my.jar cn/itcast/jvm/t3/load/G.class
已添加清單
正在添加: cn/itcast/jvm/t3/load/G.class(輸入 = 481) (輸出 = 322)(壓縮了 33%)
將 jar 包拷貝到 JAVA_HOME/jre/lib/ext
重新執(zhí)行 Load5_2
輸出
ext G init
sun.misc.Launcher$ExtClassLoader@29453f44
5.3 雙親委派機(jī)制
所謂的雙親委派抵恋,就是指調(diào)用類加載器的 loadClass 方法時(shí),查找類的規(guī)則
注意
這里的雙親宝磨,翻譯為上級(jí)似乎更為合適纹坐,因?yàn)樗鼈儾]有繼承關(guān)系
ClassLoader類中的 loadClass 方法
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
//1.檢查該類是否已經(jīng)加載
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
//2.有上級(jí)的話灭美,委派上級(jí) loadClass
c = parent.loadClass(name, false);
} else {
//3.如果沒有上級(jí)了(ExtClassLoader),則委派BootstrapClassLoader
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
//4.每一層找不到痕惋,調(diào)用findClass 方法(每個(gè)類加載器直接拓展)來加載
c = findClass(name);
//5.記錄耗時(shí)
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
5.4 線程上下文類加載器
我們?cè)谑褂?JDBC 時(shí)私恬,都需要加載 Driver 驅(qū)動(dòng)拙吉,不知道你注意到?jīng)]有脊僚,不寫
Class.forName("com.mysql.jdbc.Driver")
也是可以讓 com.mysql.jdbc.Driver
正確加載的顶燕,你知道是怎么做的嗎庇麦?
讓我們追蹤一下源碼:
public class DriverManager {
//注冊(cè)驅(qū)動(dòng)的集合
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
private DriverManager(){}
//初始化驅(qū)動(dòng)
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
先不看別的蝙寨,看看 DriverManager 的類加載器:
System.out.println(DriverManager.class.getClassLoader());
打印 null晒衩,表示它的類加載器是 Bootstrap ClassLoader嗤瞎,會(huì)到 JAVA_HOME/jre/lib 下搜索類,但
JAVA_HOME/jre/lib 下顯然沒有 mysql-connector-java-5.1.47.jar 包听系,這樣問題來了贝奇,在
DriverManager 的靜態(tài)代碼塊中,怎么能正確加載 com.mysql.jdbc.Driver 呢靠胜?
繼續(xù)看 loadInitialDrivers() 方法:
private static void loadInitialDrivers() {
String drivers;
try {
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
// 1)使用 ServiceLoader 機(jī)制加載驅(qū)動(dòng)掉瞳,即 SPI
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
println("DriverManager.initialize: jdbc.drivers = " + drivers);
// 2)使用 jdbc.drivers 定義的驅(qū)動(dòng)名加載驅(qū)動(dòng)
if (drivers == null || drivers.equals("")) {
return;
}
String[] driversList = drivers.split(":");
println("number of Drivers:" + driversList.length);
for (String aDriver : driversList) {
try {
println("DriverManager.Initialize: loading " + aDriver);
// 這里的 ClassLoader.getSystemClassLoader() 就是應(yīng)用程序類加載器
Class.forName(aDriver, true,ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}
}
先看 2)發(fā)現(xiàn)它最后是使用 Class.forName 完成類的加載和初始化,關(guān)聯(lián)的是應(yīng)用程序類加載器浪漠,因此
可以順利完成類加載
再看 1)它就是大名鼎鼎的 Service Provider Interface (SPI)
約定如下陕习,在 jar 包的 META-INF/services 包下,以接口全限定名名為文件址愿,文件內(nèi)容是實(shí)現(xiàn)類名稱
這樣就可以使用
ServiceLoader<接口類型> allImpls = ServiceLoader.load(接口類型.class);
Iterator<接口類型> iter = allImpls.iterator();
while(iter.hasNext()) {
iter.next();
}
來得到實(shí)現(xiàn)類该镣,體現(xiàn)的是【面向接口編程+解耦】的思想,在下面一些框架中都運(yùn)用了此思想:
JDBC
Servlet 初始化器
Spring 容器
Dubbo(對(duì) SPI 進(jìn)行了擴(kuò)展)
接著看 ServiceLoader.load 方法:
public static <S> ServiceLoader<S> load(Class<S> service) {
// 獲取線程上下文類加載器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
線程上下文類加載器是當(dāng)前線程使用的類加載器响谓,默認(rèn)就是應(yīng)用程序類加載器损合,它內(nèi)部又是由Class.forName 調(diào)用了線程上下文類加載器完成類加載,具體代碼在 ServiceLoader 的內(nèi)部類LazyIterator 中:
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,
"Provider " + cn + " not found");
}
if (!service.isAssignableFrom(c)) {
fail(service,
"Provider " + cn + " not a subtype");
}
try {
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}
5.5 自定義類加載器
問問自己歌粥,什么時(shí)候需要自定義類加載器
1)想加載非 classpath 隨意路徑中的類文件
2)都是通過接口來使用實(shí)現(xiàn)塌忽,希望解耦時(shí),常用在框架設(shè)計(jì)
3)這些類希望予以隔離失驶,不同應(yīng)用的同名類都可以加載土居,不沖突,常見于 tomcat 容器
步驟:
繼承 ClassLoader 父類
要遵從雙親委派機(jī)制嬉探,重寫 findClass 方法
? 注意不是重寫 loadClass 方法擦耀,否則不會(huì)走雙親委派機(jī)制
讀取類文件的字節(jié)碼
調(diào)用父類的 defineClass 方法來加載類
使用者調(diào)用該類加載器的 loadClass 方法