命令行
//編譯成 class 文件
javac Test.java
//反匯編 class 文件
javap -V Test.class
Android Studio 編譯的 class
文件位于 build/intermediates/clases/debug/包名 下
IDEA 插件
jclasslib Bytecode viewer
ASM Bytecode Viewer
這兩款插件都可以在 Android Studio Plugins 里直接下載安裝
字節(jié)碼的組成
方法調(diào)用在JVM中轉(zhuǎn)換成的是字節(jié)碼執(zhí)行,字節(jié)碼指令執(zhí)行的數(shù)據(jù)結(jié)構(gòu)就是棧幀。
棧幀的數(shù)據(jù)結(jié)構(gòu)主要分為四個(gè)部分:局部變量表、操作數(shù)棧、動(dòng)態(tài)鏈接以及方法返回地址(包括正常調(diào)用和異常調(diào)用的完成結(jié)果)。
局部變量表(local variables)
當(dāng)方法被調(diào)用時(shí),參數(shù)會(huì)傳遞到從0開(kāi)始的連續(xù)的局部變量表的索引位置上。local variables的最大長(zhǎng)度是在編譯期間決定的。一個(gè)局部變量表的占用了32位的存儲(chǔ)空間(一個(gè)存儲(chǔ)單位稱之為slot,槽),所以可以存儲(chǔ)一個(gè)boolean、byte、char、short、float、int、refrence和returnAdress數(shù)據(jù),long和double需要2個(gè)連續(xù)的局部變量表來(lái)保存,通過(guò)較小位置的索引來(lái)獲取。如果被調(diào)用的是實(shí)例方法,那么第0個(gè)位置存儲(chǔ)“this”關(guān)鍵字代表當(dāng)前實(shí)例對(duì)象的引用。
操作數(shù)棧
操作數(shù)棧同局部變量表一樣,也是編譯期間就能決定了其存儲(chǔ)空間(最大的單位長(zhǎng)度)。
操作數(shù)棧是在JVM字節(jié)碼執(zhí)行一些指令(第二部分會(huì)介紹一些指令集)時(shí)創(chuàng)建的,主要是把局部變量表中的變量壓入操作數(shù)棧,在操作數(shù)棧中進(jìn)行字節(jié)碼指令的操作,再將變量出操作數(shù)棧,結(jié)果入操作數(shù)棧。
動(dòng)態(tài)鏈接
每個(gè)棧幀指向運(yùn)行時(shí)常量池中該棧幀所屬的方法的引用,也就是字節(jié)碼的發(fā)放調(diào)用的引用。動(dòng)態(tài)鏈接就是將符號(hào)引用所表示的方法,轉(zhuǎn)換成方法的直接引用。加載階段或第一次使用時(shí)轉(zhuǎn)化為直接引用的(將變量的訪問(wèn)轉(zhuǎn)化為訪問(wèn)這些變量的存儲(chǔ)結(jié)構(gòu)所在的運(yùn)行時(shí)內(nèi)存位置)就叫做靜態(tài)解析。JVM的動(dòng)態(tài)鏈接還支持運(yùn)行期轉(zhuǎn)化為直接引用。也可以叫做Late Binding,晚期綁定。
方法返回地址
方法正常退出會(huì)把返回值壓入調(diào)用者的棧幀的操作數(shù)棧,PC計(jì)數(shù)器的值就會(huì)調(diào)整到方法調(diào)用指令后面的一條指令。這樣使得當(dāng)前的棧幀能夠和調(diào)用者連接起來(lái),并且讓調(diào)用者的棧幀的操作數(shù)棧繼續(xù)往下執(zhí)行。
方法的異常調(diào)用完成,主要是JVM拋出的異常,如果異常沒(méi)有被捕獲住,或者遇到athrow字節(jié)碼指令顯示拋出,那么就沒(méi)有返回值給調(diào)用者。
Java 代碼:
public class MyTest {
private int myNum = 20;
public void func() {
myNum = 50;
}
}
編譯后的 class 源文件:
cafe babe 0000 0033 0015 0a00 0400 1109
0003 0012 0700 1307 0014 0100 056d 794e
756d 0100 0149 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 1a4c 636f
6d2f 7961 7a68 6964 6576 2f64 656d 6f2f
4d79 5465 7374 3b01 0004 6675 6e63 0100
0a53 6f75 7263 6546 696c 6501 000b 4d79
5465 7374 2e6a 6176 610c 0007 0008 0c00
0500 0601 0018 636f 6d2f 7961 7a68 6964
6576 2f64 656d 6f2f 4d79 5465 7374 0100
106a 6176 612f 6c61 6e67 2f4f 626a 6563
7400 2100 0300 0400 0000 0100 0200 0500
0600 0000 0200 0100 0700 0800 0100 0900
0000 3900 0200 0100 0000 0b2a b700 012a
1014 b500 02b1 0000 0002 000a 0000 000a
0002 0000 0007 0004 0008 000b 0000 000c
0001 0000 000b 000c 000d 0000 0001 000e
0008 0001 0009 0000 0035 0002 0001 0000
0007 2a10 32b5 0002 b100 0000 0200 0a00
0000 0a00 0200 0000 0b00 0600 0c00 0b00
0000 0c00 0100 0000 0700 0c00 0d00 0000
0100 0f00 0000 0200 10
javap 反匯編后的代碼:
Classfile /Users/zengyazhi/Documents/zyzdev/AndroidDemo/app/build/intermediates/classes/debug/com/yazhidev/demo/MyTest.class
Last modified 2018-12-28; size 393 bytes
MD5 checksum 2872209fbe3efb46c70b23bf85be75fd
Compiled from "MyTest.java"
public class com.yazhidev.demo.MyTest
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#17 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#18 // com/yazhidev/demo/MyTest.myNum:I
#3 = Class #19 // com/yazhidev/demo/MyTest
#4 = Class #20 // java/lang/Object
#5 = Utf8 myNum
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcom/yazhidev/demo/MyTest;
#14 = Utf8 func
#15 = Utf8 SourceFile
#16 = Utf8 MyTest.java
#17 = NameAndType #7:#8 // "<init>":()V
#18 = NameAndType #5:#6 // myNum:I
#19 = Utf8 com/yazhidev/demo/MyTest
#20 = Utf8 java/lang/Object
{
public com.yazhidev.demo.MyTest();
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: bipush 20
7: putfield #2 // Field myNum:I
10: return
LineNumberTable:
line 7: 0
line 8: 4
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 this Lcom/yazhidev/demo/MyTest;
public void func();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: bipush 50
3: putfield #2 // Field myNum:I
6: return
LineNumberTable:
line 11: 0
line 12: 6
LocalVariableTable:
Start Length Slot Name Signature
0 7 0 this Lcom/yazhidev/demo/MyTest;
}
SourceFile: "MyTest.java"
操作碼
opcode(指令) = 操作碼 + 操作數(shù)
例如 bipush 10 這是一條指令,是由操作碼 bipush 后跟一個(gè)操作數(shù) 10 組成,該指令的作用是將整型數(shù) 10 壓到操作數(shù)棧中。
-
aload_0(指令碼:0x2a)
從局部變量數(shù)組中加載一個(gè)對(duì)象引用到操作數(shù)棧的棧頂,最后的數(shù)字對(duì)應(yīng)的是局部變量數(shù)組中的位置,只能是0,1,2,3。(第一個(gè)局部變量是this引用)
-
invokespecial(0xb7)
只能調(diào)用三類方法:<init>方法;私有方法;super.method()。因?yàn)檫@三類方法的調(diào)用對(duì)象在編譯時(shí)就可以確定
-
invokevirtual(0xb6)
是一種動(dòng)態(tài)分派的調(diào)用指令
-
bipush(0x10)
用來(lái)把一個(gè)字節(jié)作為整型壓到操作數(shù)棧中
-
putfield(0xb5)
后面跟一個(gè)操作數(shù)(該操作數(shù)引用的是運(yùn)行時(shí)常量池里的一個(gè)字段,在這里這個(gè)字段是 myNum),將棧頂?shù)闹蒂x給這個(gè)。賦給這個(gè)字段的值,以及包含這個(gè)字段的對(duì)象引用,在執(zhí)行這條指令的時(shí)候,都會(huì)從操作數(shù)棧頂上 pop 出來(lái)
-
ldc(0x12)
常量池中的常量值入棧
-
CHECKCAST(0xc0)
類型強(qiáng)轉(zhuǎn)
部分字節(jié)碼指令集可見(jiàn):
解析
回到上面 MyTest 的構(gòu)造函數(shù)里:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: bipush 20
7: putfield #2 // Field myNum:I
10: return
ASM Bytecode viewer 顯示的字節(jié)碼為:
// class version 51.0 (51)
// access flags 0x21
public class com/yazhidev/demo/MyTest {
// compiled from: MyTest.java
// access flags 0x2
private I myNum
// access flags 0x1
public <init>()V
L0
LINENUMBER 7 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
L1
LINENUMBER 8 L1
ALOAD 0
BIPUSH 20
PUTFIELD com/yazhidev/demo/MyTest.myNum : I
RETURN
L2
LOCALVARIABLE this Lcom/yazhidev/demo/MyTest; L0 L2 0
MAXSTACK = 2
MAXLOCALS = 1
// access flags 0x1
public func()V
L0
LINENUMBER 11 L0
ALOAD 0
BIPUSH 50
PUTFIELD com/yazhidev/demo/MyTest.myNum : I
L1
LINENUMBER 12 L1
RETURN
L2
LOCALVARIABLE this Lcom/yazhidev/demo/MyTest; L0 L2 0
MAXSTACK = 2
MAXLOCALS = 1
}
從字節(jié)碼看泛型
Java 的泛型是完全在編譯器中實(shí)現(xiàn)的,由編譯器執(zhí)行類型檢查和類型推斷,然后生成普通的非泛型的字節(jié)碼,虛擬機(jī)完全不感知泛型的存在。編譯器使用泛型類型信息保證類型安全,然后在生成字節(jié)碼之前將其清除。
Java 代碼:
public class Generic<T> {
private T data;
public T get() {
return data;
}
public void set(T data) {
this.data = data;
}
}
從生成的字節(jié)碼中可以看到,泛型 T 已經(jīng)被擦除了:
private Ljava/lang/Object; data
public getData()Ljava/lang/Object;
public setData(Ljava/lang/Object;)V
類型擦除與多態(tài)沖突的問(wèn)題
子類 B,指定了泛型類型:
public class B extends Generic<Number> {
private Number n;
public Number get() {
return n;
}
public void set(Number n) {
this.n = n;
}
}
子類 C,未指定泛型類型:
public class C extends Generic {
private Number n;
public Number get() {
return n;
}
public void set(Number n) {
this.n = n;
}
}
我們?cè)趯?B 類時(shí),指定了泛型類型為 Number,對(duì)于 B 類的方法 get()Number 和 set(Number),我們的本意應(yīng)該是對(duì)父類的 get()T 和 set(T) 方法進(jìn)行重寫。但上面我們知道了,父類的 get()T 和 set(T) 在字節(jié)碼中實(shí)際上是 get()Object 和 set(Object),與類 B 的方法 set(Number) 方法參數(shù)不一樣,理論上應(yīng)該算重載而不是重寫。為了解決這一沖突,JVM 采用了一種特殊的方法:橋接。
我們先看 B 類的字節(jié)碼:
public get()Ljava/lang/Number;
public set(Ljava/lang/Number;)V
// access flags 0x1041
public synthetic bridge set(Ljava/lang/Object;)V
L0
LINENUMBER 7 L0
ALOAD 0
ALOAD 1
CHECKCAST java/lang/Number
INVOKEVIRTUAL com/yazhidev/demo/B.set (Ljava/lang/Number;)V
RETURN
L1
LOCALVARIABLE this Lcom/yazhidev/demo/B; L0 L1 0
MAXSTACK = 2
MAXLOCALS = 2
// access flags 0x1041
public synthetic bridge get()Ljava/lang/Object;
L0
LINENUMBER 7 L0
ALOAD 0
INVOKEVIRTUAL com/yazhidev/demo/B.get ()Ljava/lang/Number;
ARETURN
L1
LOCALVARIABLE this Lcom/yazhidev/demo/B; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
可以發(fā)現(xiàn)編譯器自動(dòng)生成了 set(Object) 和 get()Object 兩個(gè)橋接方法來(lái)重寫父類方法,同時(shí)這兩個(gè)橋接方法實(shí)際上調(diào)用了對(duì)應(yīng)的 set(Number) 方法和 get()Number 方法。虛擬機(jī)通過(guò)使用橋接方法,來(lái)解決了類型擦除和多態(tài)的沖突。對(duì)于開(kāi)發(fā)者來(lái)說(shuō),對(duì)于指定了泛型類型為 Number 的 B 類來(lái)說(shuō),其 set(Number) 方法就是對(duì)父類方法 set(T) 的重寫,同理 get()Number 也是對(duì)父類方法 get()T 的重寫。
但 C 類則有些不同,C 類未指定泛型類型,所以父類中的方法為 get()Object 和 set(Object),C 類中的 set(Number) 與父類 set(Object) 方法參數(shù)不同,理所當(dāng)然是重載,我們都知道,只有返回值不同不滿足重載條件,所以對(duì) C 類的 get()Number 方法來(lái)說(shuō),應(yīng)該算是對(duì)父類方法 get()T 的重寫。
我們來(lái)看 C 類的字節(jié)碼:
public get()Ljava/lang/String;
public set(Ljava/lang/String;)V
// access flags 0x1041
public synthetic bridge get()Ljava/lang/Object;
L0
LINENUMBER 7 L0
ALOAD 0
INVOKEVIRTUAL com/yazhidev/demo/C.get ()Ljava/lang/String;
ARETURN
L1
LOCALVARIABLE this Lcom/yazhidev/demo/C; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
可以發(fā)現(xiàn)編譯期自動(dòng)生成了 get()Object 橋接方法來(lái)重寫父類方法。但我們發(fā)現(xiàn)字節(jié)碼里卻同時(shí)存在了兩個(gè)只有返回值類型不同的同名方法,這是為什么呢?
這里就需要提到方法特征簽名,只有特征簽名不同的方法才可以共存。
-
Java 層方法簽名 = 方法名 + 參數(shù)類型 + 參數(shù)順序
所以在 Java 語(yǔ)言里,重載一個(gè)方法需要兩個(gè)同名方法的參數(shù)類型不同,或者參數(shù)順序不同,只有返回值類型不同是無(wú)法通過(guò)編譯的。
-
JVM 層方法簽名 = 方法名 + 參數(shù)類型 + 參數(shù)順序 + 返回值類型 + 可能拋出的異常
所以在 class 文件里,是可以存在兩個(gè)只有返回值類型不同的同名方法。也就是上面的
get()Object和get()Number