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)原理就是通過StringBuilder
的append
進(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");
}
}
上圖中純數(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)如下圖
上圖中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_info
和CONSTANT_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ù)回到例子
例子中只是一個簡單的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
的簡單名稱就為fun
和a
。 -
方法和字段的描述符:描述符的作用是用來描述字段的數(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 |
因為volatile
和transient
關(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=1
和static int a=1
触机,虛擬機(jī)對這兩種變量的賦值方式和時刻都有所不同。對于前者的賦值是在實例構(gòu)造器<init>方法中進(jìn)行的玷或,換而言之就是一個類的構(gòu)造的方法沒有被執(zhí)行前儡首,該類的成員變量是還沒賦值的;而對于后者偏友,則有兩種方式可以選擇:在類構(gòu)造器<clinit>方法中或者使用ConstantValue屬性蔬胯。目前Javac編譯器的選擇是如果同時使用final
和static
來修飾一個變量,并且這個變量的數(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ī)指令集中有monitorenter
和monitorexit
兩條指令來支持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
修改原來的變量洼畅,但try
中return
返回的是副本變量吩案,所以如果是賦值操作,即使執(zhí)行了finally
中的代碼帝簇,變量也不一定會改變徘郭,需要看變量是基本類型還是引用類型靠益。
但是如果在finally里面添加一個return,那么第9行和第19行加載的就是finally
塊里修改的值(iload_1),再在最后添加一個iload_1
和ireturn
,感興趣的可以自己去看一下字節(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。
如上圖撩笆,新建一個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)的還望指出嚼锄,謝謝减拭。