開(kāi)篇就提到效能優(yōu)化涉及的范圍會(huì)很廣,考慮后面需要經(jīng)常用到 asm 字節(jié)碼插樁,我們首先從 《Gradle 插件 + ASM 實(shí)戰(zhàn)》開(kāi)始講,但又希望大家能知其然也知其所以然,因此我們首先得講下 JVM 虛擬機(jī)加載 Class 字節(jié)碼的原理。這往往也是我面試新同學(xué)必問(wèn)的一個(gè)內(nèi)容,因?yàn)槿绻麑?duì)這個(gè)不了解的話,像插件化與熱修復(fù)、性能優(yōu)化、覆蓋率統(tǒng)計(jì)等等很多功能都是不好實(shí)現(xiàn)的。小公司很少有人用,這也是實(shí)話,至于大家要不要學(xué),這就看個(gè)人情況了,其實(shí)也不是用不用得上的問(wèn)題,就看大家愿不愿意做一個(gè)吃螃蟹的人。我們主要從以下三個(gè)方面來(lái)說(shuō):
1. class 文件字節(jié)碼結(jié)構(gòu)
1.1 class 字節(jié)碼示例
我們先來(lái)看一個(gè)非常簡(jiǎn)單的 HelloWorld.java
public class HelloWorld {
public HelloWorld() {
}
public static void main(String[] args) {
System.out.println("Hello World!");
}
}
用文本編輯器打開(kāi)生成的 HelloWorld.class 文件,是這樣的:
cafe babe 0000 0033 0022 0a00 0600 1409
0015 0016 0800 170a 0018 0019 0700 1a07
001b 0100 063c 696e 6974 3e01 0003 2829
5601 0004 436f 6465 0100 0f4c 696e 654e
756d 6265 7254 6162 6c65 0100 124c 6f63
616c 5661 7269 6162 6c65 5461 626c 6501
0004 7468 6973 0100 264c 636f 6d2f 6578
616d 706c 652f 6d79 6170 706c 6963 6174
696f 6e2f 4865 6c6c 6f57 6f72 6c64 3b01
0004 6d61 696e 0100 1628 5b4c 6a61 7661
2f6c 616e 672f 5374 7269 6e67 3b29 5601
0004 6172 6773 0100 135b 4c6a 6176 612f
6c61 6e67 2f53 7472 696e 673b 0100 0a53
6f75 7263 6546 696c 6501 000f 4865 6c6c
6f57 6f72 6c64 2e6a 6176 610c 0007 0008
0700 1c0c 001d 001e 0100 0c48 656c 6c6f
2057 6f72 6c64 2107 001f 0c00 2000 2101
0024 636f 6d2f 6578 616d 706c 652f 6d79
6170 706c 6963 6174 696f 6e2f 4865 6c6c
6f57 6f72 6c64 0100 106a 6176 612f 6c61
6e67 2f4f 626a 6563 7401 0010 6a61 7661
2f6c 616e 672f 5379 7374 656d 0100 036f
7574 0100 154c 6a61 7661 2f69 6f2f 5072
696e 7453 7472 6561 6d3b 0100 136a 6176
612f 696f 2f50 7269 6e74 5374 7265 616d
0100 0770 7269 6e74 6c6e 0100 1528 4c6a
6176 612f 6c61 6e67 2f53 7472 696e 673b
2956 0021 0005 0006 0000 0000 0002 0001
0007 0008 0001 0009 0000 002f 0001 0001
0000 0005 2ab7 0001 b100 0000 0200 0a00
0000 0600 0100 0000 0a00 0b00 0000 0c00
0100 0000 0500 0c00 0d00 0000 0900 0e00
0f00 0100 0900 0000 3700 0200 0100 0000
09b2 0002 1203 b600 04b1 0000 0002 000a
0000 000a 0002 0000 000c 0008 000d 000b
0000 000c 0001 0000 0009 0010 0011 0000
0001 0012 0000 0002 0013
好家伙,這怎么能夠看得懂?但是既然 java 虛擬機(jī)能夠看懂,我們也可以想辦法看懂,用 javap -verbose HelloWorld.class 看起來(lái)就稍微簡(jiǎn)單一點(diǎn):
Last modified 2021-1-7; size 586 bytes
MD5 checksum bf91e508b76a0dc7d4c0250b0e55f75b
Compiled from "HelloWorld.java"
public class com.example.myapplication.HelloWorld
minor version: 0
major version: 51
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 // com/example/myapplication/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 Lcom/example/myapplication/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 com/example/myapplication/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 com.example.myapplication.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 10: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/example/myapplication/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 12: 0
line 13: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
}
1.2 類(lèi)文件結(jié)構(gòu)
.class 文件是一組以 8 位字節(jié)為基礎(chǔ)單位的二進(jìn)制流,各數(shù)據(jù)項(xiàng)目嚴(yán)格按照順序緊湊地排列在 .class 文件中,中間沒(méi)有添加任何分隔符,這使得整個(gè) .class 文件中存儲(chǔ)的內(nèi)容幾乎全都是程序需要的數(shù)據(jù),沒(méi)有空隙存在。至于具體有哪些內(nèi)容,這里有一張表大家可以參考。

