難頂,學(xué)不動了!
同學(xué)們好,今天二哥是來還債的,記得先拖到文末點個贊再回來細(xì)細(xì)的讀,好不好!
最近一段時間,我一直在學(xué)習(xí) Java 虛擬機和字節(jié)碼方面的知識,為的就是有朝一日成為真正牛逼的技術(shù)大佬!不知道大家有沒有這種感覺,就是一開始學(xué)習(xí)編程的時候,真心不想看底層的東西,就想直接上來擼代碼,但時間久了以后,總感覺缺點啥~~~~
于是我開始閱讀《深入理解計算機系統(tǒng)》、《圖解 TCP/IP》、《深入理解 Java 虛擬機》這些偏底層的書籍,看得煩了,就去刷我之前給大家推薦過的兩個視頻課,《哈佛大學(xué)的 CS50》和《計算機科學(xué)速成課》,慢慢的,就有一種頓悟的感覺,嗯,這種感覺還是挺舒服的,很容易飄的那種(嘿嘿)。
我之前已經(jīng)分享過三篇關(guān)于 Java 虛擬機和字節(jié)碼方面的內(nèi)容,大家可以再溫習(xí)一遍。
這三篇的內(nèi)容還是非常肝的,讀起來也比較輕松,但如果你是初學(xué)者,讀起來感覺很吃力的話,不要緊,我再來補一篇更全面、更細(xì)致、更通俗的,從另外一個視角切入,完事了可以把這四篇一起添加到收藏夾,以后興致比較高的時候可以再咀嚼下。
01、字節(jié)碼
計算機比較“傻”,只認(rèn) 0 和 1,這意味著我們編寫的代碼最終都要編譯成機器碼才能被計算機執(zhí)行。Java 在誕生之初就提出了一個非常著名的宣傳口號: "一次編寫,處處運行"。
Write Once, Run Anywhere.
為了這個口號,Java 的親媽 Sun 公司以及其他虛擬機提供商發(fā)布了許多可以在不同平臺上運行的 Java 虛擬機,而這些虛擬機都擁有一個共同的功能,那就是可以載入和執(zhí)行同一種與平臺無關(guān)的字節(jié)碼(Byte Code)。
有了 Java 虛擬機的幫助,我們編寫的 Java 源代碼不必再根據(jù)不同平臺編譯成對應(yīng)的機器碼了,只需要生成一份字節(jié)碼,然后再將字節(jié)碼文件交由運行在不同平臺上的 Java 虛擬機讀取后執(zhí)行就可以了。
如今的 Java 虛擬機非常強大,不僅支持 Java 語言,還支持很多其他的編程語言,比如說 Groovy、Scala、Koltin 等等。

來看一段代碼吧。
public class Main {
private int age = 18;
public int getAge() {
return age;
}
}
編譯生成 Main.class 文件后,可以在命令行使用 xxd Main.class 打開 class 文件(我用的是 Intellij IDEA,在 macOS 環(huán)境下)。

對于這些 16 進(jìn)制內(nèi)容,除了開頭的 cafe babe,剩下的內(nèi)容大致可以翻譯成:啥玩意啊這......
同學(xué)們別慌,就從"cafe babe"說起吧,這 4 個字節(jié)稱之為魔數(shù),也就是說,只有以"cafe babe"開頭的 class 文件才能被 Java 虛擬機接受,這 4 個字節(jié)就是字節(jié)碼文件的身份標(biāo)識。
目光右移,0000 是 Java 的次版本號,0037 轉(zhuǎn)化為十進(jìn)制是 55,是主版本號,Java 的版本號從 45 開始,每升一個大版本,版本號加 1,大家可以啟動福爾摩斯模式,推理一下。
再往后面就是字符串常量池?!?a style="font-weight: bold;color: rgb(53, 179, 120);border-bottom: 1px solid rgb(53, 179, 120);" data-linktype="2">class 文件》那一篇我是順著十六進(jìn)制內(nèi)容往下分析的,可能初學(xué)者看起來比較頭大,這次我們換一種更容易懂的方式。
02、反編譯字節(jié)碼文件
Java 內(nèi)置了一個反編譯命令 javap,可以通過 javap -help 了解 javap 的基本用法。

