JVM筆記:Java虛擬機(jī)的字節(jié)碼指令詳解

1.字節(jié)碼

Java能發(fā)展到現(xiàn)在般卑,其“一次編譯,多處運(yùn)行”的功能功不可沒爽雄,這里最主要的功勞就是JVM和字節(jié)碼了蝠检,在不同平臺和操作系統(tǒng)上根據(jù)JVM規(guī)范的定制JVM可以運(yùn)行相同字節(jié)碼(.Class文件),并得到相同的結(jié)果挚瘟。之所以被稱之為字節(jié)碼叹谁,是因為字節(jié)碼文件由十六進(jìn)制值組成饲梭,而JVM以兩個十六進(jìn)制值為一組,即以字節(jié)為單位進(jìn)行讀取焰檩。在Java中一般是用javac命令編譯源代碼為字節(jié)碼文件憔涉,將java文件編譯后生成.class文件交由Java虛擬機(jī)去執(zhí)行,在android上析苫,class文件被包裝成.dex文件交由DVM執(zhí)行兜叨。

通過學(xué)習(xí)Java字節(jié)碼指令可以對代碼的底層運(yùn)行結(jié)構(gòu)有所了解,能更深層次了解代碼背后的實現(xiàn)原理衩侥,例如字符串的相加的實現(xiàn)原理就是通過StringBuilderappend進(jìn)行相加国旷。用過字節(jié)碼的視角看它的執(zhí)行步驟,對Java代碼的也能有更深的了解茫死,知其然跪但,也要知其所以然。

通過學(xué)習(xí)字節(jié)碼知識還可以實現(xiàn)字節(jié)碼插樁功能峦萎,例如用ASM 屡久、AspectJ等工具對字節(jié)碼層面的代碼進(jìn)行操作,實現(xiàn)一些Java代碼不好操作的功能骨杂。

1. 字節(jié)碼的格式

下面舉個簡單的例子涂身,分析其字節(jié)碼的結(jié)構(gòu)

public class Main {
    public static void main(String[] args) {
        System.out.println("HelloWorld");
    }
}
Main.class的字節(jié)碼

上圖中純數(shù)字字母就是字節(jié)碼,右邊的是具體代碼執(zhí)行的字節(jié)碼指令搓蚪。

上面看似一堆亂碼蛤售,但是JVM對字節(jié)碼是有規(guī)范的,下面一點(diǎn)一點(diǎn)分析其代碼結(jié)構(gòu)

1.1魔數(shù)(Magic Number)

魔數(shù)唯一的作用是確定這個文件是否為一個能被虛擬機(jī)接收的Class文件妒潭。很多文件存儲標(biāo)準(zhǔn)中都使用魔數(shù)來進(jìn)行身份識別悴能,譬如gif和jpeg文件頭中都有魔數(shù)。魔數(shù)的定義可以隨意雳灾,只要這個魔數(shù)還沒有被廣泛采用同時又不容易引起混淆即可漠酿。