虛擬機(jī)加載 .class 文件,就是按照上面這樣的規(guī)則去解析,最終解析的結(jié)果大致就是 javap -verbose 命令所生成的那樣,如果大家只是閱讀文章的話,建議大家自己要一點(diǎn)一點(diǎn)去嘗試解析下,當(dāng)然直播上我會(huì)帶大家一起來(lái)看。
2. jvm 類(lèi)的加載機(jī)制
2.1 類(lèi)的加載時(shí)機(jī)
在 JVM 虛擬機(jī)規(guī)范中并沒(méi)有規(guī)定加載的時(shí)機(jī),但是卻規(guī)定了初始化的時(shí)機(jī),有以下五種情況需要必須立即對(duì)類(lèi)進(jìn)行初始化:
- 遇到 new、getstatic、putstatic 或 invokestatic 這 4 條字節(jié)碼指令時(shí),如果類(lèi)沒(méi)有進(jìn)行過(guò)初始化,則需要先觸發(fā)其初始化。生成這 4 條指令最常見(jiàn)的 Java 代碼場(chǎng)景是:使用 new 關(guān)鍵字實(shí)例化對(duì)象、讀取或設(shè)置一個(gè)類(lèi)的靜態(tài)字段(被 final 修飾、已在編譯期把結(jié)果放入到常量池的靜態(tài)字段除外)以及調(diào)用一個(gè)類(lèi)的靜態(tài)方法的時(shí)候
- 使用 java.lang.reflect 包的方法對(duì)類(lèi)進(jìn)行反射調(diào)用的時(shí)候
- 當(dāng)初始化一個(gè)類(lèi)的時(shí)候,如果發(fā)現(xiàn)其父類(lèi)還沒(méi)有被初始化過(guò),則需要先觸發(fā)其父類(lèi)的初始化
- 當(dāng)虛擬機(jī)啟動(dòng)時(shí),用戶需要指定一個(gè)要執(zhí)行的主類(lèi)(包含 main() 方法的類(lèi)),虛擬機(jī)會(huì)先初始化這個(gè)主類(lèi)
- 當(dāng)使用 JDK 1.7 的動(dòng)態(tài)語(yǔ)言支持時(shí),如果一個(gè) java.lang.invoke.MethodHandle 實(shí)例最后的解析結(jié)果 REF_getStatic、REF_putStatic、REF_invodeStatic 的方法句柄,并且這個(gè)方法句柄所對(duì)應(yīng)的類(lèi)沒(méi)有進(jìn)行過(guò)初始化,則需要先觸發(fā)其初始化。
2.2 類(lèi)的加載流程
類(lèi)的加載過(guò)程大致分為 5 個(gè)步驟:加載、驗(yàn)證、準(zhǔn)備、解析和初始化,作為過(guò)來(lái)人早期我犯過(guò)很?chē)?yán)重的錯(cuò)誤,那就是為了面試習(xí)慣背,這樣過(guò)段時(shí)間發(fā)現(xiàn)很容易忘記,而且開(kāi)發(fā)中遇到類(lèi)似的問(wèn)題往往不知所措,因此希望大家能好好的理解理解,這樣才能做到一勞永逸:
2.2.1 加載
- 通過(guò)一個(gè)類(lèi)的全限定名獲取定義此類(lèi)的二進(jìn)制字節(jié)流
- 將二進(jìn)制字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)換為方法區(qū)中的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)
- 在內(nèi)存中生成一個(gè)代表此類(lèi)的 java.lang.Class 的對(duì)象,作為方法區(qū)中這個(gè)類(lèi)的訪問(wèn)入口
- jvm 虛擬機(jī)并沒(méi)有規(guī)定從哪里獲取二進(jìn)制字節(jié)流。我們可以從 .class 靜態(tài)存儲(chǔ)文件中獲取,也可以從 apk、zip、jar 等包中讀取,可以從數(shù)據(jù)庫(kù)中讀取,也可以從網(wǎng)絡(luò)中獲取,甚至我們自己可以在運(yùn)行時(shí)自動(dòng)生成。
- 在內(nèi)存中實(shí)例化一個(gè)代表此類(lèi)的 java.lang.Class 對(duì)象之后,并沒(méi)有規(guī)定此 Class 對(duì)象是方法 Java 堆中的,有些虛擬機(jī)就會(huì)將 Class 對(duì)象放到方法區(qū)中,比如 HotSpot,一個(gè) ClassLoader 只會(huì)實(shí)例化一個(gè) Class 對(duì)象。
2.2.2 驗(yàn)證
- 文件格式驗(yàn)證:主要驗(yàn)證二進(jìn)制字節(jié)流數(shù)據(jù)是否符合 .class 文件的規(guī)范,并且該 .class 文件是否在本虛擬機(jī)的處理范圍之內(nèi)(版本號(hào)驗(yàn)證)。只有通過(guò)了文件格式的驗(yàn)證之后,二進(jìn)制的字節(jié)流才會(huì)進(jìn)入到內(nèi)存中的方法區(qū)進(jìn)行存儲(chǔ)。而且只有通過(guò)了文件格式驗(yàn)證之后,才會(huì)進(jìn)行后面三個(gè)驗(yàn)證,后面三個(gè)驗(yàn)證都是基于方法區(qū)中的存儲(chǔ)結(jié)構(gòu)進(jìn)行的
- 元數(shù)據(jù)驗(yàn)證:主要是對(duì)類(lèi)的元數(shù)據(jù)信息進(jìn)行語(yǔ)義檢查,保證不存在不符合 Java 語(yǔ)義規(guī)范的元數(shù)據(jù)信息
- 字節(jié)碼驗(yàn)證:字節(jié)碼驗(yàn)證是整個(gè)驗(yàn)證中最復(fù)雜的一個(gè)過(guò)程,在元數(shù)據(jù)驗(yàn)證中,驗(yàn)證了元數(shù)據(jù)信息中的數(shù)據(jù)類(lèi)型做完校驗(yàn)后,字節(jié)碼驗(yàn)證主要對(duì)類(lèi)的方法體進(jìn)行校驗(yàn)分析,保證被校驗(yàn)的類(lèi)的方法不會(huì)做出危害虛擬機(jī)的行為
- 符號(hào)引用驗(yàn)證:符號(hào)引用驗(yàn)證發(fā)生在連接的第三個(gè)階段解析階段中,主要是保證解析過(guò)程可以正確地執(zhí)行。符號(hào)引用驗(yàn)證是類(lèi)本身引用的其他類(lèi)的驗(yàn)證,包括:通過(guò)一個(gè)類(lèi)的全限定名是否可以找到對(duì)應(yīng)的類(lèi),訪問(wèn)的其他類(lèi)中的字段和方法是否存在,并且訪問(wèn)性是否合適等
2.2.3 準(zhǔn)備
- 在方法區(qū)中分配內(nèi)存的只有類(lèi)變量(被 static 修飾的變量),而不包括實(shí)例變量,實(shí)例變量將會(huì)跟隨著對(duì)象在 Java 堆中為其分配內(nèi)存
- 初始化類(lèi)變量的時(shí)候,是將類(lèi)變量初始化為其類(lèi)型對(duì)應(yīng)的 0 值,比如有如下類(lèi)變量,在準(zhǔn)備階段完成之后,val 的值是 0 而不是設(shè)置,為 val 復(fù)制為具體值,是在初始化階段
- 對(duì)于常量,其對(duì)應(yīng)的值會(huì)在編譯階段就存儲(chǔ)在字段表的 ConstantValue 屬性當(dāng)中,所以在準(zhǔn)備階段結(jié)束之后,常量的值就是 ConstantValue 所指定的值了。
2.2.4 解析
- 虛擬機(jī)規(guī)范中并未規(guī)定解析階段發(fā)生的具體時(shí)間,只規(guī)定了在執(zhí)行newarray、new、putfidle、putstatic、getfield、getstatic 等 16 個(gè)指令之前,對(duì)它們所使用的符號(hào)引用進(jìn)行解析。所以虛擬機(jī)可以在類(lèi)被加載器加載之后就進(jìn)行解析,也可以在執(zhí)行這幾個(gè)指令之前才進(jìn)行解析
- 對(duì)同一個(gè)符號(hào)引用進(jìn)行多次解析是很常見(jiàn)的事,除 invokedynamic 指令以外,虛擬機(jī)實(shí)現(xiàn)可以對(duì)第一次解析的結(jié)果進(jìn)行緩存,以后解析相同的符號(hào)引用時(shí),只要取緩存的結(jié)果就可以了
- 解析動(dòng)作主要對(duì)類(lèi)或接口、字段、類(lèi)方法、接口方法、方法類(lèi)型、方法句柄和調(diào)用點(diǎn)限定符 7 類(lèi)符號(hào)引用進(jìn)行解析
2.2.5 初始化
- 類(lèi)構(gòu)造器 <clinit>() 是由編譯器自動(dòng)收集類(lèi)中出現(xiàn)的類(lèi)變量、靜態(tài)代碼塊中的語(yǔ)句合并產(chǎn)生的,收集的順序是在源文件中出現(xiàn)的順序決定的,靜態(tài)代碼塊可以訪問(wèn)出現(xiàn)在靜態(tài)代碼塊之前的類(lèi)變量,出現(xiàn)的靜態(tài)代碼塊之后的類(lèi)變量,只可以賦值,但是不能訪問(wèn)。
- <clinit>() 類(lèi)構(gòu)造器和<init>()實(shí)例構(gòu)造器不同,類(lèi)構(gòu)造器不需要顯示的父類(lèi)的類(lèi)構(gòu)造,在子類(lèi)的類(lèi)構(gòu)造器調(diào)用之前,會(huì)自動(dòng)的調(diào)用父類(lèi)的類(lèi)構(gòu)造器。因此虛擬機(jī)中第一個(gè)被調(diào)用的 <clinit>() 方法是 java.lang.Object 的類(lèi)構(gòu)造器
- 由于父類(lèi)的類(lèi)構(gòu)造器優(yōu)先于子類(lèi)的類(lèi)構(gòu)造器執(zhí)行,所以父類(lèi)中的 static{} 代碼塊也優(yōu)先于子類(lèi)的 static{} 執(zhí)行
- 類(lèi)構(gòu)造器<clinit>() 對(duì)于類(lèi)來(lái)說(shuō)并不是必需的,如果一個(gè)類(lèi)中沒(méi)有類(lèi)變量,也沒(méi)有 static{},那這個(gè)類(lèi)不會(huì)有類(lèi)構(gòu)造器 <clinit>()
- 接口中不能有 static{},但是接口中也可以有類(lèi)變量,所以接口中也可以有類(lèi)構(gòu)造器 <clinit>{},但是接口的類(lèi)構(gòu)造器和類(lèi)的類(lèi)構(gòu)造器有所不同,接口在調(diào)用類(lèi)構(gòu)造器的時(shí)候,如果不需要,不用調(diào)用父接口的類(lèi)構(gòu)造器,除非用到了父接口中的類(lèi)變量,接口的實(shí)現(xiàn)類(lèi)在初始化的時(shí)候也不會(huì)調(diào)用接口的類(lèi)構(gòu)造器
- 虛擬機(jī)會(huì)保證一個(gè)類(lèi)的 <clinit>() 方法在多線程環(huán)境中被正確地加鎖、同步,如果多個(gè)線程同時(shí)去初始化一個(gè)類(lèi),那么只有一個(gè)線程去執(zhí)行這個(gè)類(lèi)的類(lèi)構(gòu)造器 <clinit>(),其他線程會(huì)被阻塞,直到活動(dòng)線程執(zhí)行完類(lèi)構(gòu)造器 <clinit>() 方法
2.3 雙親委派模型
雙親委派模型,我們看一下 ClassLoader 的源碼就能明白了,我們公司的 Shadow 就是利用這個(gè)點(diǎn)來(lái)做插件類(lèi)加載的,來(lái)公司后我自主學(xué)習(xí)看的第一個(gè)源碼就是 Shadow ,順便打個(gè)廣告 Shadow 是一個(gè)騰訊自主研發(fā)的 Android 插件框架,經(jīng)過(guò)線上億級(jí)用戶量檢驗(yàn)。 Shadow 不僅開(kāi)源分享了插件技術(shù)的關(guān)鍵代碼,還完整的分享了上線部署所需要的所有設(shè)計(jì)。與市面上其他插件框架相比,Shadow 主要具有以下特點(diǎn):
- 復(fù)用獨(dú)立安裝App的源碼:插件App的源碼原本就是可以正常安裝運(yùn)行的。
- 零反射無(wú) Hack 實(shí)現(xiàn)插件技術(shù):從理論上就已經(jīng)確定無(wú)需對(duì)任何系統(tǒng)做兼容開(kāi)發(fā),更無(wú)任何隱藏 API 調(diào)用,和 Google 限制非公開(kāi) SDK 接口訪問(wèn)的策略完全不沖突。
- 全動(dòng)態(tài)插件框架:一次性實(shí)現(xiàn)完美的插件框架很難,但 Shadow 將這些實(shí)現(xiàn)全部動(dòng)態(tài)化起來(lái),使插件框架的代碼成為了插件的一部分。插件的迭代不再受宿主打包了舊版本插件框架所限制。
- 宿主增量極?。旱靡嬗谌珓?dòng)態(tài)實(shí)現(xiàn),真正合入宿主程序的代碼量極?。?5KB,160方法數(shù)左右)。
Kotlin 實(shí)現(xiàn):core.loader,core.transform 核心代碼完全用 Kotlin 實(shí)現(xiàn),代碼簡(jiǎn)潔易維護(hù)。
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
// 是否已經(jīng)被加載了
Class<?> clazz = findLoadedClass(className);
if (clazz == null) {
ClassNotFoundException suppressed = null;
try {
// 先從 parent 中加載
clazz = parent.loadClass(className, false);
} catch (ClassNotFoundException e) {
suppressed = e;
}
if (clazz == null) {
try {
// 最后再?gòu)?this 加載
clazz = findClass(className);
} catch (ClassNotFoundException e) {
e.addSuppressed(suppressed);
throw e;
}
}
}
return clazz;
}
3. jvm 虛擬機(jī)執(zhí)行引擎
了解了 .class 里面有啥,了解了 .class 怎么被解析加載,最后自然得了解下字節(jié)碼命令是怎么執(zhí)行的。在這之前我們先得了解兩個(gè)概念,什么是棧幀?什么是分派?
3.1 棧幀
棧幀(Stack Frame)是用于支持虛擬機(jī)進(jìn)行方法調(diào)用和方法執(zhí)行的數(shù)據(jù)結(jié)構(gòu),它是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)中的虛擬機(jī)棧(Virtual Machine Stack)的棧元素。棧幀存儲(chǔ)了方法的局部變量表、操作數(shù)棧、動(dòng)態(tài)連接和方法返回地址等信息。每一個(gè)方法從調(diào)用開(kāi)始至執(zhí)行完成的過(guò)程,都對(duì)應(yīng)著一個(gè)棧幀在虛擬機(jī)棧里面從入棧到出棧的過(guò)程。每一個(gè)棧幀都包括了局部變量表、操作數(shù)棧、動(dòng)態(tài)連接、方法返回地址和一些額外的附加信息。在編譯程序代碼的時(shí)候,棧幀中需要多大的局部變量表,多深的操作數(shù)棧都已經(jīng)完全確定了,并且寫(xiě)入到方法表的 Code 屬性之中,因此一個(gè)棧幀需要分配多少內(nèi)存,不會(huì)受到程序運(yùn)行期變量數(shù)據(jù)的影響,而僅僅取決于具體的虛擬機(jī)實(shí)現(xiàn)。一個(gè)線程中的方法調(diào)用鏈可能會(huì)很長(zhǎng),很多方法都同時(shí)處于執(zhí)行狀態(tài)。對(duì)于執(zhí)行引擎來(lái)說(shuō),在活動(dòng)線程中,只有位于棧頂?shù)臈攀怯行У?,稱為當(dāng)前棧幀(Current Stack Frame),與這個(gè)棧幀相關(guān)聯(lián)的方法稱為當(dāng)前方法(Current Method),執(zhí)行引擎運(yùn)行的所有字節(jié)碼指令都只針對(duì)當(dāng)前棧幀進(jìn)行操作。
3.2 分派
分派調(diào)用有可能是靜態(tài)的,也有可能是動(dòng)態(tài)的,我們?nèi)绻斫饬诉@個(gè),就會(huì)知道 Java 中的多態(tài)性是怎么實(shí)現(xiàn)的,像“重載”和“重寫(xiě)”等。Java 虛擬機(jī)識(shí)別方法的關(guān)鍵在于類(lèi)名、方法名以及方法描述符。前面兩個(gè)就不做過(guò)多的解釋了,至于方法描述符,它是由方法的參數(shù)類(lèi)型以及返回類(lèi)型所構(gòu)成。在同一個(gè)類(lèi)中,如果同時(shí)出現(xiàn)多個(gè)名字相同且描述符也相同的方法,那么 Java 虛擬機(jī)會(huì)在類(lèi)的驗(yàn)證階段報(bào)錯(cuò)。
可以看到,Java 虛擬機(jī)與 Java 語(yǔ)言不同,它并不限制名字與參數(shù)類(lèi)型相同,但返回類(lèi)型不同的方法出現(xiàn)在同一個(gè)類(lèi)中,對(duì)于調(diào)用這些方法的字節(jié)碼來(lái)說(shuō),由于字節(jié)碼所附帶的方法描述符包含了返回類(lèi)型,因此 Java 虛擬機(jī)能夠準(zhǔn)確地識(shí)別目標(biāo)方法。
靜態(tài)分派指的是在解析時(shí)便能夠直接識(shí)別目標(biāo)方法的情況,而動(dòng)態(tài)分派則指的是需要在運(yùn)行過(guò)程中根據(jù)調(diào)用者的動(dòng)態(tài)類(lèi)型來(lái)識(shí)別目標(biāo)方法的情況。Java 虛擬機(jī)中其實(shí)是不存在重載概念的,因?yàn)樵诰幾g期間我們就能確定需要執(zhí)行那個(gè)方法,如果非得區(qū)分那就是:重載被稱為靜態(tài)綁定或者編譯時(shí)多態(tài);而重寫(xiě)則被稱為動(dòng)態(tài)綁定。確切地說(shuō),Java 虛擬機(jī)中的靜態(tài)分派指的是在解析時(shí)便能夠直接識(shí)別目標(biāo)方法的情況,而動(dòng)態(tài)分派則指的是需要在運(yùn)行過(guò)程中根據(jù)調(diào)用者的動(dòng)態(tài)類(lèi)型來(lái)識(shí)別目標(biāo)方法的情況。Java 虛擬機(jī)執(zhí)行方法一般有五種指令:
- invokestatic:用于調(diào)用靜態(tài)方法。
- invokespecial:用于調(diào)用私有實(shí)例方法、構(gòu)造器,以及使用 super 關(guān)鍵字調(diào)用父類(lèi)的實(shí)例方法或構(gòu)造器,和所實(shí)現(xiàn)接口的默認(rèn)方法。
- invokevirtual:用于調(diào)用非私有實(shí)例方法。
- invokeinterface:用于調(diào)用接口方法。
- invokedynamic:用于調(diào)用動(dòng)態(tài)方法。
3.3 實(shí)例
有了這兩個(gè)概念后,我們就需要來(lái)看一個(gè)具體的實(shí)例了:
public class HelloWorld {
public static void main(String[] args){
int num1 = 100;
int num2 = 200;
int sum = sum(num1, num2);
System.out.println("sum = "+sum);
}
private static final int sum(int num1, int num2){
return num1 + num2;
}
}
javap -verbose HelloWorld.class:
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=4, args_size=1
0: bipush 100
2: istore_1
3: sipush 200
6: istore_2
7: iload_1
8: iload_2
9: invokestatic #2 // Method sum:(II)I
12: istore_3
13: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
16: new #4 // class java/lang/StringBuilder
19: dup
20: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V
23: ldc #6 // String sum =
25: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
28: iload_3
29: invokevirtual #8 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
32: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
35: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
38: return
LineNumberTable:
line 12: 0
line 13: 3
line 14: 7
line 15: 13
line 16: 38
LocalVariableTable:
Start Length Slot Name Signature
0 39 0 args [Ljava/lang/String;
3 36 1 num1 I
7 32 2 num2 I
13 26 3 sum I
這個(gè)理解是比較重要的,雖然我們?cè)诤竺嬷v asm 的時(shí)候會(huì)有傻瓜式操作,但是能不能理解怎么寫(xiě)為什么要那么寫(xiě),就靠我們對(duì)著每一條指令集的理解了。我們需要知道每個(gè)指令代表的是什么意思,比如 bipush 100 代表把數(shù)字 100 壓入棧中,istore_1 代表把剛壓入棧的 100 放到局部變量表中。我們需要清楚的知道每運(yùn)行一個(gè)指令,當(dāng)前棧和局部變量表中的數(shù)據(jù)是怎樣變化的。
本文基本都是文字原理,大家要有耐心,如果能夠理解其實(shí)是非常簡(jiǎn)單的東西。這本身是三四次課的內(nèi)容,我把其壓縮到了一兩次課來(lái)講??紤]到大家的水平不一,很多同學(xué)可能會(huì)感覺(jué)沒(méi)有講到位,因此大家可以去找些額外文章用來(lái)輔助理解,但是大的方向肯定是這個(gè)方向。