OK,我們輸入命令 javap -v -p Main.class 來查看一下輸出的內(nèi)容。
Classfile /Users/maweiqing/Documents/GitHub/TechSisterLearnJava/codes/TechSister/target/classes/com/itwanger/jvm/Main.class
Last modified 2021年4月15日; size 385 bytes
SHA-256 checksum 6688843e4f70ae8d83040dc7c8e2dd3694bf10ba7c518a6ea9b88b318a8967c6
Compiled from "Main.java"
public class com.itwanger.jvm.Main
minor version: 0
major version: 55
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #3 // com/itwanger/jvm/Main
super_class: #4 // java/lang/Object
interfaces: 0, fields: 1, methods: 2, attributes: 1
Constant pool:
#1 = Methodref #4.#18 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#19 // com/itwanger/jvm/Main.age:I
#3 = Class #20 // com/itwanger/jvm/Main
#4 = Class #21 // java/lang/Object
#5 = Utf8 age
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcom/itwanger/jvm/Main;
#14 = Utf8 getAge
#15 = Utf8 ()I
#16 = Utf8 SourceFile
#17 = Utf8 Main.java
#18 = NameAndType #7:#8 // "<init>":()V
#19 = NameAndType #5:#6 // age:I
#20 = Utf8 com/itwanger/jvm/Main
#21 = Utf8 java/lang/Object
{
private int age;
descriptor: I
flags: (0x0002) ACC_PRIVATE
public com.itwanger.jvm.Main();
descriptor: ()V
flags: (0x0001) 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 18
7: putfield #2 // Field age:I
10: return
LineNumberTable:
line 6: 0
line 7: 4
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 this Lcom/itwanger/jvm/Main;
public int getAge();
descriptor: ()I
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field age:I
4: ireturn
LineNumberTable:
line 9: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/itwanger/jvm/Main;
}
SourceFile: "Main.java"
睜大眼睛瞧過去,感覺內(nèi)容挺多的。同學(xué)們不要著急,我們來一行一行分析。
第 1 行:
Classfile /Users/maweiqing/Documents/GitHub/TechSisterLearnJava/codes/TechSister/target/classes/com/itwanger/jvm/Main.class
字節(jié)碼文件的位置。
第 2 行:
Last modified 2021年4月15日; size 385 bytes
字節(jié)碼文件的修改日期、文件大小。
第 3 行:
SHA-256 checksum 6688843e4f70ae8d83040dc7c8e2dd3694bf10ba7c518a6ea9b88b318a8967c
字節(jié)碼文件的 SHA-256 值。
第 4 行:
Compiled from "Main.java"
說明該字節(jié)碼文件編譯自 Main.java 源文件。
第 5 行:
public class com.itwanger.jvm.Main
字節(jié)碼文件的類全名。
第 6 行 minor version: 0,次版本號。
第 7 行 major version: 55,主版本號。
第 8 行:
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
類訪問標(biāo)記,一共有 8 種。

