深讀解析編譯插樁技術(shù)(三)解密 JVM 字節(jié)碼

成為一名優(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_version2 個字節(jié)長抄罕,表示當前 Class 文件的次版號允蚣。
  • 3)、major_version2 個字節(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)驹止、lengthlength 表示 bytes 的長度浩聋,比如 length = 10,則表示接下來的數(shù)據(jù)是 10 個連續(xù)的 u1 類型數(shù)據(jù)臊恋。
  • 3)衣洁、bytesu1 類型數(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"台腥。
  • 2)宏赘、引用數(shù)據(jù)類型
    • ClassName => L + 全路徑類名(其中的 "." 替換為 "/",最后加分號)黎侈,例如 String => Ljava/lang/String;察署。
  • 3)、數(shù)組(引用類型)
    • 不同類型的數(shù)組 => "[該類型對應(yīng)的描述名"蜓竹,例如 int 數(shù)組 => "[I"箕母,String 數(shù)組 => "[Ljava/lang/Sting;",二維 int 數(shù)組 => "[[I"俱济。

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)罕扎、SignatureJDK 1.5 中新增的屬性聚唐,用于支持泛型情況下的方法簽名,由于 Java 的泛型采用擦除法實現(xiàn)腔召,在為了避免類型信息被擦除后導(dǎo)致簽名混亂杆查,需要這個屬性記錄泛型中的相關(guān)信息
  • 9)臀蛛、SourceFile包含一個指向 Utf8 常量項的索引亲桦,即 Class 對應(yīng)的源碼文件名
  • 10)掺栅、SourceDebugExtension:用于存儲額外的調(diào)試信息烙肺。
  • 11)、LineNumberTableJava 源碼的行號與字節(jié)碼指令的對應(yīng)關(guān)系氧卧。
  • 12)桃笙、LocalVariableTable局部變量數(shù)組/本地變量表,用于保存變量名沙绝,變量定義所在行搏明。
  • 13)、LocalVariableTypeTableJDK 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_lengthattribute_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ù)類型的取值范圍為 0x000xFF,對應(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_pclength 這兩個參數(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
  • longlload
  • floatfload
  • doubledload
  • referenceaload

2)、將操作數(shù)棧中的計算結(jié)果存儲在局部變量區(qū)中

  • int(boolean细办、byte橙凳、char蕾殴、short)istore
  • longlstore
  • floatfstore
  • doubledstore
  • referenceastore

這里需要注意的是,局部變量的加載與存儲指令都需要指明所加載單元的下標岛啸,例如: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é)碼的含義,如下所示:

  • CodeJVM 字節(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)的返回指令:

  • voidreturn
  • int(boolean、byte漂彤、char雾消、short)ireturn
  • longlreturn
  • floatfreturn
  • doubledreturn
  • referenceareturn

方法調(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)載請注明出處。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末绞幌,一起剝皮案震驚了整個濱河市蕾哟,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖谭确,帶你破解...
    沈念sama閱讀 212,542評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件帘营,死亡現(xiàn)場離奇詭異,居然都是意外死亡逐哈,警方通過查閱死者的電腦和手機芬迄,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,596評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來昂秃,“玉大人禀梳,你說我怎么就攤上這事〕β妫” “怎么了算途?”我有些...
    開封第一講書人閱讀 158,021評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長蚀腿。 經(jīng)常有香客問我郊艘,道長,這世上最難降的妖魔是什么唯咬? 我笑而不...
    開封第一講書人閱讀 56,682評論 1 284
  • 正文 為了忘掉前任纱注,我火速辦了婚禮,結(jié)果婚禮上胆胰,老公的妹妹穿的比我還像新娘狞贱。我一直安慰自己,他們只是感情好蜀涨,可當我...
    茶點故事閱讀 65,792評論 6 386
  • 文/花漫 我一把揭開白布瞎嬉。 她就那樣靜靜地躺著,像睡著了一般厚柳。 火紅的嫁衣襯著肌膚如雪氧枣。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,985評論 1 291
  • 那天别垮,我揣著相機與錄音便监,去河邊找鬼。 笑死碳想,一個胖子當著我的面吹牛烧董,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播胧奔,決...
    沈念sama閱讀 39,107評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼逊移,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了龙填?” 一聲冷哼從身側(cè)響起胳泉,我...
    開封第一講書人閱讀 37,845評論 0 268
  • 序言:老撾萬榮一對情侶失蹤拐叉,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后扇商,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體凤瘦,經(jīng)...
    沈念sama閱讀 44,299評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,612評論 2 327
  • 正文 我和宋清朗相戀三年钳吟,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片窘拯。...
    茶點故事閱讀 38,747評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡红且,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出涤姊,到底是詐尸還是另有隱情暇番,我是刑警寧澤,帶...
    沈念sama閱讀 34,441評論 4 333
  • 正文 年R本政府宣布思喊,位于F島的核電站壁酬,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏恨课。R本人自食惡果不足惜舆乔,卻給世界環(huán)境...
    茶點故事閱讀 40,072評論 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望剂公。 院中可真熱鬧希俩,春花似錦、人聲如沸纲辽。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,828評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽拖吼。三九已至鳞上,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間吊档,已是汗流浹背篙议。 一陣腳步聲響...
    開封第一講書人閱讀 32,069評論 1 267
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留怠硼,地道東北人涡上。 一個月前我還...
    沈念sama閱讀 46,545評論 2 362
  • 正文 我出身青樓,卻偏偏與公主長得像拒名,于是被迫代替她去往敵國和親吩愧。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,658評論 2 350

推薦閱讀更多精彩內(nèi)容