這里字節(jié)碼中的魔數(shù)為0xCafeBabe(咖啡寶貝),這個魔數(shù)值在Java還被稱作Oak語言的時候就已經(jīng)確定下來了谎亩,據(jù)原開發(fā)成員所說是為了尋找一些好玩的炒嘲、容易記憶的東西,選擇0xCafeBabe是因為它象征著著名咖啡品牌Peet`s Coffee中深受喜歡的Baristas咖啡匈庭,咖啡同樣也是Java的logo標(biāo)志夫凸。

1.2版本號(Version Number)

緊接著魔數(shù)的四個字節(jié)(00 00 00 33)存儲的是Class文件的版本號。前兩個是次版本號(Minor Version)阱持,轉(zhuǎn)化為十進(jìn)制為0夭拌;后兩個為主版本號(Major Version),轉(zhuǎn)化為十進(jìn)制為52,序號52對應(yīng)的主版本號為1.8鸽扁,所以編譯該文件的Java版本號為1.8.0蒜绽。高版本的JDK能向下兼容以前的版本的Class文件,但不能運(yùn)行以后版本的Class文件桶现,及時文件格式并未發(fā)生變化躲雅,虛擬機(jī)也必須拒絕執(zhí)行超過其版本號的Class文件。

1.3常量池(Constant Pool)

這部分內(nèi)容前面做了一個簡要的筆記巩那,感興趣的可以去看看吏夯。

緊接著版本號之后的是常量池入口,常量池可以理解為Class文件之中的資源倉庫即横,它是Class文件結(jié)構(gòu)中與其他項目關(guān)聯(lián)最多的數(shù)據(jù)結(jié)構(gòu)噪生,也是占用Class文件控件最大的數(shù)據(jù)項目之一,同事也是在Class文件中第一個出現(xiàn)的表類型數(shù)據(jù)項目东囚。

常量池的前兩個字節(jié)(00 22)代表的是常量池容量計數(shù)器跺嗽,與Java中語言習(xí)慣不一樣的是,這個容量計數(shù)是從1開始的页藻,這里的22轉(zhuǎn)換成十進(jìn)制后為34桨嫁,去除一個下標(biāo)計數(shù)即表示常量池中有33個常量,這一點(diǎn)從字節(jié)碼中的Constant pool也可以看到份帐,最后一個是#33 = Utf8 (Ljava/lang/String;)V

容量計數(shù)器后存儲的是常量池的數(shù)據(jù)璃吧。 常量池中存儲兩類常量:字面量與符號引用。字面量為代碼中聲明為Final的常量值(例如字符串)废境,符號引用如類和接口的全局限定名畜挨、字段的名稱和描述符、方法的名稱和描述符,當(dāng)虛擬機(jī)運(yùn)行時噩凹,需要從常量池獲得對應(yīng)的符號引用巴元,再在類創(chuàng)建時或者運(yùn)行時解析、翻譯到內(nèi)存地址中驮宴。如下圖逮刨。

常量池的每一項常量都是一個表,在JDK71.7之前共有11中結(jié)構(gòu)不同的表結(jié)構(gòu)數(shù)據(jù)堵泽,在JDK1.7之后為了更好底支持動態(tài)語言調(diào)用修己,又額外增加了三種(CONSTANT_MethodHandle_info、CONSTANT_MethodType_info和CONSTANT_InvokeDynamic_info)迎罗,總計14中箩退,表結(jié)構(gòu)如下圖

常量池數(shù)據(jù)表

上圖中tag是標(biāo)志位,用于區(qū)分常量類型佳谦,length表示這個UTF-8編碼的字符串長度是多少節(jié),它后面緊更著的長度為length字節(jié)的連續(xù)數(shù)據(jù)是一個使用UTF-8縮略編碼表示的字符串滋戳。上圖的u1钻蔑,u2啥刻,u4,u8表示比特數(shù)量咪笑,分別為1,2,4,8個byte可帽。

UTF-8縮略編碼與普通UTF-8編碼的區(qū)別是:從\u0001\u007f之間的字符(相當(dāng)于1-127的ASCII碼)的縮略編碼使用一個字節(jié)表示,從\u0080\u07ff之間的所有字符的縮略編碼用兩個字節(jié)表示窗怒,從\u0800\uffff之間的所有字符的縮略編碼就按照普通UTF-8編碼規(guī)則使用三個字節(jié)表示映跟,這么做的主要目的還是為了節(jié)省空間。

由于Class文件中方法扬虚、字段等都需要引用CONSTANT_Utf8_info型常量來描述名稱努隙,所以CONSTANT_Utf8_info型常量的最大長度就是Java中的方法、字段名的最大長度辜昵。這里的最大長度就是length的最大值荸镊,即u2類型能表達(dá)的最大值65535,所以Java程序中如果定義了超過64K英文字符的變量或發(fā)放名堪置,將會無法編譯躬存。

回到上面那個例子,00 22后面跟著的是 0A 0006 0014,第一個字節(jié)0A轉(zhuǎn)化為十進(jìn)制為10舀锨,表示的常量類型為CONSTANT_Methodref_info岭洲,這從常量表中可以看到這個類型后面會兩個u2來表示index,分別表示CONSTANT_Class_infoCONSTANT_NameAndType_info。所以0006和0014轉(zhuǎn)化為10進(jìn)制分別是6和20坎匿。這里可能不知道這些數(shù)字指代什么意思盾剩,下面展示的是編譯后的字節(jié)碼指令就可以清楚了。

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            // HelloWorld
   #4 = Methodref          #24.#25        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #26            // com/verzqli/snake/Main
   #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/verzqli/snake/Main;
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Utf8               args
  #17 = Utf8               [Ljava/lang/String;
  #18 = Utf8               SourceFile
  #19 = Utf8               Main.java
  #20 = NameAndType        #7:#8          // "<init>":()V
  #21 = Class              #28            // java/lang/System
  #22 = NameAndType        #29:#30        // out:Ljava/io/PrintStream;
  #23 = Utf8               HelloWorld
  #24 = Class              #31            // java/io/PrintStream
  #25 = NameAndType        #32:#33        // println:(Ljava/lang/String;)V
  #26 = Utf8               com/verzqli/snake/Main
  #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

從上面可以看到Constant pool中一共有33個常量碑诉,第一個常量類型為Methodref彪腔,他其實指代的是這個Main類,它是最基礎(chǔ)的Object類进栽,然后這里它有兩個索引分別指向6和20德挣,分別是Class和NameAndType類型,和上面十六進(jìn)制字節(jié)碼描述的一樣快毛。

1.4訪問標(biāo)志(Access Flags)

在常量池結(jié)束后格嗅,緊接著的兩個字節(jié)代表訪問標(biāo)志,這個標(biāo)志用于識別一些類或者接口層次的訪問信息唠帝,包括:這個Class是類還是接口屯掖;是否定義為public類型;是否定義為abstract類型襟衰,如果是類的話贴铜,是否被聲明為final等,具體的標(biāo)志位以及標(biāo)志的含義見下表。

標(biāo)志名稱 標(biāo)志值 含義
ACC_PUBLIC 0x0001 標(biāo)識是否為public類型
ACC_FINAL 0x0010 標(biāo)識是否被聲明為final绍坝,只有類可設(shè)置
ACC_SUPER 0x0020 用于兼容早期編譯器徘意,新編譯器都設(shè)置改標(biāo)志,以在使用invokespecial指令時對子類方法做特殊處理
ACC_SYNTHETIC 0x1000 標(biāo)識這個類并非由用戶代碼產(chǎn)生轩褐,而是由編譯器產(chǎn)生
ACC_INTERFACE 0x0200 標(biāo)識是否為一個接口椎咧,接口默認(rèn)同事設(shè)置ACC_ABSTRACT
ACC_ABSTRACT 0x0400 標(biāo)識是否為一個抽象類,不可與ACC_FINAL同時設(shè)置
ACC_ANNOTATION 0x2000 標(biāo)識這是否是一個注解類
ACC_ENUM 0x4000 標(biāo)識這是否是一個枚舉

ACCESS_FLAGS中一共有16個標(biāo)志位可用把介,當(dāng)前只定義了其中8個(上面顯示了比8個多勤讽,是因為ACC_PRIVATE,ACC_PROTECTED拗踢,ACC_STATIC脚牍,ACC_VOLATILE,ACC_TRANSTENT并不是修飾類的秒拔,這里寫出來是讓大家知道還有這么些標(biāo)志符)莫矗,對于沒有使用到的標(biāo)志位要求一律為0。Java不會窮舉上面所有標(biāo)志的組合砂缩,而是同|運(yùn)算來組合表示作谚,至于這些標(biāo)志位是如何表示各種狀態(tài),可以看這篇文章庵芭,講的很清楚妹懒。

我們繼續(xù)回到例子


標(biāo)志

例子中只是一個簡單的Main類,所以他的標(biāo)志是ACC_PUBLIC和ACC_SUPER,其他標(biāo)志都不存在双吆,所以它的訪問標(biāo)志為0x0001|0x0020=0x0021眨唬。

1.5 類索引、父類索引好乐、接口索引

類索引和父類索引都是一個u2類型的數(shù)據(jù)匾竿,接口索引是一組u2類型的數(shù)據(jù)的集合,Class文件中由著三項數(shù)據(jù)來確定這個類的繼承關(guān)系蔚万。這三者按順序排列在訪問標(biāo)志之后岭妖,本文例子中他們分別是:0005,0006,0000,也就是類索引為5反璃,父類索引為6昵慌,接口索引集合大小為0 ,查詢上面字節(jié)碼指令的常量池可以一一對應(yīng)(5對應(yīng)com/verzqli/snake/Main,6對應(yīng)java/lang/Object)淮蜈。

類索引確定這個類的全限定名斋攀,父類索引確定這個類的父類全限定 名,因為Java不允許多重繼承梧田,所以父類索引只有一個淳蔼,除了Object外侧蘸,所有的類都有其父類,也就是其父類索引不為0.接口索引即可用來描述這個類實現(xiàn)了哪些接口肖方,這些被實現(xiàn)的接口按implements(如果這個類本身就是一個接口闺魏,則應(yīng)當(dāng)是extends語句)后的接口順序從左到右排列在接口索引集合中。

1.6 字段表集合(Field Info)

字段表用于描述類和接口中聲明的變量,包含類級別的變量以及實例變量濒蒋。但是不包含方法內(nèi)部聲明的局部變量计寇。在Java中描述一個字段可能包含一下信息:

  • 字段的作用域(public,private,protected修飾符)
  • 是實例變量還是類變量(static修飾符)
  • 是否可變(final修飾符)
  • 并發(fā)可見 (vlolatile修飾符桑寨,是否強(qiáng)制從主內(nèi)存中讀寫)
  • 是否可悲序列化(transient修飾符)
  • 字段數(shù)據(jù)基本類型(基本類型、對象、數(shù)組)
  • 字段名稱
    上述信息中猜憎,每個修飾符都是bool值,要么有要么沒有搔课,很適合用和訪問標(biāo)志一樣的標(biāo)志位來表示胰柑。而字段名稱,字段數(shù)據(jù)類型只能引用常量池中的常量來描述爬泥。其中字段修飾符的訪問標(biāo)志和含義如下表柬讨。
標(biāo)志名稱 標(biāo)志值 含義
ACC_PUBLIC 0x0001 標(biāo)識是否為private類型
ACC_PRIVATE 0x0002 標(biāo)識是否為private類型
ACC_PROTECTED 0x0004 標(biāo)識是否為protectes類型
ACC_STATIC 0x0008 標(biāo)識是否為靜態(tài)類型
ACC_FINAL 0x0010 標(biāo)識是否被聲明為final,只有類可設(shè)置
ACC_VOLATILE 0x0040 標(biāo)識是否被聲明volatile
ACC_TRANSIENT 0x0080 標(biāo)識是否被聲明transient
ACC_SYNTHETIC 0x1000 標(biāo)識這個類并非由用戶代碼產(chǎn)生袍啡,而是由編譯器產(chǎn)生
ACC_ENUM 0x4000 標(biāo)識這是否是一個枚舉

字段表的結(jié)構(gòu)分為兩部分踩官,第一部分為兩個字節(jié),描述字段個數(shù)(fields_count)境输;第二部分是每個字段的詳細(xì)信息(fields_info)蔗牡,按順序排列分別是訪問標(biāo)志(access_flags)、字段名稱索引(name_index)嗅剖、字段的描述符索引(descriptor_index)辩越、屬性表計數(shù)器(attribute_count)和屬性信息列表(attributes)。除了最后未知的屬性信息信粮,其他都是u2的數(shù)據(jù)類型黔攒。

繼續(xù)看例子,這個例子選的有點(diǎn)尷尬,忘記往里面放一個變量蒋院,所以在類索引后面的第一個u2 數(shù)據(jù)為0000 表示字段個數(shù)為0亏钩,所以后續(xù)的數(shù)據(jù)也沒有了。只能假設(shè)一組數(shù)據(jù)來看看字段表的結(jié)構(gòu)

字節(jié)碼 00 01 00 02 00 03 00 07 00 00
描述 字段表個數(shù) 訪問標(biāo)志 字段名稱索引 字段的描述符索引 屬性個數(shù)
內(nèi)容 1 ACC_PRIVATE 3 7 0

字段表集合中不會列出從超類或者父類接口中繼承而來的字段欺旧,但有可能列出原本Java代碼之中不存在的字段姑丑,譬如在內(nèi)部類中為了保持對外部類的訪問性,會自動添加指向外部類實例的字段辞友。另外栅哀,在Java中字段是無法重載的震肮,對于字節(jié)碼來講,只有兩個字段的描述符不一致留拾,該字段才是合法的戳晌。

為了便于理解,這里對上面提到的一些名詞進(jìn)行一下解釋

  • 全限定名:本文中的Main類的全限定名為com/verzqlisnake/Main痴柔,僅僅把包名中的.替換成/即可為了使連續(xù)的多個全限定名補(bǔ)償混淆沦偎,一般在使用時最后會假如一個;,表示全限定名結(jié)束咳蔚。
  • 簡單名詞:值得是沒有類型和參數(shù)修飾的方法或字段名稱豪嚎,例如public void fun()private int a的簡單名稱就為funa
  • 方法和字段的描述符:描述符的作用是用來描述字段的數(shù)據(jù)類型或方法的參數(shù)列表(數(shù)量谈火、類型和順序)和返回值侈询。描述符包含基本數(shù)據(jù)類型和無返回值的void,主要表示為下表中形式。
描述字符 含義
描述 字段表個數(shù)
I 基本類型int
S 基本類型short
J 基本類型long,這里注意不是L糯耍,L是最后一個
F 基本類型float
D 基本類型double
B 基本類型byte
C 基本類型char
Z 基本類型boolean
V 特殊類型void
L 對象類型扔字,例如Ljava/lang/String

對于數(shù)組類型,每一位度使用一個前置的[來描述温技,例如String[]數(shù)組將被記錄為[Ljava/lang/String,String[][]數(shù)組被記錄為[[Ljava/lang/String ;int[]數(shù)組被記錄為[I革为。

用描述符來描述方法時,要先按照參數(shù)列表荒揣,后返回值的順序描述篷角,參數(shù)列表按照參數(shù)的嚴(yán)格順序放在一組小括號()之中。例如方法void fun()的描述符為()V,String.toString()的描述符為()Ljava/lang/String系任。public void multi(int i,String j,float[] c)的描述符為(ILjava/lang/String;[F)V恳蹲。

1.7 方法表集合(Field Info)

方法表的結(jié)構(gòu)和字段表的結(jié)構(gòu)幾乎完全一致,存儲的格式和描述也非常相似俩滥。方法表的結(jié)構(gòu)和字段表一樣嘉蕾,包含兩部分。第一部分為方法計數(shù)器霜旧,第二部分為每個方法的詳細(xì)信息错忱,依次包含了訪問標(biāo)志(access_flags)、方法名稱索引(name_index)挂据、方法的描述符索引(descriptor_index)以清、屬性表計數(shù)器(attribute_count)和屬性信息列表(attributes)。這些數(shù)據(jù)的含義也和字段表非常相似崎逃,僅在訪問標(biāo)志和屬性表集合的可選項中有所區(qū)別掷倔。

類型 名稱 數(shù)量
u2 access_flags 1
u2 name_index 1
u2 descriptor_index 1
u2 attribute_count 1
attribute_info attribute_info attribute_count

因為volatiletransient關(guān)鍵字不能修飾方法,所以方法標(biāo)的訪問標(biāo)志中也就沒有這兩項標(biāo)志个绍,與之對應(yīng)的勒葱,synchronized浪汪、native、strictfp凛虽、abstract可以修飾方法死遭,所以方發(fā)表的訪問標(biāo)志中增加了這幾類標(biāo)志,如下表

標(biāo)志名稱 標(biāo)志值 含義
ACC_PUBLIC 0x0001 標(biāo)識方法是否為private
ACC_PRIVATE 0x0002 標(biāo)識方法是否為private
ACC_PROTECTED 0x0004 標(biāo)識方法是否為protectes
ACC_STATIC 0x0008 標(biāo)識方法是否為靜態(tài)
ACC_FINAL 0x0010 標(biāo)識方法是否被聲明為final
ACC_SYNCHRONIZED 0x0020 標(biāo)識方法是否被聲明synchronized
ACC_BRIDGE 0x0040 標(biāo)識方法是否由編譯器產(chǎn)生的橋接方法
ACC_VARARGS 0x0080 標(biāo)識這個類是否接受不定參數(shù)
ACC_NATIVE 0x0100 標(biāo)識方法是否為native \
ACC_ABSTRACT 0x0400 標(biāo)識方法是否為abstract
ACC_STRICTFP 0x0800 標(biāo)識方法是否為strictfp
ACC_SYNTHETIC 0x1000 標(biāo)識方法是否由編譯器自動產(chǎn)生的

繼續(xù)分析本文例子凯旋,方法表數(shù)據(jù)在字段表之后的數(shù)據(jù) 0002 0001 0007 0008 0001 0009

字節(jié)碼 00 02 00 01 00 07 00 08 00 01 0009
描述 方法表個數(shù) 訪問標(biāo)志 方法名稱索引 方法的描述符索引 屬性表計數(shù)器 屬性名稱索引
內(nèi)容 1 ACC_PUBLIC 7 8 1 9

從上表可以看到方法表中有兩個方法呀潭,分別是編譯器添加的實例構(gòu)造器<init>和代碼中的main()方法。第一個方法的訪問標(biāo)志為ACC_PUBLIC,方法名稱索引為7(對應(yīng)<init>)至非,方法描述符索引為8(對應(yīng)()V)蜗侈,符合前面的常量池中的數(shù)據(jù)。

   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code

接著屬性表計數(shù)器的值為1睡蟋,表示此方法的屬性表集合有一箱屬性,屬性名稱索引為9枷颊,對應(yīng)常量池中為Code戳杀,說明此屬性是方法的字節(jié)碼描述。

方法重寫 : 如果父類方法在子類中沒有被重寫(Override),方法表集合中就不會出現(xiàn)來自父類的方法信息夭苗。但同樣的信卡,有可能會出現(xiàn)由編譯器自動添加的方法,最典型的便是類構(gòu)造器<clinit>方法和實例構(gòu)造器<init>方法题造。
方法重載:在Java中藥重載(OverLoad)一個方法傍菇,除了要與原方法遇有相同的簡單名詞外,還需要要有一個與原方法完全不同的特征簽名界赔。特征簽名是一個方法中各個參數(shù)在常量池中的字段符號引用的集合丢习,返回值并不會包含在前面中,因此無法僅僅依靠返回值不同來重載一個方法淮悼。
但是在Class文件中咐低,特征簽名的范圍更大一些,只要描述符不是完全一致的兩個方法也是可以共存的袜腥。也就是說见擦,如果兩個方法有相同的名稱和特征簽名,但返回值不同羹令,那么也是可以合法共存于同一個Class文件的鲤屡,也就是說Java語法不支持,但是Class文件支持福侈。

1.8 屬性表集合(attribute Info)

屬性表在前面的講解中已經(jīng)出現(xiàn)過數(shù)次酒来,在Class文件、字段表癌刽、方法表都可以攜帶自己的屬性表集合役首,已用于描述某些場景專有的信息
與Class文件中其他的數(shù)據(jù)項目要求嚴(yán)格的順序尝丐、長度和內(nèi)容不同,屬性表集合的限制稍微寬松了一些衡奥,不在要求各個屬性表具有嚴(yán)格的順序爹袁,只要不與已有的屬性名重復(fù),任何人實現(xiàn)的編譯器都可以想屬性表中寫入自己定義的屬性信息:Java虛擬機(jī)運(yùn)行時會忽略掉它不認(rèn)識的屬性矮固,具體的預(yù)定義屬性入下表失息。

屬性名稱 使用位置 含義
Code 方法表 Java代碼編譯成的字節(jié)碼指令
ConstantValue 字段表 final關(guān)鍵字定義的常量池
Deprecated 類,方法档址,字段表 被聲明為deprecated的方法和字段
Exceptions 方法表 方法拋出的異常
EnclosingMethod 類文件 僅當(dāng)一個類為局部類或者匿名類是才能擁有這個屬性盹兢,這個屬性用于標(biāo)識這個類所在的外圍方法
InnerClass 類文件 內(nèi)部類列表
LineNumberTable Code屬性 Java源碼的行號與字節(jié)碼指令的對應(yīng)關(guān)系
LocalVariableTable Code屬性 方法的局部變量描述
StackMapTable Code屬性 JDK1.6中新增的屬性,供新的類型檢查檢驗器檢查和處理目標(biāo)方法的局部變量和操作數(shù)有所需要的類是否匹配
Signature 類守伸,方法表绎秒,字段表 JDK1.5中新增的屬性,用于支持泛型情況下的方法簽名尼摹。任何類见芹,接口,初始化方法或成員的泛型前面如果包含了類型變量(Type Variables)或參數(shù)化類型(Parameterized Type)蠢涝,則signature屬性會為它記錄泛型前面信息玄呛,由于Java的泛型采用擦除法實現(xiàn),在為了便面類型信息被擦除后導(dǎo)致簽名混亂和二,需要這個屬性記錄泛型中的相關(guān)信息徘铝。
SourceFile 類文件 記錄源文件名稱
SourceDebugExtension 類文件 JDK1.6中新增的屬性,用于存儲額外的調(diào)試信息
Synthetic 類惯吕,方法表惕它,字段表 標(biāo)志方法或字段為編譯器自動生成的
LocalVariableTypeTable JDK1.5中新增的屬性,使用特征簽名代替描述符混埠,是為了引入泛型語法之后能描述泛型參數(shù)化類型而添加
RuntimeVisibleAnnotations 類怠缸,方法表,字段表 JDK1.5中新增的屬性钳宪,為動態(tài)注解提供支持 揭北,用于指明那些注解是運(yùn)行時(運(yùn)行時就是進(jìn)行反射調(diào)用)可見的
RuntimeInvisibleAnnotations 表,方法表吏颖,字段表 JDK1.5中新增的屬性搔体,和上面剛好相反,用于指明哪些注解是運(yùn)行時不可見的
RuntimeVisibleParameterAnnotation 方法表 JDK1.5中新增的屬性半醉,作用與RuntimeVisibleAnnotations屬性類似疚俱,只不過作用對象為方法
RuntimeInvisibleParameterAnnotation 方法表 JDK1.5中新增的屬性,作用與RuntimeInvisibleAnnotations屬性類似缩多,作用對象哪個為方法參數(shù)
AnnotationDefault 方法表 JDK1.5中新增的屬性呆奕,用于記錄注解類元素的默認(rèn)值
BootstrapMethods 類文件 JDK1.7中新增的屬性养晋,用于保存invokeddynamic指令引用的引導(dǎo)方式限定符

對于每個屬性,它的名稱需要從常量池中應(yīng)用一個CONSTANT_Utf8_info類型的常量來標(biāo)書梁钾,而屬性值的結(jié)構(gòu)則是完全子墩醫(yī)德绳泉,只需要通過一個u4的長度屬性去說明屬性值做占用的位數(shù)即可,其符合規(guī)則的結(jié)構(gòu)如下圖姆泻。

類型 名稱 數(shù)量
u2 attribute_name_index 1
u4 attribute_length 1
u1 infoattribute_length

因為屬性表中的屬性包含二十多種零酪,下面只對幾個屬性做一個簡要描述。

  • 1.8.1 Code 屬性

Java程序方法體中的代碼經(jīng)過Javac編譯器處理后拇勃,最終變?yōu)樽止?jié)碼指令存儲在Code屬性內(nèi)四苇,Code屬性出現(xiàn)在方法表的屬性集合之中,但并未所有的方法表都必須存在這個屬性:接口或者抽象類中的方法就不存在Code屬性方咆。如果方法表有Code屬性月腋,那么它的結(jié)構(gòu)將如下表所示。

類型 名稱 數(shù)量
u2 attribute_name_index 1
u4 attribute_length 1
u2 max_stack 1
u2 max_locals 1
u4 code_length 1
u1 code code_length
u2 exception_table_length 1
exception_info exception_table exception_length
u2 attributes_count 1
attribute_info attributes attributes_count

attribute_name_index:一項指向CONSTANT_Utf8_info型常量的索引瓣赂,常量值固定為“Code”罗售,他代表了該屬性的名稱。
attribute_length: 屬性值得長度钩述,由于屬性名稱索引和長度一共為6字節(jié),所以屬性值長度固定為整個屬性表長度減去6個字節(jié)穆碎。
max_stack:操作數(shù)棧深度的最大值牙勘,裝虛擬機(jī)運(yùn)行的時候需要根據(jù)這個值來分配棧幀中的操作棧深度,沒有定義好回歸的遞歸發(fā)生的棧溢出就是超過了這個值所禀。
max_locals:局部變量表所需的存儲空間方面。這里的單位是Slot,Slot是虛擬機(jī)為局部變量表分配內(nèi)存所使用得最小單位。對于byte色徘、char恭金、float、int褂策、short横腿、boolean、returnAddress這些長度不超過32位的整型數(shù)據(jù)斤寂,每個局部變量占用一個Slot耿焊。像double和float兩種64位的數(shù)據(jù)類型需要兩個Slot來存放位置。方法參數(shù)(實例方法中隱藏的this)遍搞、顯示異常處理器的參數(shù)(就是try-catch語句中catch鎖定義的異常)罗侯、放大提中定義的局部變量都需要使用局部變量表來存放。因為Slot可以重用溪猿,所以這個最大值并不是所有的Slot之和钩杰,當(dāng)代碼執(zhí)行超過一個局部變量的作用于時纫塌,這個局部變量所占用的Slot可以被其他局部變量使用,所以該值主要根據(jù)變量的所用域來計算大小讲弄。
code_length:字節(jié)碼長度措左。雖然是u4長度,但是虛擬機(jī)規(guī)定了一個方法中的字節(jié)碼指令條數(shù)不超過u2(65535)條垂睬,超過的話編譯器會拒絕編譯媳荒。
code:存儲編譯后生成的字節(jié)碼指令。每個字節(jié)碼指令是一個u1類型的單字節(jié)驹饺。當(dāng)虛擬機(jī)督導(dǎo)一個字節(jié)碼時钳枕,可以找到這個字節(jié)碼代碼的指令,并可以知道這個指令后面是否需要跟隨參數(shù)以及參數(shù)的意思赏壹。一個u1數(shù)據(jù)的取值范圍為0x00~0xff鱼炒,也就是一共可以表達(dá)256條指令,目前蝌借,Java虛擬機(jī)以及定義了其中200多條編碼值對應(yīng)的指令含義昔瞧,具體指令可以看虛擬機(jī)字節(jié)碼指令表
因為異常表對于Code屬性不是必須存在的菩佑,后面幾個類型也沒有太大的重要性自晰,這里就暫時略過。

  • 1.8.2 Exceptions屬性

    這里的Exceptions屬性是在方法表中與Code屬性評級的一項屬性稍坯,Exceptions屬性的作用是列舉出方法中可能拋出的受查異常(Checked Exceptions)酬荞,也就是方法描述時在throws關(guān)鍵詞后面列舉的異常,其結(jié)構(gòu)如下圖瞧哟。
類型 名稱 數(shù)量
u2 attribute_name_index 1
u2 attribute_lrngth 1
u2 number_of_exception 1
u2 exception_index_table number_of_exceptions

number_of_exception:表示方法可能拋出此項值數(shù)值的受查異常混巧,每一種受查異常exception_index_table表示。
exception_index_table:表示一個指向常量池中CONSTANT_Class_indo型常量的索引勤揩,所以咧党,代表了該種受查異常的類型。

  • 1.8.3 SourceFile屬性

SourceFile屬性用于記錄生成這個Class文件的源碼文件名稱陨亡“猓可以使用Javac的-g:none和-g:source選項來關(guān)閉或者生成這項信息。對于大多數(shù)類來說负蠕,類名和文件名是一致的聪舒,但是例如內(nèi)部類等一些特殊情況就會不一樣。如果不生成這個屬性虐急,當(dāng)拋出異常時箱残,堆棧中將不會顯示出錯代碼所屬的文件名,其結(jié)構(gòu)入下表:

類型 名稱 數(shù)量
u2 attribute_name_index 1
u4 attribute_length 1
u2 sourcefile_index 1

sourcefile_index:指向常量池中的CONSTANT_Utf8_indo型常量,常量值是源碼文件的文件名。

  • 1.8.3 InnerClass屬性

InnerClass屬性用于記錄內(nèi)部類與宿主之間的關(guān)聯(lián)被辑,如果一個類中定義了內(nèi)部類燎悍,那編譯器將會為他以及它所包含的內(nèi)部類生成InnerClasses屬性,其表結(jié)構(gòu)如下圖:

類型 名稱 數(shù)量
u2 attribute_name_index 1
u4 attribute_length 1
u2 number_of_classes 1
inner_classes_info inner_classes number_of_classes

number_of_classes:表示內(nèi)部類信息的個數(shù)盼理。每一個內(nèi)部類的信息都由一inner_classes_info表進(jìn)行描述谈山,改表結(jié)果如下:

類型 名稱 數(shù)量
u2 inner_class_info_index 1
u2 outer_class_info_index 1
u2 inner_name_index 1
u2 inner_class_access_flags 1

inner_class_info_index:指向常量池中的CONSTANT_Class_indo型常量的索引,表示內(nèi)部類的符號引用宏怔。
outer_class_info_index:指向常量池中的CONSTANT_Class_indo型常量的索引奏路,表示宿主類的符號引用。
inner_class_access_flags:內(nèi)部類的訪問標(biāo)志臊诊,類似于類的access_flags鸽粉。

  • 1.8.4 ConstantValue屬性

    ConstantValue屬性的作用是通知虛擬機(jī)自動為靜態(tài)變量賦值。只有被static關(guān)鍵字修飾的變量(類變量)才可以使用這項屬性抓艳,例如int a=1static int a=1触机,虛擬機(jī)對這兩種變量的賦值方式和時刻都有所不同。對于前者的賦值是在實例構(gòu)造器<init>方法中進(jìn)行的玷或,換而言之就是一個類的構(gòu)造的方法沒有被執(zhí)行前儡首,該類的成員變量是還沒賦值的;而對于后者偏友,則有兩種方式可以選擇:在類構(gòu)造器<clinit>方法中或者使用ConstantValue屬性蔬胯。目前Javac編譯器的選擇是如果同時使用finalstatic來修飾一個變量,并且這個變量的數(shù)據(jù)類型是基本類型或者字符串類型時位他,就生成ConstantValue屬性來初始化笔宿,如果這個變量沒有被final修飾,或者并非基本類型變量或字符串棱诱,則會選擇在<clinit>方法中進(jìn)行初始化

    <clinit>:類構(gòu)造器涝动。在jvm第一次加載class文件時調(diào)用迈勋,因為是類級別的,所以只加載一次醋粟,是編譯器自動收集類中所有類變量(static修飾的變量)和靜態(tài)語句塊(static{})靡菇,中的語句合并產(chǎn)生的,編譯器收集的順序米愿,是由程序員在寫在源文件中的代碼的順序決定的厦凤。
    <init>:實例構(gòu)造器方法,在實例創(chuàng)建出來的時候調(diào)用育苟,包括調(diào)用new操作符较鼓;調(diào)用Class或java.lang.reflect.Constructor對象的newInstance()方法;調(diào)用任何現(xiàn)有對象的clone()方法;通過java.io.ObjectInputStream類的getObject()方法反序列化博烂。

<clinit>方法和類的構(gòu)造函數(shù)不同香椎,它不需要顯示調(diào)用父類的構(gòu)造方法,虛擬機(jī)會保證子類的<clinit>方法執(zhí)行之前禽篱,父類的此方法已經(jīng)執(zhí)行完畢畜伐,因此虛擬機(jī)中第一個被執(zhí)行的<clinit>方法的類肯定是java.lang.Object。言而言之就是先需要<clinit>完成類級別的變量和代碼塊的加載躺率,再進(jìn)行對象級別的加載信息玛界,所以經(jīng)常看的面試題子類和父類哪個語句先被執(zhí)行就是這些決定的悼吱。

public class Main {
     static final int a=1;
}
字節(jié)碼:
  static final int a;
    descriptor: I
    flags: ACC_STATIC, ACC_FINAL
    ConstantValue: int 1
未添加final
public class Main {
      static int a=1;
}
字節(jié)碼:
  public com.verzqli.snake.Main();
    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 12: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/verzqli/snake/Main;
  //可以看到  這里的初始化放在了Main的類構(gòu)造器中
  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: iconst_1
         1: putstatic     #2                  // Field a:I
         4: return
      LineNumberTable:
        line 13: 0
}

public class Main {
        int a=1;
}
字節(jié)碼:
  //可以看到  這里的初始化放在了Main的實例構(gòu)造器中
  public com.verzqli.snake.Main();
    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: iconst_1
         6: putfield      #2                  // Field a:I
         9: return
}

2. 字節(jié)碼指令

字節(jié)碼指令是一個字節(jié)長度的慎框,代表著某種特點(diǎn)操作含義的數(shù)字,總數(shù)不超過256條(全部字節(jié)碼指令匯編)舆绎,鲤脏。對于大部分與數(shù)據(jù)類型相關(guān)的字節(jié)碼指令,它們的操作碼助記符中都有特殊字符來表明專門為那種數(shù)據(jù)類型服務(wù)吕朵,如下表:

描述字符 含義
i 基本類型int
s 基本類型short
l 基本類型long,這里注意不是L猎醇,L是最后一個
f 基本類型float
d 基本類型double
b 基本類型byte
c 基本類型char
b 基本類型boolean
a 對象類型引用reference

這里有一個注意的點(diǎn),這對于不是整數(shù)類型的byte努溃、char硫嘶、short、boolean梧税。編譯器會在編譯器或運(yùn)行期將byte和short類型的數(shù)據(jù)帶符號擴(kuò)展(Sign-extend)為相應(yīng)的int類型數(shù)據(jù)沦疾,將boolean和char類型數(shù)據(jù)零位擴(kuò)展(Zero-extend)為相應(yīng)的int數(shù)據(jù)。同樣在處理上訴類型的數(shù)組數(shù)據(jù)是第队,也會轉(zhuǎn)換為使用int類型的字節(jié)碼指令來處理哮塞。

2.1 加載和存儲指令。

加載和存儲指令用于將數(shù)據(jù)在棧幀中的局部變量表和操作數(shù)棧之間來回傳輸凳谦。

<類型>load_<下標(biāo)>:將一個局部變量加載到操作數(shù)棧忆畅。例如iload_1,將一個int類型局部變量(下標(biāo)為1尸执,0一般為this)從局部變量表加載到操作棧家凯,其他的也都類似,例如:dload_2,fload_3如失。
<類型>store_<下標(biāo)>:將一個數(shù)值從操作數(shù)棧棧頂存儲到局部變量表绊诲。例如istore_3,將一個int類型的數(shù)值從操作數(shù)棧棧頂存儲到局部變量3中褪贵,后綴為3掂之,證明局部變量表中已經(jīng)存在了兩個值。
<類型>const_<具體的值>:將一個常量加載到操作數(shù)棧。例如iconst_3,將常量3加載到操作數(shù)棧板惑。
wide擴(kuò)展:當(dāng)上述的下標(biāo)志超過3時橄镜,就不用下劃線的方式了,而是使用istore 6冯乘,load的寫法也是一樣洽胶。
bipush、sipush裆馒、ldc :當(dāng)上述的const指令后面的值變得很大時姊氓,該指令也會改變。

  • 當(dāng) int 取值 -1~5 時喷好,JVM 采用 iconst 指令將常量壓入棧中翔横。
  • 當(dāng) int 取值 -128~127 時,JVM 采用 bipush 指令將常量壓入棧中梗搅。
  • 當(dāng) int 取值 -32768~32767 時禾唁,JVM 采用 sipush 指令將常量壓入棧中。
  • 當(dāng) int 取值 -2147483648~2147483647 時无切,JVM 采用 ldc 指令將常量壓入棧中荡短。

看例子:

 public void save() {
       int a = 1;
       int b = 6;
       int c = 128;
       int d = 32768 ;
       float f = 2.0f;
   }
字節(jié)碼:
   Code:
     stack=1, locals=6, args_size=1
        0: iconst_1               //將常量1入棧,
        1: istore_1               //將棧頂?shù)?存入局部變量表,下標(biāo)為1哆键,因為0存儲了整個類的this
        2: bipush        6       //將常量6入棧掘托,同時也是以wide擴(kuò)展的形式
        4: istore_2               //將棧頂?shù)?存入局部變量表,下標(biāo)為2
        5: sipush        128    //將常量128入棧,
        8: istore_3               //將棧頂?shù)?28存入局部變量表,下標(biāo)為3 籍嘹,后面一樣的意思
        9: ldc           #2        // int 32768
       11: istore        4
       13: fconst_2
       14: fstore        5
       16: return

2.2 運(yùn)算指令闪盔。

運(yùn)算主要分為兩種:對征信數(shù)據(jù)進(jìn)行運(yùn)算的指令和對浮點(diǎn)型數(shù)據(jù)運(yùn)算的指令,和前面說的一樣辱士,對于byte泪掀、char、short颂碘、和 boolean類型的算數(shù)質(zhì)量都使用int類型的指令替代异赫。整數(shù)和浮點(diǎn)數(shù)的運(yùn)算指令在移除和被領(lǐng)出的時候也有各自不同的表現(xiàn)行為。具體的指令也是在運(yùn)算指令前加上對應(yīng)的類型即可凭涂,例如加法指令:iadd,ladd,fadd,dadd。

  • 加法指令:(i,l,f,d)add
  • 減法指令:(i,l,f,d)sub
  • 乘法法指令:(i,l,f,d)mul
  • 除法指令:(i,l,f,d)div
  • 求余指令:(i,l,f,d)rem
  • 取反指令:(i,l,f,d)neg
  • 位移指令: ishl贴妻、ishr切油、iushr、lshl名惩、lshr澎胡、lushr
  • 按位或指令:ior、lor
  • 按位與指令:iand、land
  • 按位異或指令: ixor攻谁、lxor
  • 局部變量自增: iinc(例如for循環(huán)中i++)
  • 比較指令: dcmpg稚伍、dcmpl、fcmpg戚宦、fcmpl个曙、lcmp

上面的指令沒必要強(qiáng)記,需要的時候查找一下即可受楼,看多了也自然就熟悉了垦搬。至于浮點(diǎn)數(shù)運(yùn)算的精度損失之類的這里就不多做贅述了。

2.3 類型轉(zhuǎn)換指令艳汽。

類型轉(zhuǎn)換指令可以將兩種不同的數(shù)值類型進(jìn)行相互轉(zhuǎn)換猴贰,這些轉(zhuǎn)換一般用于實現(xiàn)用戶代碼中的顯示類型轉(zhuǎn)換操作。

Java虛擬機(jī)直接支持寬化數(shù)據(jù)類型轉(zhuǎn)換(小范圍數(shù)據(jù)轉(zhuǎn)換為大數(shù)據(jù)類型),不需要顯示的轉(zhuǎn)換指令河狐,例如int轉(zhuǎn)換long米绕,float和double。舉例:int a=10;long b =a

Java虛擬機(jī)轉(zhuǎn)換窄化數(shù)據(jù)類型轉(zhuǎn)換時馋艺,必須顯示的調(diào)用轉(zhuǎn)化指令栅干。舉例:long b=10;int a = (long)b

類型轉(zhuǎn)換的字節(jié)碼指令其實就比較簡單了丈钙,<前類型>2<后類型>非驮,例如i2l,l2i,i2f,i2d。當(dāng)然這里舉的都是基本數(shù)據(jù)類型雏赦,如果是對象劫笙,當(dāng)類似寬化數(shù)據(jù)類型時就直接使用,當(dāng)類似窄化數(shù)據(jù)類型時星岗,需要checkcast指令填大。

public class Main {
    public static void main(String[] args) {
        int a = 1;
        long b = a;
        Parent Parent = new Parent();
        Son son = (Son) Parent;
    }
}
字節(jié)碼:
  Code:
      stack=2, locals=6, args_size=1
         0: iconst_1
         1: istore_1
         2: iload_1
         3: i2l
         4: lstore_2
         5: new           #2                  // class com/verzqli/snake/Parent
         8: dup
         9: invokespecial #3                  // Method com/verzqli/snake/Parent."<init>":()V
        12: astore        4
        14: aload         4
        16: checkcast     #4                  // class com/verzqli/snake/Son
        19: astore        5
        21: return

注意上面這個轉(zhuǎn)換時錯誤的,父類是不能轉(zhuǎn)化為子類的俏橘,編譯期正常允华,但是運(yùn)行是會報錯的,這就是checkcast指令的原因寥掐。

2.4 對象創(chuàng)建和訪問指令

雖然累實例和數(shù)組都是對象靴寂,但Java蘇尼基對類實例和數(shù)組的創(chuàng)建與操作使用了不同的字節(jié)碼指令屁药。對象創(chuàng)建后弥激,就可以通過對象訪問指令獲取對象實例或者數(shù)組實例中的字段或者數(shù)組元素,這些指令如下侦副。

  • new:創(chuàng)建類實例的指令
  • newarray污它、anewarray剖踊、multianewarray:創(chuàng)建數(shù)組的指令
  • getfield庶弃、putfield、getstatic德澈、putstatic:訪問類字段(static字段歇攻,被稱為類變量)和實例字段(非static字段,)梆造。
  • (b缴守、c、s澳窑、i斧散、l、f摊聋、d鸡捐、a)aload:很明顯,就是基礎(chǔ)數(shù)據(jù)類型加上aload麻裁,將一個數(shù)組元素加載到操作數(shù)棧箍镜。
  • (b、c煎源、s色迂、i、l手销、f歇僧、d、a)astore:同上面一樣的原理锋拖,將操作數(shù)棧棧頂?shù)闹荡鎯Φ綌?shù)組元素中诈悍。
  • arraylength:取數(shù)組長度
  • instanceof、checkcast:檢查類實例類型的指令兽埃。

2.4 操作數(shù)棧管理指令

如同操作一個普通數(shù)據(jù)結(jié)構(gòu)中的堆棧那樣侥钳,Java虛擬機(jī)提供了一些直接操作操作數(shù)棧的指令。

  • pop柄错、pop2:將操作數(shù)棧棧頂?shù)囊粋€或兩個元素出棧舷夺。
  • dup、dup2售貌、dup_x1给猾、dup2_x1、dup_x2颂跨、dup2_x2:服戰(zhàn)棧頂一個或兩個數(shù)值并將期值復(fù)制一份或兩份后重新壓入棧頂敢伸。
  • swap:將棧頂兩個數(shù)互換。

2.5 方法調(diào)用和返回指令毫捣。

方法調(diào)用的指令只要包含下面這5條

  • invokespecial:用于調(diào)用一些需要特殊處理的實例方法详拙,包括實例初始化方法、私有方法和父類方法蔓同。
  • invokestatic:用于調(diào)用static方法饶辙。
  • invokeinterface:用于調(diào)用接口方法,他會在運(yùn)行時搜索一個實現(xiàn)了這個接口方法的對象斑粱,找出合適的方法進(jìn)行調(diào)用弃揽。
  • invokevirtual:用于調(diào)用對象的實例方法,根據(jù)對象的實際類型進(jìn)行分派则北。
  • invokedynamic:用于在運(yùn)行時動態(tài)解析出調(diào)用點(diǎn)限定符所引用的方法矿微,并執(zhí)行該方法。前面4條指令的分派邏輯都固話在Java虛擬機(jī)內(nèi)部尚揣,而此條指令的分派邏輯是由用戶設(shè)定的引導(dǎo)方法決定的涌矢。
  • (i,l,f,d, 空)return:根據(jù)前面的類型來確定返回的數(shù)據(jù)類型,為空時表示void

2.5 異常處理指令快骗。

在Java程序中顯示拋出異常的操作(throw語句)都由athrow指令來實現(xiàn)娜庇。但是處理異常(catch語句)不是由字節(jié)碼指令來實現(xiàn)的,而是采用異常表來完成的方篮,如下例子名秀。

public class Main {
   public static void main(String[] args) throws Exception{
       try {
           Main a=new Main();
       }catch (Exception e){
           e.printStackTrace();
       }
   }
}
字節(jié)碼:
public static void main(java.lang.String[]) throws java.lang.Exception;
   descriptor: ([Ljava/lang/String;)V
   flags: ACC_PUBLIC, ACC_STATIC
   Code:
     stack=2, locals=2, args_size=1
        0: new           #2                  // class com/verzqli/snake/Main
        3: dup
        4: invokespecial #3                  // Method "<init>":()V
        7: astore_1
        8: goto          16
       11: astore_1
       12: aload_1
       13: invokevirtual #5                  // Method java/lang/Exception.printStackTrace:()V
       16: return

2.6 同步指令

Java虛擬機(jī)可以支持方法級的同步和方法內(nèi)部一段指令序列的同步,這兩種同步結(jié)構(gòu)都是使用Monitor
實現(xiàn)的藕溅。
正常情況下Java運(yùn)行是同步的匕得,無需使用字節(jié)碼控制。虛擬機(jī)可以從方法常量池的方法表結(jié)構(gòu)中的ACC_SYNCHRONIZE訪問標(biāo)志得知一個方法是否聲明為同步方法巾表。當(dāng)方法調(diào)用時汁掠,調(diào)用指令將會檢查方法的ACC_SYNCHRONIZE訪問表示是否被設(shè)置,如果設(shè)置了攒发,執(zhí)行線程就要求先持有Monitor,然后才能執(zhí)行方法调塌,最后當(dāng)方法完成時釋放Monitor。在方法執(zhí)行期間惠猿,執(zhí)行線程持有了Monitor羔砾,其他任何一個線程都無法在獲取到同一個Monitor。如果一個同步方法執(zhí)行期間拋出了異常偶妖,并且在方法內(nèi)部無法處理次異常姜凄,那么這個同步方法所持有的Monitor將在異常拋出到同步方法之外時自動釋放。
同步一段指令集序列通常是由synchronized語句塊來表示的趾访,Java虛擬機(jī)指令集中有monitorentermonitorexit兩條指令來支持synchronized關(guān)鍵字态秧。如下例子

public class Main {
    public void main() {
        synchronized (Main.class) {
            System.out.println("synchronized");
        }
        function();
    }

    private void function() {
        System.out.printf("function");
    }
}

字節(jié)碼:
 Code:
      stack=3, locals=3, args_size=1
         0: ldc           #2                  // class com/verzqli/snake/Main  將Main引用入棧
         2: dup                                // 復(fù)制棧頂引用 Main
         3: astore_1                        // 將棧頂應(yīng)用存入到局部變量astore1中
         4: monitorenter                  // 將棧頂元素(Main)作為鎖,開始同步
        5: getstatic     #3                 // Field java/lang/System.out:Ljava/io/PrintStream;
         8: ldc           #4                   // String synchronized ldc指令在運(yùn)行時創(chuàng)建這個字符串
        10: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        13: aload_1                         // 將局部變量表的astore1入棧(Main)
        14: monitorexit                    //退出同步
        15: goto          23                  // 方法正常結(jié)束扼鞋,跳轉(zhuǎn)到23
        18: astore_2                        //這里是出現(xiàn)異常走的路徑申鱼,將棧頂元素存入局部變量表
        19: aload_1                          // 將局部變量表的astore1入棧(Main)
        20: monitorexit                      //退出同步
        21: aload_2                          //將前面存入局部變量的異常astore2入棧
        22: athrow                            //  把異常對象長線拋出給main方法的調(diào)用者
        23: aload_0                          // 將類this入棧愤诱,以便下面調(diào)用類的方法
        24: invokespecial #6                  // Method function:()V
        27: return

編譯器必須確保無論方法通過何種方式完成,方法中調(diào)用過的每條monitorenter指令都必須執(zhí)行其對應(yīng)的monitorexit指令捐友,無論這個方法是正常結(jié)束還是異常結(jié)束淫半。

3 實例

前面說了一堆,空看理論既枯燥又難懂匣砖,理論就圖一樂科吭,真懂還得看例子。

例一:

相信面試過的人基本地看過這個面試題猴鲫,然后還扯過值傳遞還是引用傳遞這個問題对人,下面從字節(jié)碼的角度來分析這個問題。

public class Main {
    String str="newStr";
    String[] array={"newArray1","newArray2"};

  public static void main(String[] args) {
      Main main=new Main();
      main.change(main.str, main.array);
      System.out.println(main.str);
      System.out.println(Arrays.toString(main.array));
  }

  private   void change(String str, String[] array) {
      str="newStrEdit";
      array[0]="newArray1Edit";
  }
}
輸出結(jié)果:
newStr
[newArray1Edit, newArray2]
字節(jié)碼:

private void change(java.lang.String, java.lang.String[]);
  descriptor: (Ljava/lang/String;[Ljava/lang/String;)V
  flags: ACC_PRIVATE
  Code:
    stack=3, locals=3, args_size=3
       0: ldc           #14                 // String newStrEdit
       2: astore_1
       3: aload_2
       4: iconst_0
       5: ldc           #15                 // String newArray1Edit
       7: aastore
       8: return

}

這里main方法的字節(jié)碼內(nèi)容可以忽略拂共,主要看這個change方法牺弄,下面用圖來表示。


圖一

這是剛進(jìn)入這個方法的情況宜狐,這時候還沒有執(zhí)行方法的內(nèi)容猖闪,局部變量表存了三個值,第一個是this指代這個類肌厨,在普通方法內(nèi)之所以可以拿到外部的全局變量就是因為方法內(nèi)部的局部變量表的第一個就是類的this培慌,當(dāng)獲取外部變量時,先將這個this入棧aload_0柑爸,然后就可以獲取到這個類所有的成員變量(也就是外部全局變量)了吵护。
因為這個方法傳進(jìn)來了兩個值,這里局部變量表存儲的是這兩個對象的引用表鳍,也就是在堆上的內(nèi)存地址馅而。

圖二

上面執(zhí)行了str = "newStrEdit";這條語句,先ldc指令創(chuàng)建了newStrEdit(0xaaa)字符串入棧譬圣,然后astore_1指令將棧頂?shù)闹当4嬖倬植孔兞?中瓮恭,覆蓋了原來的地址,所以這里對局部變量表的修改完全沒有影響外面的值厘熟。
圖三

上面執(zhí)行array[0] = "newArrar1Edit";這條語句屯蹦,將array的地址入棧,再將要修改的數(shù)組下標(biāo)0入棧绳姨,最后創(chuàng)建newArray1Edit字符串入棧登澜。最后調(diào)用aastore指令將棧頂?shù)囊眯蛿?shù)值(newArray1Edit)、數(shù)組下標(biāo)(0)飘庄、數(shù)組引用(0xfff)依次出棧脑蠕,最后將數(shù)值存入對應(yīng)的數(shù)組元素中,這里可以看到對這個數(shù)組的操作一直都是這個0xfff地址跪削,這個地址和外面的array指向的是同一個數(shù)組對象谴仙,所以這里修改了迂求,外界的那個array也就同樣修改了內(nèi)容。

例二:

看過前面那個例子應(yīng)該對局部變量表是什么有所了解晃跺,下面這個例子就不繪制上面那個圖了锁摔。這個例子也是一個常見的面試題,判斷try-catch-finally-return的執(zhí)行順序哼审。

finally是一個最終都會執(zhí)行的代碼塊,finally里面的return會覆蓋try和catch里面的return,同時在finally里面修改局部變量不會影響try和catch里面的局部變量值孕豹,除非trycatch里面返回的值是一個引用類型涩盾。

 public static void main(String[] args) {
        Main a=new Main();
        System.out.println("args = [" + a.testFinally() + "]");;
    }

    public   int testFinally(){
        int i=0;
        try{
            i=2;
            return i;
        }catch(Exception e){
            i=4;
            return i;
        }finally{
            i=6;
        }
字節(jié)碼:
public int testFinally();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=5, args_size=1
         0: iconst_0                // 常量0入棧
         1: istore_1                // 賦值給內(nèi)存變量1(i) i=0
         2: iconst_2                // 常量2入棧
         3: istore_1                // 賦值給內(nèi)存變量1(i) i=2
         4: iload_1                 // 內(nèi)存變量1(i)入棧
         5: istore_2                // 將數(shù)據(jù)存儲在內(nèi)存變量2 這里原因下面說明
         6: bipush        6         // 常量6入棧
         8: istore_1                // 保存再內(nèi)存變量1
         9: iload_2                 // 加載內(nèi)存變量2
        10: ireturn                 // 返回上一句加載的內(nèi)存變量2(i) i=2
        11: astore_2                // 看最下面的異常表,如果2-6發(fā)生異常励背,就從11開始春霍,下面就是發(fā)生異常后進(jìn)入catch的內(nèi)容
        12: iconst_4                // 常量4入棧
        13: istore_1                // 保存在局部變量1
        14: iload_1                 // 加載局部變量1
        15: istore_3                // 將局部變量1內(nèi)容保存到局部變量3,原因和上面5一樣
        16: bipush        6         // 常量6入棧 (進(jìn)入了catch最后也會執(zhí)行finally,所以這里會重新再執(zhí)行一遍finally)
        18: istore_1                // 保存在局部變量1
        19: iload_3                 // 加載局部變量3并返回
        20: ireturn                 //上面類似的語句叶眉,不過是catch-finally的路徑
        21: astore        4         // finally 生成的冗余代碼址儒,這里發(fā)生的異常會拋出去
        23: bipush        6
        25: istore_1
        26: aload         4
        28: athrow
      Exception table:
         from    to  target type
             2     6    11   Class java/lang/Exception  //如果2-6發(fā)生指定的Exception異常(try),就從11開始
             2     6    21   any                        //如果2-6發(fā)生任何其他異常(finally)衅疙,就從21開始
            11    16    21   any                        //如果11-16發(fā)生任何其他異常(catch)莲趣,就從21開始
            21    23    21   any                        //其實這里有點(diǎn)不太能理解為什么會循環(huán),如果有知道的大佬可以解答一下

在Java1.4之后 Javac編譯器 已經(jīng)不再為 finally 語句生成 jsr 和 ret 指令了饱溢, 當(dāng)異常處理存在finally語句塊時喧伞,編譯器會自動在每一段可能的分支路徑之后都將finally語句塊的內(nèi)容冗余生成一遍來實現(xiàn)finally語義。(21~28)绩郎。但我們Java代碼中潘鲫,finally語句塊是在最后的,編譯器在生成字節(jié)碼時候肋杖,其實將finally語句塊的執(zhí)行指令移到了ireturn指令之前溉仑,指令重排序了。所以状植,從字節(jié)碼層面浊竟,我們解釋了,為什么finally語句總會執(zhí)行津畸!

如果try中有return,會在return之前執(zhí)行finally中的代碼逐沙,但是會保存一個副本變量(第五和第十五行)。finally修改原來的變量洼畅,但tryreturn返回的是副本變量吩案,所以如果是賦值操作,即使執(zhí)行了finally中的代碼帝簇,變量也不一定會改變徘郭,需要看變量是基本類型還是引用類型靠益。
但是如果在finally里面添加一個return,那么第9行和第19行加載的就是finally塊里修改的值(iload_1),再在最后添加一個iload_1ireturn,感興趣的可以自己去看一下字節(jié)碼残揉。

例三:

還是上面那個類似的例子胧后,這里做一下改變

 public static void main(String[] args) {
        Main a = new Main();
        System.out.println("args = [" + a.testFinally1() + "]");
        System.out.println("args = [" + a.testFinally2() + "]");
    }

    public StringBuilder testFinally1() {
        StringBuilder a = new StringBuilder("start");
        try {
            a.append("try");
            return a;
        } catch (Exception e) {
            a.append("catch");
            return a;
        } finally {
            a.append("finally");
        }
    }

    public String testFinally2() {
        StringBuilder a = new StringBuilder("start");
        try {
            a.append("try");
            return a.toString();
        } catch (Exception e) {
            a.append("catch");
            return a.toString();
        } finally {
            a.append("finally");
        }
    }

輸出結(jié)果:
args = [starttryfinally]
args = [starttry]

這里就不列舉全局字節(jié)碼了,兩個方法有點(diǎn)多抱环,大家可以自己嘗試去看一下壳快。這里做一下說明為什么第一個返回的結(jié)果沒有finally
首先這個方法的局部變量表1里面存儲了一個StringBuilder地址镇草,執(zhí)行到try~finally這一部分沒什么區(qū)別眶痰,都是復(fù)制了一份變量1的地址到變量3,注意梯啤,這兩個地址是一樣的竖伯。
那為什么第二個返回方法少了finally呢,那是因為s.toString()方法這個看起來是在return后面因宇,但其實這個方法屬于這個try代碼塊七婴,分為兩步,先調(diào)用toString()生成了一個新的字符串starttry然后返回察滑,所以這里的字節(jié)碼邏輯就如下:

      17: aload_1
      18: invokevirtual #12                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      21: astore_2
      22: aload_1
      23: ldc           #18                 // String finally
      25: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      28: pop
      29: aload_2
      30: areturn

可以很清楚的看到 調(diào)用append方法拼接“start”和“try”后打厘,先調(diào)用了toString()方法然后將值存入局部變量2。這時候finally沒有和上面那樣復(fù)制一份變量贺辰,而是繼續(xù)使用局部變量1的引用來繼續(xù)append婚惫,最后的結(jié)果也存入了局部變量1中,最后返回的是局部變量2中的值starttry魂爪,但是要注意此時局部變量1中指向的StringBuilder的值卻是starttryfinally先舷,所以這也就是方法1中返回的值。

4.如何快捷查看字節(jié)碼

如果是ide的話滓侍,應(yīng)該都可以蒋川,通過``Setting->Tools->External Tools進(jìn)入 然后創(chuàng)建一個自定義的tools。


設(shè)置

調(diào)用

如上圖撩笆,新建一個External Tools,第一行輸入你電腦的javap.exe地址捺球,第二行是你想要的命令符,第三行是顯示位置夕冲,設(shè)置好后要對著代碼右鍵即可一鍵查看字節(jié)碼指令氮兵,方便快捷。

5.Tips(后續(xù)有就繼續(xù)更新)

5.1 對象被new指令創(chuàng)建后為什么會執(zhí)行一個dup(將棧頂?shù)臄?shù)據(jù)復(fù)制一份并壓入棧)歹鱼?

對象被new之后還需要調(diào)用invokespecial <init>來初始化泣栈,這里需要拿到一份new指令分配的內(nèi)存地址,然后棧中還存在的一份地址是供這個對象給其他地方調(diào)用的,否則棧中如果不存在這個引用之后南片,任何地方都訪問不到這個類了掺涛,所以就算這個類沒有被任何地方調(diào)用,棧中還是會存在一份它的引用疼进。

6. 總結(jié)

本來只是想寫點(diǎn)字節(jié)碼指令的筆記薪缆,結(jié)果越記越多,本文大部分理論知識來自于《深入理解Java虛擬機(jī)--周志明》伞广,寫得多了拣帽,錯誤在所難免,如果有發(fā)現(xiàn)的還望指出嚼锄,謝謝减拭。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市灾票,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌茫虽,老刑警劉巖刊苍,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異濒析,居然都是意外死亡正什,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進(jìn)店門号杏,熙熙樓的掌柜王于貴愁眉苦臉地迎上來婴氮,“玉大人,你說我怎么就攤上這事盾致≈骶” “怎么了?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵庭惜,是天一觀的道長罩驻。 經(jīng)常有香客問我,道長护赊,這世上最難降的妖魔是什么惠遏? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮骏啰,結(jié)果婚禮上节吮,老公的妹妹穿的比我還像新娘。我一直安慰自己判耕,他們只是感情好透绩,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般渺贤。 火紅的嫁衣襯著肌膚如雪雏胃。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天志鞍,我揣著相機(jī)與錄音瞭亮,去河邊找鬼。 笑死固棚,一個胖子當(dāng)著我的面吹牛统翩,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播此洲,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼厂汗,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了呜师?” 一聲冷哼從身側(cè)響起娶桦,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎汁汗,沒想到半個月后衷畦,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡知牌,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年祈争,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片角寸。...
    茶點(diǎn)故事閱讀 40,090評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡菩混,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出扁藕,到底是詐尸還是另有隱情沮峡,我是刑警寧澤,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布亿柑,位于F島的核電站帖烘,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏橄杨。R本人自食惡果不足惜秘症,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望式矫。 院中可真熱鬧乡摹,春花似錦、人聲如沸采转。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至板熊,卻和暖如春框全,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背干签。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工津辩, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人容劳。 一個月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓喘沿,卻偏偏與公主長得像,于是被迫代替她去往敵國和親竭贩。 傳聞我的和親對象是個殘疾皇子蚜印,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,033評論 2 355

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