表明當(dāng)前類是 ACC_PUBLIC | ACC_SUPER。位運算符 | 的意思是如果相對應(yīng)位是 0,則結(jié)果為 0,否則為 1,所以 0x0001 | 0x0020 的結(jié)果是 0x0021(需要轉(zhuǎn)成二進(jìn)制進(jìn)行運算)。
第 9 行:
this_class: #3 // com/itwanger/jvm/Main
當(dāng)前類的索引,指向常量池中下標(biāo)為 3 的常量,可以看得出當(dāng)前類是 Main 類。
第 10 行:
super_class: #4 // java/lang/Object
父類的索引,指向常量池中下標(biāo)為 6 的常量,可以看得出當(dāng)前類的父類是 Object 類。
第 11 行:
interfaces: 0, fields: 1, methods: 2, attributes: 1
當(dāng)前類有 0 個接口,1 個字段(age),2 個方法(write()方法和缺省的默認(rèn)構(gòu)造方法),1 個屬性(該類僅有的一個屬性是 SourceFIle,包含了源碼文件的信息)。
03、常量池
接下來是 Constant pool,也就是字節(jié)碼文件最重要的常量池部分??梢园殉A砍乩斫鉃樽止?jié)碼文件中的資源倉庫,主要存放兩大類信息。
1)字面量(Literal),有點類似 Java 中的常量概念,比如文本字符串,final 常量等。
2)符號引用(Symbolic References),屬于編譯原理方面的概念,包括 3 種:
類和接口的全限定名(Fully Qualified Name) 字段的名稱和描述符(Descriptor) 方法的名稱和描述符
Java 虛擬機是在加載字節(jié)碼文件的時候才進(jìn)行的動態(tài)鏈接,也就是說,字段和方法的符號引用只有經(jīng)過運行期轉(zhuǎn)換后才能獲得真正的內(nèi)存地址。當(dāng) Java 虛擬機運行時,需要從常量池獲取對應(yīng)的符號引用,然后在類創(chuàng)建或者運行時解析并翻譯到具體的內(nèi)存地址上。
當(dāng)前字節(jié)碼文件中一共有 21 個常量,它們之間是有鏈接的,逐個分析會比較亂,我們采用順藤摸瓜的方式,從上依次往下看,那些被鏈接的常量我們就點到為止。
注:
#號后面跟的是索引,索引沒有從 0 開始而是從 1 開始,是因為設(shè)計者考慮到,“如果要表達(dá)不引用任何一個常量的含義時,可以將索引值設(shè)為 0 來表示”(《深入理解 Java 虛擬機》描述的)。=號后面跟的是常量的類型,沒有包含前綴CONSTANT_和后綴_info。全文中提到的索引等同于下標(biāo),為了靈活描述,沒有做統(tǒng)一。
第 1 個常量:
#1 = Methodref #4.#18 // java/lang/Object."<init>":()V
類型為 Methodref,表明是用來定義方法的,指向常量池中下標(biāo)為 4 和 18 的常量。
第 4 個常量:
#4 = Class #21 // java/lang/Object
類型為 Class,表明是用來定義類(或者接口)的,指向常量池中下標(biāo)為 21 的常量。
第 21 個常量:
#21 = Utf8 java/lang/Object
類型為 Utf8,UTF-8 編碼的字符串,值為 java/lang/Object。
第 18 個常量:
#18 = NameAndType #7:#8 // "<init>":()V
類型為 NameAndType,表明是字段或者方法的部分符號引用,指向常量池中下標(biāo)為 7 和 8 的常量。
第 7 個常量:
#7 = Utf8 <init>
類型為 Utf8,UTF-8 編碼的字符串,值為 <init>,表明為構(gòu)造方法。
第 8 個常量:
#8 = Utf8 ()V
類型為 Utf8,UTF-8 編碼的字符串,值為 ()V,表明方法的返回值為 void。
到此為止,第 1 個常量算是摸完了。組合起來的意思就是,Main 類使用的是默認(rèn)的構(gòu)造方法,來源于 Object 類。
第 2 個常量:
#2 = Fieldref #3.#19 // com/itwanger/jvm/Main.age:I
類型為 Fieldref,表明是用來定義字段的,指向常量池中下標(biāo)為 3 和 19 的常量。
第 3 個常量:
#3 = Class #20 // com/itwanger/jvm/Main
類型為 Class,表明是用來定義類(或者接口)的,指向常量池中下標(biāo)為 20 的常量。
第 19 個常量:
#19 = NameAndType #5:#6 // age:I
類型為 NameAndType,表明是字段或者方法的部分符號引用,指向常量池中下標(biāo)為 5 和 6 的常量。
第 5 個常量:
#5 = Utf8 age
類型為 Utf8,UTF-8 編碼的字符串,值為 age,表明字段名為 age。
第 6 個常量:
#6 = Utf8 I
類型為 Utf8,UTF-8 編碼的字符串,值為 I,表明字段的類型為 int。
關(guān)于字段類型的描述符映射表如下圖所示。

到此為止,第 2 個常量算是摸完了。組合起來的意思就是,聲明了一個類型為 int 的字段 age。
04、字段表集合
字段表用來描述接口或者類中聲明的變量,包括類變量和成員變量,但不包含聲明在方法中局部變量。
字段的修飾符一般有:
訪問權(quán)限修飾符,比如 public private protected 靜態(tài)變量修飾符,比如 static final 修飾符 并發(fā)可見性修飾符,比如 volatile 序列化修飾符,比如 transient
然后是字段的類型(可以是基本數(shù)據(jù)類型、數(shù)組和對象)和名稱。
在 Main.class 字節(jié)碼文件中,字段表的信息如下所示。
private int age;
descriptor: I
flags: (0x0002) ACC_PRIVATE
表明字段的訪問權(quán)限修飾符為 private,類型為 int,名稱為 age。
字段的訪問標(biāo)志和類的訪問標(biāo)志非常類似。

