成為一名優(yōu)秀的Android開發(fā)竭讳,需要一份完備的知識體系音同,在這里胸遇,讓我們一起成長為自己所想的那樣~。
一、Class 文件結(jié)構(gòu)初識
“與平臺無關(guān)” 的理想最終實現(xiàn)在操作系統(tǒng)的應(yīng)用層面上:眾多虛擬機廠商發(fā)布了許多可以運行在各種不同平臺上的虛擬機供汛,而這些虛擬機都可以載入和執(zhí)行同一種與平臺無關(guān)的字節(jié)碼,從而實現(xiàn)了程序的 “一次編寫,到處運行”怔昨。
而 字節(jié)碼(ByteCode)正是構(gòu)成其平臺無關(guān)性的基石雀久。Java 虛擬機不和包括 Java 在內(nèi)的任何語言綁定,它 只與 “Class文件” 這種特定的二進制文件格式所關(guān)聯(lián)趁舀,Class 文件中包含 了 Java 虛擬機指令集和符號表以及若干其他輔助信息赖捌。
虛擬機并不關(guān)心 Class 的來源是何種語言,有了字節(jié)碼矮烹,也解除了 Java 虛擬機和 Java 語言之間的耦合越庇。
Java 語言中的各種變量、關(guān)鍵字和運算符號的語義最終都是由多條字節(jié)碼命令組合而成的擂送,因此悦荒,字節(jié)碼命令所能提供的語義描述能力肯定會比 Java 語言本身更加強大。所以嘹吨,有一些 Java 語言本身無法有效支持的語言特性不代表字節(jié)碼本身就無法有效地支持搬味,這也為其他語言實現(xiàn)一些有別于 Java 的語言特性提供了基礎(chǔ)。
字節(jié)碼文件是由 十六進制值組成 的蟀拷,對于 JVM 來說碰纬,在讀取數(shù)據(jù)的時候,它會 以兩個十六進制值為一組问芬,即 一個字節(jié) 進行讀取悦析。在 Java 中,我們通常會采用 javac 命令將源代碼編譯成字節(jié)碼文件此衅,下面這幅 Java 官方圖展示了一個 .java 文件從編譯到運行的過程强戴,如下所示:
Class 文件是一組以 8 位字節(jié)為基礎(chǔ)單位的二進制流,各個數(shù)據(jù)項目嚴格按照順序緊湊地排列在 Class 文件之中挡鞍,中間沒有添加任何分隔符骑歹,這使得整個 Class 文件中存儲的內(nèi)容幾乎 全部是程序運行的必要數(shù)據(jù),沒有空隙存在墨微。
當遇到需要占用 8 位字節(jié)以上空間的數(shù)據(jù)項 時道媚,則會按照高位在前的方式分割成若干個 8 位字節(jié)進行存儲。(高位在前指 ”Big-Endian"翘县,即指最高位字節(jié)在地址最低位最域,最低位字節(jié)在地址最高位的順序來存儲數(shù)據(jù),而 X86 等處理器則是使用了相反的 “Little-Endian” 順序來存儲數(shù)據(jù))
根據(jù) JVM 規(guī)范的規(guī)定锈麸,Class 文件格式采用了一種類似于 C 語言結(jié)構(gòu)體的偽結(jié)構(gòu)來存 儲數(shù)據(jù)镀脂,而這種偽結(jié)構(gòu)中有且只有兩種數(shù)據(jù)類型:無符號數(shù)和表。
1忘伞、無符號數(shù)
無符號數(shù)屬于基本的數(shù)據(jù)類型狗热,以 u1钞馁、u2虑省、u4匿刮、u8 來分別代表 1 個字節(jié)、2 個字節(jié)探颈、4 個字節(jié)和 8 個字節(jié)的無符號數(shù)熟丸,無符號數(shù)可以用來 描述數(shù)字、索引引用伪节、數(shù)量值或者按照UTF-8 碼構(gòu)成字符串值光羞。
2、表
表是 由多個無符號數(shù)或者其他表作為數(shù)據(jù)項構(gòu)成的復(fù)合數(shù)據(jù)類型怀大,所有表都習(xí)慣性地以 “_info”
結(jié)尾纱兑。表用于 描述有層次關(guān)系的復(fù)合結(jié)構(gòu)的數(shù)據(jù),而整個 Class 文件其本質(zhì)上就是一張表化借。
對比 Linux潜慎、Windows 上的可執(zhí)行文件(例如 ELF)而言,Class 文件可以看做是 JVM 的可執(zhí)行文件蓖康。其 表格式 如下所示:
u4:表示能夠保存4個字節(jié)的無符號整數(shù)铐炫,u2同理。
ClassFile {
u4 magic; // 魔法數(shù)字蒜焊,表明當前文件是.class文件倒信,固定0xCAFEBABE
u2 minor_version; // 分別為Class文件的副版本和主版本
u2 major_version;
u2 constant_pool_count; // 常量池計數(shù)
cp_info constant_pool[constant_pool_count-1]; // 常量池內(nèi)容
u2 access_flags; // 類訪問標識
u2 this_class; // 當前類
u2 super_class; // 父類
u2 interfaces_count; // 實現(xiàn)的接口數(shù)
u2 interfaces[interfaces_count]; // 實現(xiàn)接口信息
u2 fields_count; // 字段數(shù)量
field_info fields[fields_count]; // 包含的字段信息
u2 methods_count; // 方法數(shù)量
method_info methods[methods_count]; // 包含的方法信息
u2 attributes_count; // 屬性數(shù)量
attribute_info attributes[attributes_count]; // 各種屬性
}
復(fù)制代碼
對于 Class 表結(jié)構(gòu)而言,其 前 8 個字節(jié) 依次是如下 三個元素:
- 1)泳梆、
magic
:每個 Class 文件的頭 4 個字節(jié)被稱為魔數(shù)(Magic Number)鳖悠,它的唯一作用是確定這個文件是否為一個能被虛擬機所接受的 Class 文件。很多文件存儲標準中都使用魔數(shù)來進行身份識別优妙, 譬如圖片格式乘综,如 gif 或者 jpeg 等在文件頭中都存有魔數(shù)。使用魔數(shù)而不是擴展名來進行識別主要是基于安全方面的考慮鳞溉,因為文件擴展名可以隨意地改動瘾带。并且,Class 文件的魔數(shù)獲得很有 “浪漫氣息”熟菲,值為:0xCAFEBABE(咖啡寶貝)看政。 - 2)、
minor_version
:2 個字節(jié)長抄罕,表示當前 Class 文件的次版號允蚣。 - 3)、
major_version
:2 個字節(jié)長呆贿,表示當前 Class 文件的主版本號嚷兔。(Java 的版本號是從 45 開始 的森渐,JDK 1.1 之后的每個 JDK 大版本發(fā)布會在主版本號向上加 1(JDK 1.01.1 使用了 45.045.3 的版本號),例如 JDK 1.8 就是 52.0)冒晰。需要注意的是同衣,虛擬機會拒絕執(zhí)行超過其版本號的 Class 文件。
然后壶运,我們再來簡單地了解下 其它元素 的含義:
- 4)耐齐、
constant_pool_count
:常量池數(shù)組元素個數(shù)。 - 5)蒋情、
constant_pool
:常量池埠况,是一個存儲了 cp_info 信息的數(shù)組,每一個 Class 文件都有一個與之對應(yīng)的常量池棵癣。(注意:cp_info 數(shù)組的索引從 1 開始) - 6)辕翰、
access_flags
:表示當前類的訪問權(quán)限拟逮,例如:public艾恼、private硕蛹。 - 7)某弦、
this_class 和 super_class
:存儲了指向常量池數(shù)組元素的索引撕彤,this_class 中索引指向的內(nèi)容為當前類名灰嫉,而 super_class 中索引則指向其父類類名节仿。 - 8)巢块、
interfaces_count 和 interfaces
:同上丧裁,它們存儲的也只是指向常量池數(shù)組元素的索引护桦。其內(nèi)容分別表示當前類實現(xiàn)了多少個接口和對應(yīng)的接口類類名。 - 9)煎娇、
fields_count 和 fields
:表示成員變量的數(shù)量和其信息二庵,信息由 field_info 結(jié)構(gòu)體表示。 - 10)缓呛、
methods_count 和 methods
:表示成員函數(shù)的數(shù)量和它們的信息催享,信息由 method_info 結(jié)構(gòu)體表示。 - 11)哟绊、
attributes_count 和 attributes
:表示當前類的屬性信息因妙,每一個屬性都有一個與之對應(yīng)的 attribute_info 結(jié)構(gòu)。常見的屬性信息如調(diào)試信息票髓,它需要記錄某句代碼對應(yīng)源代碼的哪一行攀涵,此外,如函數(shù)對應(yīng)的 JVM 字節(jié)碼洽沟、注解信息也是屬性信息以故。
需要注意的是,Class 表的結(jié)構(gòu)不像 XML 等描述語言裆操,由于它沒有任何分隔符號怒详,所以在上面中的這些數(shù)據(jù)項炉媒,無論是順序還是數(shù)量,甚至于數(shù)據(jù)存儲的字節(jié)序(Byte Ordering昆烁,Class 文件中字節(jié)序為 Big-Endian)這樣的細節(jié)吊骤,都是被嚴格限定的。
對于上面的各個屬性來說善玫,有不少屬性是我們需要重點掌握的水援,而 常量池可以被認為是 Class 表結(jié)構(gòu)中的重中之重。下面??茅郎,我們就先來了解下常量池。
二或渤、常量池
常量池可以理解為 Class 文件之中的資源倉庫系冗,其它的幾種結(jié)構(gòu)或多或少都會最終指向到這個資源倉庫之中。
此外薪鹦,常量池是 Class 文件結(jié)構(gòu)中與其他項 關(guān)聯(lián)最多 的數(shù)據(jù)類型掌敬,也是 占用 Class 文件空間最大 的數(shù)據(jù)項之一,同時它還是 在 Class 文件中第一個出現(xiàn)的表類型數(shù)據(jù)項池磁。因此奔害,如果沒有充分了地解常量池,后面其它的 Class 表類型數(shù)據(jù)項的學(xué)習(xí)會變得舉步維艱地熄。
假設(shè)一個常量池的容量(偏移地址:0x00000008)為十六進制數(shù) 0x0016华临,即十進制的 22,這就代表常量池中有 21 項常量端考,索引值范圍為 1~21雅潭。在 Class 文件格式規(guī)范制定之時,設(shè)計者將第 0 項常量空出來是有特殊考慮的却特,這樣做的目的在 于滿足后面某些指向常量池的索引值的數(shù)據(jù)在特定情況下需要表達 “不引用任何一個常量池項”的含義扶供。
而常量池中主要存放兩大類常量:字面量(Literal)和符號引用(Symbolic References)
。
1裂明、字面量(Literal)
字面量比較接近于 Java 語言層面的常量概念椿浓,如文本字符串、聲明為 final 的常量值等闽晦。
2扳碍、符號引用(Symbolic References)(??)
而 符號引用 則屬于編譯原理方面的概念,包括了 三類常量尼荆,如下所示:
- 1)左腔、類和接口的全限定名(Fully Qualified Name)
- 2)、字段的名稱和描述符(Descriptor))
- 3)捅儒、方法的名稱和描述符
此外液样,在虛擬機加載 Class 文件的時候會進行動態(tài)鏈接振亮,因為其字段、方法的符號引用不經(jīng)過運行期轉(zhuǎn)換的話就無法得到真正的內(nèi)存入口地址鞭莽,也就無法直接被虛擬機使用坊秸。當虛擬機運行時,需要從常量池獲得對應(yīng)的符號引用澎怒,再在類創(chuàng)建或運行時進行解析褒搔,并翻譯到具體的內(nèi)存地址之中。
connstant_pool 中存儲了一個一個的 cp_info 信息喷面,并且每一個 cp_info 的第一個字節(jié)(即一個 u1 類型的標志位)標識了當前常量項的類型星瘾,其后才是具體的常量項內(nèi)容。
下面??惧辈,我們看看有哪些具體的 常量項的類型琳状,如下表所示:
類型 | 標志 | 描述 |
---|---|---|
CONSTANT_Utf8_info | 1 | 用于存儲UTF-8編碼的字符串,它真正包含了字符串的內(nèi)容盒齿。 |
CONSTANT_Integer_info | 3 | 表示int型數(shù)據(jù)的信息 |
CONSTANT_Float_info | 4 | 表示float型數(shù)據(jù)的信息 |
CONSTANT_Long_info | 5 | 表示long型數(shù)據(jù)的信息 |
CONSTANT_Double_info | 6 | 表示double型數(shù)據(jù)的信息 |
CONSTANT_Class_info | 7 | 表示類或接口的信息 |
CONSTANT_String_info | 8 | 表示字符串念逞,但該常量項本身不存儲字符串的內(nèi)容,它僅僅只存儲了一個索引值 |
CONSTANT_Fieldref_info | 9 | 字段的符號引用 |
CONSTANT_Methodref_info | 10 | 類中方法的符號引用 |
CONSTANT_InterfaceMethodref_info | 11 | 接口中方法的符號引用 |
CONSTANT_NameAndType_info | 12 | 描述類的成員域或成員方法相關(guān)的信息 |
CONSTANT_MethodHandle_info | 15 | 表示方法句柄信息边翁,其和反射相關(guān) |
CONSTANT_MethodType_info | 16 | 標識方法類型翎承,僅包含方法的參數(shù)類型和返回值類型 |
CONSTANT_InvokeDynamic_info | 18 | 表示一個動態(tài)方法調(diào)用點,用于 invokeDynamic 指令符匾,Java 7引入 |
然后叨咖,我們需要了解其中涉及到的重點常量項類型。這里我們需要先明白 CONSTANT_String 和 CONSTANT_Utf8 的區(qū)別待讳。
CONSTANT_String 和 CONSTANT_Utf8 的區(qū)別
-
CONSTANT_Utf8
:真正存儲了字符串的內(nèi)容芒澜,其對應(yīng)的數(shù)據(jù)結(jié)構(gòu)中有一個字節(jié)數(shù)組,字符串便醞釀其中创淡。 -
CONSTANT_String
:本身不包含字符串的內(nèi)容痴晦,但其具有一個指向 CONSTANT_Utf8 常量項的索引。
我們必須要了解的是琳彩,在所有常見的常量項之中誊酌,只要是需要表示字符串的地方其實際都會包含有一個指向 CONSTANT_Utf8_info 元素的索引。而一個字符串最大長度即 u2 所能代表的最大值為 65536露乏,但是需要使用 2 個字節(jié)來保存 null 值碧浊,所以一個字符串的最大長度為 65534。
對于常見的常量項來說一般可以細分為如下 三個維度瘟仿。
常量項 Utf8
常量項 Utf8 的數(shù)據(jù)結(jié)構(gòu)如下所示:
CONSTANT_Utf8_info {
u1 tag;
u2 length;
u1 bytes[length];
}
復(fù)制代碼
其元素含義如下所示:
- 1)箱锐、
tag
:值為 1,表示是 CONSTANT_Utf8_info 類型表劳较。 - 2)驹止、
length
:length 表示 bytes 的長度浩聋,比如 length = 10,則表示接下來的數(shù)據(jù)是 10 個連續(xù)的 u1 類型數(shù)據(jù)臊恋。 - 3)衣洁、
bytes
:u1 類型數(shù)組,保存有真正的常量數(shù)據(jù)抖仅。
常量項 Class坊夫、Filed、Method撤卢、Interface环凿、String
常量項 Class、Filed凸丸、Method拷邢、Interface、String 的數(shù)據(jù)結(jié)構(gòu)分別如下所示:
CONSATNT_Class_info {
u1 tag;
u2 name_index;
}
復(fù)制代碼
CONSTANT_Fieldref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}
復(fù)制代碼
CONSTANT_MethodType_info {
u1 tag;
u2 descriptor_index;
}
復(fù)制代碼
CONSTANT_InterfaceMethodref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}
復(fù)制代碼
CONSTANT_String_info {
u1 tag;
u2 string_index;
}
復(fù)制代碼
CONSATNT_NameAndType_info {
u1 tag;
u2 name_index;
u2 descriptor_index
}
復(fù)制代碼
其元素含義如下所示:
name_index
:指向常量池中索引為 name_index 的常量表屎慢。比如 name_index = 6,表明它指向常量池中第 6 個常量忽洛。class_index
:指向當前方法腻惠、字段等的所屬類的引用。name_and_type_index
:指向當前方法欲虚、字段等的名字和類型的引用集灌。name_index
:指向某字段或方法等的名稱字符串的引用。descriptor_index
:指向某字段或方法等的類型字符串的引用复哆。
常量項 Integer欣喧、Long、Float梯找、Double
常量項 Integer唆阿、Long、Float锈锤、Double 對應(yīng)的數(shù)據(jù)結(jié)構(gòu)如下所示:
CONSATNT_Integer_info {
u1 tag;
u4 bytes;
}
復(fù)制代碼
CONSTANT_Long_info {
u1 tag;
u4 high_bytes;
u4 low_bytes;
}
復(fù)制代碼
CONSTANT_Float_info {
u1 tag;
u4 bytes;
}
復(fù)制代碼
CONSTANT_Double_info {
u1 tag;
u4 high_bytes;
u4 low_bytes;
}
復(fù)制代碼
可以看到驯鳖,在每一個非基本類型的常量項之中,除了其 tag 之外久免,最終包含的內(nèi)容都是字符串浅辙。正是因為這種互相引用的模式,才能有效地節(jié)省 Class 文件的空間阎姥。(ps:利用索引來減少空間占用是一種行之有效的方式)
三记舆、信息描述規(guī)則
對于 JVM 來說,其 采用了字符串的形式來描述數(shù)據(jù)類型呼巴、成員變量及成員函數(shù) 這三類泽腮。因此御蒲,在討論接下來各個的 Class 表項之前,我們需要了解下 JVM 中的信息描述規(guī)則盛正。下面删咱,我們來一一對此進行探討。
1豪筝、數(shù)據(jù)類型
數(shù)據(jù)類型通常包含有 原始數(shù)據(jù)類型痰滋、引用類型(數(shù)組),它們的描述規(guī)則分別如下所示:
- 1)续崖、原始數(shù)據(jù)類型:
- Java 類型的
byte敲街、char、double严望、float多艇、int、long像吻、short峻黍、boolean
=>"B"、"C"拨匆、"D"姆涩、"F"、"I"惭每、"J"骨饿、"S"、"Z"
台腥。
- Java 類型的
- 2)宏赘、引用數(shù)據(jù)類型:
-
ClassName => L + 全路徑類名(其中的 "." 替換為 "/",最后加分號)黎侈,例如
String => Ljava/lang/String;
察署。
-
ClassName => L + 全路徑類名(其中的 "." 替換為 "/",最后加分號)黎侈,例如
- 3)、數(shù)組(引用類型):
-
不同類型的數(shù)組 => "[該類型對應(yīng)的描述名"蜓竹,例如
int 數(shù)組 => "[I"箕母,String 數(shù)組 => "[Ljava/lang/Sting;",二維 int 數(shù)組 => "[[I"
俱济。
-
不同類型的數(shù)組 => "[該類型對應(yīng)的描述名"蜓竹,例如
2嘶是、成員變量
在 JVM 規(guī)范之中,成員變量即 Field Descriptor 的描述規(guī)則如下所示:
FiledDescriptor:
# 1蛛碌、僅包含 FieldType 一種信息
FieldType
FiledType:
# 2聂喇、FiledType 的可選類型
BaseType | ObjectType | ArrayType
BaseType:
B | C | D | F | I | J | S | Z
ObjectType:
L + 全路徑ClassName;
ArrayType:
[ComponentType:
# 3、與 FiledType 的可選類型一樣
ComponentType:
FiledType
復(fù)制代碼
在注釋1處希太,F(xiàn)iledDescriptor 僅僅包含了 FieldType 一種信息克饶;注釋2處,可以看到誊辉,FiledType 的可選類型為3中:BaseType矾湃、ObjectType、ArrayType堕澄,對于每一個類型的規(guī)則描述邀跃,我們在 數(shù)據(jù)類型 這一小節(jié)已詳細分析過了。而在注釋3處蛙紫,這里 ComponentType
是一種 JVM 規(guī)范中新定義的類型拍屑,不過它是 由 FiledType 構(gòu)成,其可選類型也包含 BaseType坑傅、ObjectType僵驰、ArrayType 這三種。此外唁毒,對于字節(jié)碼來講蒜茴,如果兩個字段的描述符不一致, 那字段重名就是合法的浆西。
3矮男、成員函數(shù)描述規(guī)則
在 JVM 規(guī)范之中,成員函數(shù)即 Method Descriptor 的描述規(guī)則如下所示:
MethodDescriptor:
# 1室谚、括號內(nèi)的是參數(shù)的數(shù)據(jù)類型描述,* 表示有 0 至多個 ParameterDescriptor崔泵,最后是返回值類型描述
( ParameterDescriptor* ) ReturnDescriptor
ParameterDescriptor:
FieldType
ReturnDescriptor:
FieldType | VoidDescriptor
VoidDescriptor:
// 2秒赤、void 的描述規(guī)則為 "V"
V
復(fù)制代碼
在注釋1處,MethodDescriptor 由兩個部分組成憎瘸,括號內(nèi)的是參數(shù)的數(shù)據(jù)類型描述入篮,表示有 0 至多個 ParameterDescriptor,最后是返回值類型描述幌甘。注釋2處潮售,要注意 void 的描述規(guī)則為 "V"。例如锅风,一個 void hello(String str)
的函數(shù) => (Ljava/lang/String;)V
酥诽。
了解了信息的描述規(guī)則之后,我們就可以來看看 Class 表中的其它重要的表項:filed_info 與 method_info皱埠。
四肮帐、filed_info 與 method_info
字段表(field_info)用于描述接口或者類中聲明的變量。字段(field)包括類級變量以及實例級變量,但 不包括在方法內(nèi)部聲明的局部變量训枢。
filed_info 與 method_info 數(shù)據(jù)結(jié)構(gòu)的偽代碼分別如下所示:
field_info {
u2 access_flags;
u2 name
u2 descriptor_index
u2 attributes_count
attribute_info attributes[attributes_count]
}
復(fù)制代碼
method_info {
u2 access_flags;
u2 name
u2 descriptor_index
u2 attributes_count
attribute_info attributes[attributes_count]
}
復(fù)制代碼
可以看到托修,filed_info 與 method_info 都包含有 訪問標志、名字引用恒界、描述信息睦刃、屬性數(shù)量與存儲屬性 的數(shù)據(jù)結(jié)構(gòu)。對于 method_info 所描述的成員函數(shù)來說十酣,它的內(nèi)容經(jīng)過編譯之后得到的 Java 字節(jié)碼會保存在屬性之中涩拙。
注意:類構(gòu)造器為 “< clinit >” 方法,而實例構(gòu)造器為 “< init >” 方法婆誓。
下面吃环,我們就來了解下 access_flags 的相關(guān)知識。
五洋幻、access_flags
access_flag 的取值類型在 Class郁轻、Filed、Method 之中都是不同的文留,我們分別來看看好唯。
1、Class 的 access_flags 取值類型
access_flags 中一共有 16 個標志位可以使用燥翅,當前只定義了其中 8 個(JDK 1.5 增加了后面 3 種)骑篙,沒有使用到的標志位要求一律為 0。Class 的 access_flags 取值類型如下表示:
標志名 | 標志值 | 標志含義 |
---|---|---|
ACC_PUBLIC | 0x0001 | public類型 |
ACC_FINAL | 0x0010 | final類型 |
ACC_SUPER | 0x0020 | 使用新的invokespecial語義 |
ACC_INTERFACE | 0x0200 | 接口類型 |
ACC_ABSTRACT | 0x0400 | 抽象類型 |
ACC_SYNTHETIC | 0x1000 | 該類不由用戶代碼生成 |
ACC_ANNOTATION | 0x2000 | 注解類型 |
ACC_ENUM | 0x4000 | 枚舉類型 |
例如一個 “public Class JsonChao” 的類所對應(yīng)的 access_flags 為 0021(0X0001 和 0X0020 相結(jié)合)森书。下面的 Filed 與 Method 的計算也是同理靶端。
2、Filed 的 access_flag 取值類型
接口之中的字段必須有 ACC_PUBLIC凛膏、ACC_STATIC杨名、ACC_FINAL 標志,這些都是由 Java 本身的語言規(guī)則所決定的猖毫。Filed 的 access_flag 取值類型如下表所示:
名稱 | 值 | 描述 |
---|---|---|
ACC_PUBLIC | 0x0001 | public |
ACC_PRIVATE | 0x0002 | private |
ACC_PROTECTED | 0x0004 | protected |
ACC_STATIC | 0x0008 | static |
ACC_FINAL | 0x0010 | final |
ACC_VOLATILE | 0x0040 | volatile |
ACC_TRANSIENT | 0x0080 | transient台谍,不能被序列化 |
ACC_SYNTHETIC | 0x1000 | 由編譯器自動生成 |
ACC_ENUM | 0x4000 | enum,字段為枚舉類型 |
3吁断、Method 的 access_flag 取值
Method 的 access_flag 取值如下表所示:
名稱 | 值 | 描述 |
---|---|---|
ACC_PUBLIC | 0x0001 | public |
ACC_PRIVATE | 0x0002 | private |
ACC_PROTECTED | 0x0004 | protected |
ACC_STATIC | 0x0008 | static |
ACC_FINAL | 0x0010 | final |
ACC_SYNCHRONIZED | 0x0020 | synchronized |
ACC_BRIDGE | 0x0040 | bridge趁蕊,方法由編譯器產(chǎn)生 |
ACC_VARARGS | 0x0080 | 該方法帶有變長參數(shù) |
ACC_NATIVE | 0x0100 | native |
ACC_ABSTRACT | 0x0400 | abstract |
ACC_STRICT | 0x0800 | strictfp |
ACC_SYNTHETIC | 0x1000 | 方法由編譯器生成 |
需要注意的是,當 Method 的 access_flags 的取值為 ACC_SYNTHETIC
時仔役,該 Method 通常被稱之為 合成函數(shù)掷伙。此外,當內(nèi)部類訪問外部類的私有成員時骂因,在 Class 文件中也會生成一個 ACC_SYNTHETIC 修飾的函數(shù)炎咖。
六、屬性
只要不與已有屬性名重復(fù),任何人 實現(xiàn)的編譯器都可以向?qū)傩员碇袑懭胱约憾x的屬性信息乘盼,Java 虛擬機運行時會忽略掉它所不認識的屬性升熊。
attribute_info 的數(shù)據(jù)結(jié)構(gòu)偽代碼如下所示:
attribute_info {
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length];
}
復(fù)制代碼
attribute_info 中的各個元素的含義如下所示:
-
attribute_name_index
:為 CONSTANT_Utf8 類型常量項的索引,表示屬性的名稱绸栅。 -
attribute_length
:屬性的長度级野。 -
info
:屬性具體的內(nèi)容。
1粹胯、attribute_name_index
attribute_name_index 所指向的 Utf8 字符串即為屬性的名稱蓖柔,而 屬性的名稱是被用來區(qū)分屬性的。所有的屬性名稱如下所示(其中下面?? 標紅的為重要屬性):
- 1)风纠、
ConstantValue
:僅出現(xiàn)在 filed_info 中况鸣,描述常量成員域的值,通知虛擬機自動為靜態(tài)變量賦值竹观。對于非 static 類型的變量(也就是實例變量)的賦值是在實例構(gòu)造器方法中進行的;而對 于類變量镐捧,則有兩種方式可以選擇:在類構(gòu)造器方法中或者使用 ConstantValue 屬性。如果變量沒有被 final 修飾臭增,或者并非基本類型及字 符串懂酱,則將會選擇在方法中進行初始化。 - 2)誊抛、
Code
:僅出現(xiàn) method_info 中列牺,描述函數(shù)內(nèi)容,即該函數(shù)內(nèi)容編譯后得到的虛擬機指令拗窃,try/catch 語句對應(yīng)的異常處理表等等瞎领。 - 3)、
StackMapTable
:在 JDK 1.6 發(fā)布后增加到了 Class 文件規(guī)范中随夸,它是一個復(fù)雜的變長屬性默刚。這個屬性會在虛擬機類加載的字節(jié)碼驗證階段被新類型檢查驗證器(Type Checker)使用,目的在于代替以前比較消耗性能的基于數(shù)據(jù)流 分析的類型推導(dǎo)驗證器逃魄。它省略了在運行期通過數(shù)據(jù)流分析去確認字節(jié)碼的行為邏輯合法性的步驟,而是在編譯階 段將一系列的驗證類型(Verification Types)直接記錄在 Class 文件之中澜搅,通過檢查這些驗證類型代替了類型推導(dǎo)過程伍俘,從而大幅提升了字節(jié)碼驗證的性能。這個驗證器在 JDK 1.6 中首次提供勉躺,并在 JDK 1.7 中強制代替原本基于類型推斷的字節(jié)碼驗證器癌瘾。StackMapTable 屬性中包含零至多個棧映射幀(Stack Map Frames),其中的類型檢查驗證器會通過檢查目標方法的局部變量和操作數(shù)棧所需要的類型來確定一段字節(jié)碼指令是否符合邏輯約束饵溅。 - 4)妨退、
Exceptions
:當函數(shù)拋出異常或錯誤時,method_info 將會保存此屬性咬荷。 - 5)冠句、InnerClasses:用于記錄內(nèi)部類與宿主類之間的關(guān)聯(lián)。
- 6)幸乒、EnclosingMethod
- 7)懦底、Synthetic:標識方法或字段為編譯器自動生成的。
- 8)罕扎、
Signature
:JDK 1.5 中新增的屬性聚唐,用于支持泛型情況下的方法簽名,由于 Java 的泛型采用擦除法實現(xiàn)腔召,在為了避免類型信息被擦除后導(dǎo)致簽名混亂杆查,需要這個屬性記錄泛型中的相關(guān)信息。 - 9)臀蛛、
SourceFile
:包含一個指向 Utf8 常量項的索引亲桦,即 Class 對應(yīng)的源碼文件名。 - 10)掺栅、SourceDebugExtension:用于存儲額外的調(diào)試信息烙肺。
- 11)、
LineNumberTable
:Java 源碼的行號與字節(jié)碼指令的對應(yīng)關(guān)系氧卧。 - 12)桃笙、
LocalVariableTable
:局部變量數(shù)組/本地變量表,用于保存變量名沙绝,變量定義所在行搏明。 - 13)、
LocalVariableTypeTable
:JDK 1.5 中新增的屬性闪檬,它使用特征簽名代替描述符星著,是為了引入泛型語法之后能描述泛型參數(shù)化類型而添加。 - 14)粗悯、Deprecated
- 15)虚循、RuntimeVisibleAnnotations
- 16)、RuntimeInvisibleAnnotations
- 17)样傍、RuntimeVisibleParameterAnnotations
- 18)横缔、RuntimeInvisibleParameterAnnotations
- 19)、AnnotationDefault
- 20)衫哥、BootstrapMethods:JDK 1.7中新增的屬性靖秩,用于保存 invokedynamic 指令引用的引導(dǎo)方法限定符怜校。切記忽媒,類文件的屬性表中最多也只能有一個 BootstrapMethods 屬性寓辱。
在上述表格中粮坞,我們可以發(fā)現(xiàn),不同類型的屬性可能會出現(xiàn)在 ClassFile 中不同的成員里初狰,當 JVM 在解析 Class 文件時會校驗 Class 成員應(yīng)該禁止攜帶有哪些類型的屬性莫杈。此外,屬性也可以包含子屬性跷究,例如:"Code" 屬性中包含有 "LocalVariableTable"姓迅。
2、Code_attribute(??)
首先俊马,要注意 并非所有的方法表都必須存在這個屬性丁存,例如接口或者抽象類中的方法就不存在 Code 屬性。
Code_attribute 的數(shù)據(jù)結(jié)構(gòu)偽代碼如下所示:
Code_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 max_stack;
u2 max_locals;
u4 code_length;
u1 code[code_length];
u2 exception_table_length;
{
u2 start_pc;
u2 end_pc;
u2 handler_pc;
u2 catch_type;
} exception_table[exception_table_length];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
復(fù)制代碼
Code_attribute 中的各個元素的含義如下所示:
-
attribute_name_index柴我、attribute_length
:attribute_length 的值為整個 Code 屬性減去 attribute_name_index 和 attribute_length 的長度解寝。 -
max_stack
:為當前方法執(zhí)行時的最大棧深度,所以 JVM 在執(zhí)行方法時艘儒,線程棧的棧幀(操作數(shù)棧聋伦,operand satck)大小是可以提前知道的。每一個函數(shù)執(zhí)行的時候都會分配一個操作數(shù)棧和局部變量數(shù)組界睁,而 Code_attribure 需要包含它們觉增,以便 JVM 在執(zhí)行函數(shù)前就可以分配相應(yīng)的空間。 -
max_locals
:**為當前方法分配的局部變量個數(shù)翻斟,包括調(diào)用方式時傳遞的參數(shù)逾礁。long 和 double 類型計數(shù)為 2,其他為 1访惜。max_locals 的單位是 Slot,Slot 是
虛擬機為局部變量分配內(nèi)存所使用的最小單位嘹履。局部變量表中的 Slot 可以重用,當代碼執(zhí)行超出一個局部變量的作用域時债热,這個局部變量 所占的 Slot 可以被其他局部變量所使用砾嫉,Javac 編譯器會根據(jù)變量的作用域來分配 Slot 給各個 變量使用,然后計算出 max_locals 的大小**窒篱。
-
code_length
:為方法編譯后的字節(jié)碼的長度焕刮。 -
code:用于存儲字節(jié)碼指令的一系列字節(jié)流。既然叫字節(jié)碼指令墙杯,那么每個指令就是一個 u1 類型的單字節(jié)济锄。一個 u1 數(shù)據(jù)類型的取值范圍為 0x00
0xFF,對應(yīng)十進制的 0255霍转,也就是一共可以表達 256 條指令。 -
exception_table_length
:表示 exception_table 的長度一汽。 -
exception_table
:每個成員為一個 ExceptionHandler避消,并且一個函數(shù)可以包含多個 try/catch 語句低滩,一個 try/catch 語句對應(yīng) exception_table 數(shù)組中的一項。 -
start_pc岩喷、end_pc
:為異常處理字節(jié)碼在 code[] 的索引值恕沫。當程序計數(shù)器在 [start_pc, end_pc) 內(nèi)時,表示異常會被該 ExceptionHandler 捕獲纱意。 -
handler_pc
:表示 ExceptionHandler 的起點婶溯,為 code[] 的索引值。 -
catch_type
:為 CONSTANT_Class 類型常量項的索引偷霉,表示處理的異常類型迄委。如果該值為 0,則該 ExceptionHandler 會在所有異常拋出時會被執(zhí)行类少,可以用來實現(xiàn) finally 代碼叙身。當 catch_type 的值為 0 時,代表任意異常情況都需要轉(zhuǎn)向到 handler_pc 處進行處理硫狞。此外信轿,編譯器使用異常表而不是簡單的跳轉(zhuǎn)命令來實現(xiàn) Java 異常及 finally 處理機制。 -
attributes_count 和 attributes
:表示該 exception_table 擁有的 attribute 數(shù)量與數(shù)據(jù)残吩。
在 Code_attribute 攜帶的屬性中财忽,"LineNumberTable"
與 "LocalVariableTable"
對我們 Android 開發(fā)者來說比較重要,所以泣侮,這里我們將再單獨來講解一下它們即彪。
1)、LineNumberTable 屬性
LineNumberTable 屬性 用于 Java 的調(diào)試旁瘫,可指明某條指令對應(yīng)于源碼哪一行祖凫。
LineNumberTable 屬性的結(jié)構(gòu)如下所示:
LineNumberTable_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 line_number_table_length;
{ u2 start_pc;
u2 line_number;
} line_number_table[line_number_table_length];
}
復(fù)制代碼
其中最重要的是 line_number_table
數(shù)組,該數(shù)組元素包含如下 兩個成員變量:
- 1酬凳、
start_pc
:為 code[] 數(shù)組元素的索引惠况,用于指向 Code_attribute 中 code 數(shù)組某處指令。 - 2宁仔、
line_number
:為 start_pc 對應(yīng)源文件代碼的行號稠屠。需要注意的是,多個 line_number_table 元素可以指向同一行代碼翎苫,因為一行 Java 代碼很可能被編譯成多條指令权埠。
2、LocalVariableTable 屬性
LocalVariableTable 屬性用于 描述棧幀中局部變量表中的變量與 Java 源碼中定義的變量之間的關(guān)系煎谍,它也不是運行時必需的屬性攘蔽,但默認會生成到 Class 文件之中。
LocalVariableTable 的數(shù)據(jù)結(jié)構(gòu)如下所示:
LocalVariableTable_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 local_variable_table_length;
{
u2 start_pc;
u2 length;
u2 name_index;
u2 descriptor_index;
u2 index;
} local_variable_table[local_variable_table_length];
}
復(fù)制代碼
其中最重要的元素是 local_variable_table
數(shù)組呐粘,其中的 start_pc
與 length
這兩個參數(shù) 決定了一個局部變量在 code 數(shù)組中的有效范圍满俗。
需要注意的是转捕,每個非 static 函數(shù)都會自動創(chuàng)建一個叫做 this 的本地變量,代表當前是在哪個對象上調(diào)用此函數(shù)唆垃。并且五芝,this 對象是位于局部變量數(shù)組第1個位置(即 Slot = 0),它的作用范圍是貫穿整個函數(shù)的辕万。
此外枢步,在 JDK 1.5 引入泛型之后,LocalVariableTable 屬性增加了一個 “姐妹屬性”: LocalVariableTypeTable渐尿,這個新增的屬性結(jié)構(gòu)與 LocalVariableTable 非常相似醉途,僅僅是把記錄 的字段描述符的 descriptor_index 替換成了字段的特征簽名(Signature),對于非泛型類型來 說涡戳,描述符和特征簽名能描述的信息是基本一致的结蟋,但是泛型引入之后,由于描述符中泛型的參數(shù)化類型被擦除掉渔彰,描述符就不能準確地描述泛型類型了嵌屎,因此出現(xiàn)了 LocalVariableTypeTable。
Slot 是什么恍涂?
JVM 在調(diào)用一個函數(shù)的時候宝惰,會創(chuàng)建一個局部變量數(shù)組(即 LocalVariableTable),而 Slot 則表示當前變量在數(shù)組中的位置再沧。
七尼夺、JVM 指令碼(??)
在上面,我們了解了 常量池炒瘸、屬性淤堵、field_info、method_info 等等一系列的源碼文件組成結(jié)構(gòu)顷扩,它們是僅僅是一種靜態(tài)的內(nèi)容拐邪,這些信息并不能驅(qū)使 JVM 執(zhí)行我們在源碼中編寫的函數(shù)。
從前可知隘截,Code_attribute 中的 code 數(shù)組存儲了一個函數(shù)源碼經(jīng)過編譯后得到的 JVM 字節(jié)碼扎阶,其中僅包含如下 兩種 類型的信息:
- 1)、
JVM 指令碼
:用于指示 JVM 執(zhí)行的動作婶芭,例如加操作/減操作/new 對象东臀。其長度為 1 個字節(jié),所以 JVM 指令碼的個數(shù)不會超過 255 個(0xFF)犀农。 - 2)惰赋、
JVM 指令碼后的零至多個操作數(shù)
:操作數(shù)可以存儲在 code 數(shù)組中,也可以存儲在操作數(shù)棧(Operand stack)中呵哨。
一個 Code 數(shù)組里指令和參數(shù)的組織格式 如下所示:
1字節(jié)指令碼 0或多個參數(shù)(N字節(jié)赁濒,N>=0)
可以看到贵扰,Java 虛擬機的指令由一個字節(jié)長度的、代表著某種特定操作含義的數(shù)字(稱為操作 碼流部,Opcode)以及跟隨其后的零至多個代表此操作所需參數(shù)(稱為操作數(shù),Operands)而構(gòu)成纹坐。此外枝冀,大多數(shù)的指令都不包含操作數(shù),只有一個操作碼耘子。
字節(jié)碼指令集是一種具有鮮明特點果漾、優(yōu)劣勢都很突出的指令集架構(gòu),由于限制了 Java 虛擬機操作碼的長度為一個字節(jié)(即 0~255)谷誓,這意味著指令集的操作碼總數(shù)不可能超過 256 條绒障。
如果不考慮異常處理的話,那么 Java 虛擬機的解釋器可以使用下面這個偽代碼當做 最基本的執(zhí)行模型 來理解捍歪,如下所示:
do {
自動計算PC寄存器的值加1;
根據(jù)PC寄存器的指示位置户辱,從字節(jié)碼流中取出操作碼;
if(字節(jié)碼存在操作數(shù))從字節(jié)碼流中取出操作數(shù);
執(zhí)行操作碼所定義的操作;
} while (字節(jié)碼流長度>0);
復(fù)制代碼
由于 Java 虛擬機的操作碼長度只有一個字節(jié),所以糙臼,Java 虛擬機的指令集 對于特定的操作只提供了有限的類型相關(guān)指令去支持它庐镐。例如 在 JVM 中,大部分的指令都沒有支持整數(shù)類型 byte变逃、char 和 short必逆,甚至沒有任何指令支持 boolean 類型。因此揽乱,我們在處理 boolean名眉、byte、short 和 char 類型的數(shù)組時凰棉,需要轉(zhuǎn)換為與之對應(yīng)的 int 類型的字節(jié)碼指令來處理损拢。
眾所周知,JVM 是基于棧而非寄存器的計算模型渊啰,并且探橱,基于棧的實現(xiàn)能夠帶來很好的跨平臺特性,因為寄存器指令往往和硬件掛鉤绘证。但是隧膏,由于棧只是一個 FILO 的結(jié)構(gòu),需要頻繁地壓棧與出棧嚷那,因此胞枕,對于同樣的操作,基于棧的實現(xiàn)需要更多指令才能完成魏宽。此外腐泻,由于 JVM 需要實現(xiàn)跨平臺的特性决乎,因此棧是在內(nèi)存實現(xiàn)的,而寄存器則位于 CPU 的高速緩存區(qū)派桩,因此构诚,基于棧的實現(xiàn)其速度速度相比寄存器的實現(xiàn)要慢很多。要深入了解 JVM 的指令集铆惑,我們就必須先從 JVM 運行時的棧幀講起范嘱。
1、運行時的棧幀
棧幀(Stack Frame)是用于支持虛擬機進行方法調(diào)用和方法執(zhí)行的數(shù)據(jù)結(jié)構(gòu)员魏,它是虛擬 機運行時數(shù)據(jù)區(qū)中的虛擬機棧(Virtual Machine Stack)的棧元素丑蛤。
棧幀中存儲了方法的 局部變量表、操作數(shù)棧撕阎、動態(tài)連接和方法返回地址受裹、幀數(shù)據(jù)區(qū) 等信息。每一個方法從調(diào)用開始至執(zhí)行完成的過程虏束,都對應(yīng)著一個棧幀在虛擬機棧里面從入棧到出棧的過程棉饶。
一個線程中的方法調(diào)用鏈可能會很長,很多方法都同時處于執(zhí)行狀態(tài)魄眉。對于 JVM 的執(zhí)行引擎來 說砰盐,在活動線程中,只有位于棧頂?shù)臈攀怯行У目勇桑Q為當前棧幀(Current Stack Frame)岩梳,與這個棧幀相關(guān)聯(lián)的方法稱為當前方法(Current Method)。執(zhí)行引擎運行的所有 字節(jié)碼指令都只針對當前棧幀進行操作晃择,而 棧幀的結(jié)構(gòu) 如下圖所示:
Java 中當一個方法被調(diào)用時會產(chǎn)生一個棧幀(Stack Frame),而此方法便位于棧幀之內(nèi)冀值。而Java方法棧幀 主要包括三個部分,如下所示:
- 1)宫屠、局部變量區(qū)
- 2)列疗、操作數(shù)棧區(qū)
- 3)、幀數(shù)據(jù)區(qū)(常量池引用)
幀數(shù)據(jù)區(qū)浪蹂,即常量池引用在前面我們已經(jīng)深入地了解過了抵栈,但是還有兩個重要部分我們需要了解,一個是操作數(shù)棧坤次,另一個則是局部變量區(qū)古劲。通常來說,程序需要將局部變量區(qū)的元素加載到操作數(shù)棧中缰猴,計算完成之后产艾,然后再存儲回局部變量區(qū)。
查看字節(jié)碼的工具
我們可以使用 jclasslib 這個字節(jié)碼工具去查看字節(jié)碼,使用效果如下圖所示闷堡,代碼編譯后在菜單欄 ”View” 中選擇 ”Show Bytecode With jclasslib”隘膘,可以很直觀地看到當前字節(jié)碼文件的類信息、常量池杠览、方法區(qū)等信息弯菊。
下面??,我們就先來看看操作數(shù)棧是怎么運轉(zhuǎn)的踱阿。
2误续、操作數(shù)棧
操作數(shù)棧是為了 存放計算的操作數(shù)和返回結(jié)果。在執(zhí)行每一條指令前扫茅,JVM 要求該指令的操作數(shù)已經(jīng)被壓入到操作數(shù)棧中,并且育瓜,在執(zhí)行指令時葫隙,JVM 會將指令所需的操作數(shù)彈出,并將計算結(jié)果壓入操作數(shù)棧中躏仇。
對于操作數(shù)棧相關(guān)的操作指令有如下 三類:
1)恋脚、直接作用于操作數(shù)據(jù)棧的指令:
-
dup
:復(fù)制棧頂元素,常用于復(fù)制 new 指令所生成的未初始化的引用焰手。 -
pop
:舍棄棧頂元素糟描,常用于舍棄調(diào)用指令的返回結(jié)果。 -
wap
:交換棧頂?shù)膬蓚€元素的值书妻。
需要注意的是船响,當值為 long 或 double 類型時,需要占用兩個棧單元躲履,此時需要使用 dup2/pop2 指令替代 dup/pop 指令见间。
2)、直接將常量加載到操作數(shù)棧的指令:
對于 int(boolean工猜、byte米诉、char、short)
類型來說篷帅,有如下三類常用指令:
-
iconst
:用于加載 [-1 ,5] 的 int 值史侣。 -
biconst
:用于加載一個字節(jié)(byte)所能代表的 int 值即 [-128-127]。 -
sipush
:用于加載兩個字節(jié)(short)所能代表的 int 值即 [-32768-32767]魏身。
而對于 long惊橱、float、double叠骑、reference
類型來說李皇,各個類型都僅有一類,其實就是類似于 iconst 指令,即 lconst掉房、fconst茧跋、dconst、aconst
卓囚。
3)瘾杭、加載常量池中的常量值的指令:
-
ldc
:用于加載常量池中的常量值,如 int哪亿、long粥烁、float、double蝇棉、String讨阻、Class 類型的常量。例如 ldc #35 將加載常量池中的第 35 項常量值篡殷。
正常情況下钝吮,操作數(shù)棧的壓入彈出都是一條條指令完成。唯一的例外是在拋異常時板辽,JVM 會清除操作數(shù)棧的所有內(nèi)容奇瘦,然后將異常實例壓入操作數(shù)棧中。
3劲弦、局部變量區(qū)
局部變量區(qū)一般用來 緩存計算的結(jié)果耳标。實際上,JVM 會把局部變量區(qū)當成一個 數(shù)組邑跪,里面會依次緩存 this 指針(非靜態(tài)方法)次坡、參數(shù)、局部變量画畅。
需要注意的是贸毕,同操作數(shù)棧一樣,long 和 double 類型的值將占據(jù)兩個單元夜赵,而其它的類型僅僅占據(jù)一個單元明棍。
而對于局部變量區(qū)來說,它常用的操作指令有 三種寇僧,如下所示:
1)摊腋、將局部變量區(qū)的值加載到操作數(shù)棧中
-
int(boolean、byte嘁傀、char兴蒸、short)
:iload -
long
:lload -
float
:fload -
double
:dload -
reference
:aload
2)、將操作數(shù)棧中的計算結(jié)果存儲在局部變量區(qū)中
-
int(boolean细办、byte橙凳、char蕾殴、short)
:istore -
long
:lstore -
float
:fstore -
double
:dstore -
reference
:astore
這里需要注意的是,局部變量的加載與存儲指令都需要指明所加載單元的下標岛啸,例如:iload_0 就是加載普通方法局部變量區(qū)中的 this 指針钓觉。
3)、增值指令之 iinc
可以看到坚踩,上面兩種類型的指令操作都需要操作局部變量區(qū)和操作數(shù)棧荡灾,那么,有沒有 僅僅只作用在局部變量區(qū)的指令呢瞬铸?
它就是 iinc M N(M為負整數(shù)批幌,N為整數(shù)),它會將局部變量數(shù)組中的第 M 個單元中的 int 值增加 N嗓节,常用于 for 循環(huán)中自增量的更新荧缘,如 i++/i--。
了解了以上 JVM 的基礎(chǔ)指令之后拦宣,我們來看一個具體的栗子??胜宇,代碼和其對應(yīng)的 JVM 指令如下所示:
public static int bar(int i) {
return ((i + 1) - 2) * 3 / 4;
}
// 對應(yīng)的字節(jié)碼如下:
Code:
stack=2, locals=1, args_size=1
0: iload_0
1: iconst_1
2: iadd
3: iconst_2
4: isub
5: iconst_3
6: imul
7: iconst_4
8: idiv
9: ireturn
復(fù)制代碼
這里我們解釋下上面的幾處字節(jié)碼的含義,如下所示:
-
Code
:JVM 字節(jié)碼恢着。 -
stack
:表示該方法需要的操作數(shù)棧空間為 2财破。 -
locals
:表示該方法需要的局部變量區(qū)空間為 1掰派。 -
args_size
:表示方法的參數(shù)大小為 1。
最后左痢,我們來看看 每條指令執(zhí)行前后局部變量區(qū)和操作數(shù)棧的變化情況靡羡,如下圖所示:
了解了指令在操作數(shù)棧與局部變量區(qū)之間的轉(zhuǎn)換規(guī)律,我們下面再回過頭來系統(tǒng)地了解下以下 九類按用途分類的字節(jié)碼指令俊性。
4略步、字節(jié)碼指令用途分類匯總
1)、加載和存儲指令
加載和存儲指令用于 將數(shù)據(jù)在棧幀中的局部變量表和操作數(shù)棧之間來回傳輸定页,其指令如下所示:
- 1)趟薄、將一個局部變量加載到操作棧:`iload、iload_典徊、lload杭煎、lload_、fload卒落、fload_
羡铲、dload、dload_儡毕、aload也切、aload_`。
- 2)、將一個數(shù)值從操作數(shù)棧存儲到局部變量表:`istore雷恃、istore_疆股、lstore、lstore_褂萧、
fstore押桃、fstore_、dstore导犹、dstore_唱凯、astore、astore_`谎痢。
- 3)磕昼、將一個常量加載到操作數(shù)棧:`bipush、sipush节猿、ldc票从、ldc_w、ldc2_w滨嘱、aconst_null峰鄙、
iconst_m1、iconst_太雨、lconst_吟榴、fconst_、dconst_`囊扳。
** 4)吩翻、擴充局部變量表的訪問索引的指令:wide
。
類似于 iload_锥咸,它代表了 iload_0狭瞎、iload_1、iload_2 和 iload_3 這幾條指令搏予。這幾組指令都是某個帶有一個操作數(shù)的通用指令(例如iload熊锭,iload_0 的語義與操作數(shù)為 0 時的 iload 指令語義完全一致)。
2)雪侥、運算指令
運算或算術(shù)指令用于 對兩個操作數(shù)棧上的值進行某種特定運算球涛,并把結(jié)果重新存入到操 作棧頂。大體上算術(shù)指令可以分為 兩種:對整型數(shù)據(jù)進行運算的指令與對浮點型數(shù)據(jù)進行運算的指令校镐。其指令如下所示:
- 1)亿扁、加法指令:
iadd、ladd鸟廓、fadd从祝、dadd
襟己。 - 2)、減法指令:
isub牍陌、lsub擎浴、fsub、dsub
毒涧。 - 3)贮预、乘法指令:
imul、lmul契讲、fmul仿吞、dmul
。 - 4)捡偏、除法指令:
idiv唤冈、ldiv、fdiv银伟、ddiv
你虹。 - 5)、求余指令:
irem彤避、lrem傅物、frem、drem
琉预。 - 6)董饰、取反指令:
ineg、lneg模孩、fneg、dneg
贮缅。 - 7)榨咐、位移指令:
ishl、ishr谴供、iushr块茁、lshl、lshr绒尊、lushr
译暂。 - 8)表制、按位或指令:
ior、lor
佩耳。 - 9)、按位與指令:
iand谭跨、land
干厚。 - 10)李滴、按位異或指令:
ixor、lxor
蛮瞄。 - 11)所坯、局部變量自增指令:
iinc
。 - 12)挂捅、比較指令:
dcmpg芹助、dcmpl、fcmpg闲先、fcmpl状土、lcmp
。
3)饵蒂、類型轉(zhuǎn)換指令
類型轉(zhuǎn)換指令可以 將兩種不同的數(shù)值類型進行相互轉(zhuǎn)換声诸,例如我們可以將小范圍類型向大范圍類型的安全轉(zhuǎn)換,其指令如下所示:
-1)退盯、i2b彼乌、i2c、i2s
-2)渊迁、l2i
-3)慰照、f2i、f2l
-4)琉朽、d2i毒租、d2l、d2f
4)箱叁、對象創(chuàng)建與訪問指令
其指令如下所示:
- 1)墅垮、創(chuàng)建類實例的指令:
new
。 - 2)耕漱、創(chuàng)建數(shù)組的指令:
newarray算色、anewarray、multianewarray
螟够。 - 3)灾梦、訪問類字段(static字段,或者稱為類變量)和實例字段(非 static 字段妓笙,或者稱為實例變量)的指令:
getfield若河、putfield、getstatic寞宫、putstatic
萧福。 - 4)、把一個數(shù)組元素加載到操作數(shù)棧的指令:
baload辈赋、caload统锤、saload毛俏、iaload、laload饲窿、 faload煌寇、daload、aaload
逾雄。 - 5)阀溶、將一個操作數(shù)棧的值存儲到數(shù)組元素中的指令:
bastore、castore鸦泳、sastore银锻、iastore、 fastore做鹰、dastore击纬、aastore
。 - 6)钾麸、取數(shù)組長度的指令:
arraylength
更振。 - 7)、檢查類實例類型的指令:
instanceof饭尝、checkcast
肯腕。
5)、操作數(shù)棧管理指令
用于 直接操作操作數(shù)棧 的指令钥平,如下所示:
- 1)实撒、將操作數(shù)棧的棧頂一個或兩個元素出棧:
pop、pop2(用于操作 Long涉瘾、Double)
知态。 - 2)、復(fù)制棧頂一個或兩個數(shù)值并將復(fù)制值或雙份的復(fù)制值重新壓入棧頂:
dup立叛、dup2负敏、dup_x1、dup2_x1囚巴、dup_x2原在、dup2_x2
友扰。 - 3)彤叉、將棧最頂端的兩個數(shù)值互換:
swap
。
6)村怪、控制轉(zhuǎn)移指令
控制轉(zhuǎn)移指令就是 在有條件或無條件地修改 PC 寄存器的值秽浇。其指令如下所示:
- 1)、條件分支:
ifeq甚负、iflt柬焕、ifle审残、ifne、ifgt斑举、ifge搅轿、ifnull、ifnonnull富玷、if_icmpeq璧坟、if_icmpne、 if_icmplt赎懦、if_icmpgt雀鹃、if_icmple、if_icmpge励两、if_acmpeq 和 if_acmpne
黎茎。 - 2)、復(fù)合條件分支:
tableswitch当悔、lookupswitch
傅瞻。 - 3)、無條件分支:
goto先鱼、goto_w俭正、jsr、jsr_w焙畔、ret
掸读。
其中的 tableswitch 與 lookupswitch 含義如下:
-
tableswitch
:條件跳轉(zhuǎn)指令,針對密集的 case宏多。 -
lookupswitch
:條件跳轉(zhuǎn)指令儿惫,針對稀疏的 case。
可以看到伸但,Java 虛擬機提供的 int 類型的條件分支指令是最為豐富和強大的肾请。
7)、方法調(diào)用指令
常用的有 5條 用于方法調(diào)用的指令更胖。 如下所示:
- 1)铛铁、
invokevirtual
:用于調(diào)用對象的實例方法,根據(jù)對象的實際類型進行分派(虛方法分派)却妨,這也是 Java 語言中最常見的方法分派方式饵逐。 - 2)、
invokeinterface
:用于調(diào)用接口方法彪标,它會在運行時搜索一個實現(xiàn)了這個接口方法的對象倍权,找出適合的方法進行調(diào)用。 - 3)捞烟、
invokespecial
:用于調(diào)用一些需要特殊處理的實例方法薄声,包括實例初始化方法当船、私有方法和父類方法。 - 4)默辨、
invokestatic
:用于調(diào)用類方法(static方法)德频。 - 5)、
invokedynamic
:用于在運行時動態(tài)解析出調(diào)用點限定符所引用的方法缩幸,并執(zhí)行該方法抱婉,前面 4 條調(diào)用指令的分派邏輯都固化在 Java 虛擬機內(nèi)部,而 invokedynamic 指令的分派邏輯是由用戶所設(shè)定的引導(dǎo)方法決定的桌粉。
這里我們需要著重注意 invokespecial
指令蒸绩,它用于 調(diào)用構(gòu)造器與方法,當調(diào)用方法時铃肯,會將返回值仍然壓入操作數(shù)棧中患亿,如果當前方法沒有返回值則需要使用 pop 指令彈出。
除了 invokespecial 之外押逼,其它方法調(diào)用指令所消耗的操作數(shù)棧元素是根據(jù)調(diào)用類型以及目標方法描述符來確定的步藕。
8)、方法返回指令
返回指令是區(qū)分類型的挑格,如下所示咙冗,為不同返回類型對應(yīng)的返回指令:
-
void
:return -
int(boolean、byte漂彤、char雾消、short)
:ireturn -
long
:lreturn -
float
:freturn -
double
:dreturn -
reference
:areturn
方法調(diào)用指令與數(shù)據(jù)類型無關(guān),而 方法返回指令是根據(jù)返回值的類型區(qū)分的挫望,包括 ireturn(當返回值是 boolean立润、byte、char媳板、short 和 int 類型時使用)桑腮、lreturn、freturn蛉幸、dreturn 和 areturn破讨,另外還有一條 return 指令供聲明為 void 的方法、實例初始化方法以及類和接口的類初始化方法使用奕纫。
9)提陶、異常處理指令
在 Java 程序中顯式拋出異常的操作(throw語句)都由 athrow 指令來實現(xiàn),在 Java 虛擬機中若锁,處理異常是采用異常表來完成的搁骑。
10)斧吐、同步指令
Java 虛擬機可以 支持方法級的同步和方法內(nèi)部一段指令序列的同步又固,這兩種同步結(jié)構(gòu)都是使用管程(Monitor)來支持的仲器。
方法級的同步是隱式的,即無須通過字節(jié)碼指令來控制仰冠,它實現(xiàn)在方法調(diào)用和返回操作 之中乏冀。虛擬機可以從方法常量池的方法表結(jié)構(gòu)中的 ACC_SYNCHRONIZED 訪問標志得知一個方法是否聲明為同步方法。
當方法調(diào)用時洋只,調(diào)用指令將會檢查方法的 ACC_SYNCHRONIZED 訪問標志是否被設(shè)置辆沦,如果設(shè)置了,執(zhí)行線程就要求先成功持有管程识虚,然后才能執(zhí)行方法肢扯,最后當方法完成(無論是正常完成還是非正常完成)時會釋放管程。
同步一段指令集序列 通常是由 Java 語言中的 synchronized 語句塊 來表示的担锤,Java 虛擬機的指令集中有 monitorenter 和 monitorexit 兩條指令來支持 synchronized 關(guān)鍵字的語義蔚晨,而正確實現(xiàn) synchronized 關(guān)鍵字需要 Javac 編譯器與 Java 虛擬機兩者共同協(xié)作支持。
編譯器必須確保無論方法通過何種方式完成肛循,方法中調(diào)用過的每條 monitorenter 指令都必須執(zhí)行其對應(yīng)的 monitorexit 指令铭腕,而無論這個方法是正常結(jié)束還是異常結(jié)束。并且多糠,它會自動產(chǎn)生一個異常處理器累舷,這個異常處理器被聲明可處理所有的異常,它的目的就是用來執(zhí)行 monitorexit 指令夹孔。
八被盈、總結(jié)
深入學(xué)習(xí) JVM 字節(jié)碼無疑會對我們的整體實力有 質(zhì)的提升,如果對 JVM 字節(jié)碼了解較深搭伤,那么害捕,我們在學(xué)習(xí) Groovy、Kotlin 等這些基于 JVM 的語言時就能夠 在較短的學(xué)習(xí)時間內(nèi)進階到語言的高級層面闷畸。此外尝盼,深入了解 JVM 字節(jié)碼,能夠賦予我們通過表象透析本質(zhì)的能力佑菩,而這盾沫,也正是極客們真正所追求的一通百通的靈魂之力。*
作者:jsonchao
鏈接:https://juejin.cn/post/6844904116603486222
來源:掘金
著作權(quán)歸作者所有殿漠。商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán)赴精,非商業(yè)轉(zhuǎn)載請注明出處。