05、方法表集合
方法表用來描述接口或者類中聲明的方法,包括類方法和成員方法,以及構(gòu)造方法。方法的修飾符和字段略有不同,比如說 volatile 和 transient 不能用來修飾方法,再比如說方法的修飾符多了 synchronized、native、strictfp 和 abstract。

下面這部分為構(gòu)造方法,返回類型為 void,訪問標(biāo)志為 public。
public com.itwanger.jvm.Main();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
來詳細(xì)看一下其中 Code 屬性。
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 18
7: putfield #2 // Field age:I
10: return
LineNumberTable:
line 6: 0
line 7: 4
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 this Lcom/itwanger/jvm/Main;
stack 為最大操作數(shù)棧,Java 虛擬機在運行的時候會根據(jù)這個值來分配棧幀的操作數(shù)棧深度。
locals 為局部變量所需要的存儲空間,單位為槽(slot),方法的參數(shù)變量和方法內(nèi)的局部變量都會存儲在局部變量表中。
args_size 為方法的參數(shù)個數(shù)。
為什么 stack 的值為 2,locals 的值為 1,args_size 的值為 1 呢?默認(rèn)的構(gòu)造方法不是沒有參數(shù)和局部變量嗎?
這是因為有一個隱藏的 this 變量,只要不是靜態(tài)方法,都會有一個當(dāng)前類的對象 this 悄悄的存在著。這就解釋了為什么 locals 和 args_size 的值為 1 的問題。那為什么 stack 的值為 2 呢?因為字節(jié)碼指令 invokespecial(調(diào)用父類的構(gòu)造方法進(jìn)行初始化)會消耗掉一個當(dāng)前類的引用,所以 aload_0 執(zhí)行了 2 次,也就意味著操作數(shù)棧的大小為 2。
關(guān)于字節(jié)碼指令,我們后面再詳細(xì)介紹。
LineNumberTable,該屬性的作用是描述源碼行號與字節(jié)碼行號(字節(jié)碼偏移量)之間的對應(yīng)關(guān)系。
LocalVariableTable,該屬性的作用是描述幀棧中的局部變量與源碼中定義的變量之間的關(guān)系。大家仔細(xì)看一下,就能看到 this 的影子了。
下面這部分為成員方法 getAge(),返回類型為 int,訪問標(biāo)志為 public。
public int getAge();
descriptor: ()I
flags: (0x0001) ACC_PUBLIC
理解了構(gòu)造方法的 Code 屬性后,再看 getAge() 方法的 Code 屬性時,就很容易理解了。
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field age:I
4: ireturn
LineNumberTable:
line 9: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/itwanger/jvm/Main;
最大操作數(shù)棧為 1,局部變量所需要的存儲空間為 1,方法的參數(shù)個數(shù)為 1,是因為局部變量只有一個隱藏的 this,并且字節(jié)碼指令中只執(zhí)行了一次 aload_0。
參考鏈接:https://juejin.im/post/6844903588716609543
本來想著這一篇就徹底把 Java 字節(jié)碼給結(jié)束掉,沒想到還得再學(xué)習(xí)一下字節(jié)碼指令,難頂!
其實學(xué)習(xí)就是這樣,可以橫向擴展,也可以縱向擴展。當(dāng)我們初學(xué)編程的時候,特別想多學(xué)一點,屬于橫向擴展,當(dāng)有了一定的編程經(jīng)驗后,想更上一層樓,就需要縱向擴展,不斷深入地學(xué),連根拔起,從而形成自己的知識體系。
無論是從十六進(jìn)制的字節(jié)碼角度,還是 jclasslib 圖形化查看反編譯后的字節(jié)碼的角度,也或者是今天這樣從 javap 反編譯后的角度,都能窺探出一些新的內(nèi)容來!
初學(xué)者一開始接觸字節(jié)碼的時候會感覺比較頭大,沒關(guān)系,我當(dāng)初也是這樣,隨著時間的推移,經(jīng)驗的積累,慢慢就好了,越往深處鉆,就越能體會到那種“技術(shù)我有,雄霸天下”的感覺~
我是一直在悄悄打怪的二哥,希望能和同學(xué)們一起,變得更強,更禿(不不不,更帥),既然看到這了,就賞個在看或者星標(biāo)吧,點贊也不是不可以!
下期見~
