Java字節碼結構剖析一:常量池

Class文件的結構

Class文件是一組以8位字節為基礎單位的二進制流,各個數據項目嚴格按照順序緊湊地排列在Class文件之中,中間沒有添加任何分隔符,這使得整個Class文件中存儲的內容幾乎全部是程序運行的必要數據,沒有空隙存在。當遇到需要占用8位字節以上空間地數據項時,則會按照高位在前的方式分割成若干個8位字節進行存儲。

每一個 Class 文件對應于一個如下所示的 ClassFile 結構體。

ClassFile{u4magic;u2minor_version;u2major_version;u2constant_pool_count;cp_infoconstant_pool[constant_pool_count-1];u2access_flags;u2this_class;u2super_class;u2interfaces_count;u2interfaces[interfaces_count];u2fields_count;field_infofields[fields_count];u2methods_count;method_infomethods[methods_count];u2attributes_count;attribute_infoattributes[attributes_count];}

這種數據結構,類似C語言結構體。這個結構體中只有兩種數據類型:無符號數和表,后面的解析都要以這兩種數據類型為基礎,所以這里要先介紹這兩個概念。

無符號數屬于基本的數據類型,以u1,u2,u4,u8來分別代表1個字節,2個字節,4個字節和8個字節的無符號數,無符號數可以用來描述數字、索引引用、數量值或者按照UTF-8編碼構成字符串值。

表是由多個無符號數或者其他表作為數據項構成的復合數據類型,所有表都習慣性地以“_info”結尾。表用于描述有層次關系的復合結構的數據,整個Class文件本質就是一張表。

下面是我的案例代碼,本章將以此代碼生成的字節碼文件作為例子來分析。

publicclassMyTest2{? ? String str ="Welcome";privateintx =5;publicstaticIntegerin=10;publicstaticvoidmain(String[] args){? ? ? ? MyTest2 myTest2 =newMyTest2();? ? ? ? myTest2.setX(8);in=20;? ? }publicvoidsetX(intx){this.x = x;? ? }}

對應生成的字節碼文件格式如下:(數據內容較多,只是截了部分)

上面的數字是以16進制表示的。我們可以按照之前的結構一項項去解讀它。

Class文件解析

magic

魔數,u4類型的數據,占4個字節。魔數的唯一作用是確定這個文件是否為一個能被虛擬機所接受的 Class 文件。魔數值固定為?0xCAFEBABE?(咖啡寶貝),不會改變。

minor_version、major_version

緊接著魔數之后的4個字節為Java版本信息:第5和第6個字節是次版本號(minor_version),第7和第8個字節是主版本號(major_version)。

就看當前這個字節碼,次版本號是0×0000=0,主版本號是0×0034=52。我本地機器用的是JDK1.8,所以可生成的Class文件主版本號最大值為52.0。

下面給出了Java各個主版本號,以供參考。

constant_pool_count

常量池計數器,u2類型的數據。它是常量池的入口,表示緊跟著它后面的常量池的元素個數。算一下,0x002F=47,即常量池里的元素有47個。這里我用jdk的內置工具javap,反編譯一下,可以輸出常量池的信息以及元素個數。執行命令:javap -verbose。輸出結果如下:

Constant pool:#1= Methodref#10.#34// java/lang/Object."<init>":()V#2=String#35// Welcome#3= Fieldref#5.#36// com/shengsiyuan/jvm/bytecode/MyTest2.str:Ljava/lang/String;#4= Fieldref#5.#37// com/shengsiyuan/jvm/bytecode/MyTest2.x:I#5=Class#38// com/shengsiyuan/jvm/bytecode/MyTest2#6= Methodref#5.#34// com/shengsiyuan/jvm/bytecode/MyTest2."<init>":()V#7= Methodref#5.#39// com/shengsiyuan/jvm/bytecode/MyTest2.setX:(I)V#8= Methodref#40.#41// java/lang/Integer.valueOf:(I)Ljava/lang/Integer;#9= Fieldref#5.#42// com/shengsiyuan/jvm/bytecode/MyTest2.in:Ljava/lang/Integer;#10=Class#43// java/lang/Object#11= Utf8? ? ? ? ? ? ? str#12= Utf8? ? ? ? ? ? ? Ljava/lang/String;#13= Utf8? ? ? ? ? ? ? x#14= Utf8? ? ? ? ? ? ? I#15= Utf8in#16= Utf8? ? ? ? ? ? ? Ljava/lang/Integer;#17= Utf8? ? ? ? ? ? ? #18= Utf8? ? ? ? ? ? ? ()V#19= Utf8? ? ? ? ? ? ? Code#20= Utf8? ? ? ? ? ? ? LineNumberTable#21= Utf8? ? ? ? ? ? ? LocalVariableTable#22= Utf8? ? ? ? ? ? ? this#23= Utf8? ? ? ? ? ? ? Lcom/shengsiyuan/jvm/bytecode/MyTest2;#24= Utf8? ? ? ? ? ? ? main#25= Utf8? ? ? ? ? ? ? ([Ljava/lang/String;)V#26= Utf8? ? ? ? ? ? ? args#27= Utf8? ? ? ? ? ? ? [Ljava/lang/String;#28= Utf8? ? ? ? ? ? ? myTest2#29= Utf8? ? ? ? ? ? ? setX#30= Utf8? ? ? ? ? ? ? (I)V#31= Utf8? ? ? ? ? ? ? #32= Utf8? ? ? ? ? ? ? SourceFile#33= Utf8? ? ? ? ? ? ? MyTest2.java#34= NameAndType#17:#18// "<init>":()V#35= Utf8? ? ? ? ? ? ? Welcome#36= NameAndType#11:#12// str:Ljava/lang/String;#37= NameAndType#13:#14// x:I#38= Utf8? ? ? ? ? ? ? com/shengsiyuan/jvm/bytecode/MyTest2#39= NameAndType#29:#30// setX:(I)V#40=Class#44// java/lang/Integer#41= NameAndType#45:#46// valueOf:(I)Ljava/lang/Integer;#42= NameAndType#15:#16// in:Ljava/lang/Integer;#43= Utf8? ? ? ? ? ? ? java/lang/Object#44= Utf8? ? ? ? ? ? ? java/lang/Integer#45= Utf8? ? ? ? ? ? ? valueOf#46= Utf8? ? ? ? ? ? ? (I)Ljava/lang/Integer;

可是,我們得到的常量池里的元素個數是46。我們看常量池第一個元素,它的索引是從1開始的。所以索引值范圍是1~46。設計者將第0項常量空出來是有特殊考慮的,這樣做的目的在于滿足后面某些指向常量池的索引值的數據在特定情況下需要表達“不引用任何一個常量池項目”的含義,這種情況就可以把索引值置為0來表示。根本原因在于,索引為0也是一個常量(保留常量),只不過它不位于常量表中。這個常量就對應Null值,所以常量池的索引從1而非0開始。

常量池結構剖析

緊接其后的就是常量池了。一個Java類中定義的很多信息都是由常量池維護和描述的。可以將常量池看作是Class文件的資源庫。比如:Java類中定義的方法與變量信息,都是存儲在常量池中。常量池中主要存儲兩類常量:字面常量和符號引用。字面量,如文本字符串,Java中聲明為常量值,而符號引用如類和接口的全局限定名,字段的名稱和描述符,方法的名稱和描述符等。

注:常量池中存儲的不一定是不變的量!如,?private int x = 5?,x是變量,但“x”這個變量名字依然存在常量池中。

我們也可以把常量池當做一個數組(常量池中的每一項常量都是一個表),與一般數組不同的是,常量池數組中不同的元素類型,結構都是不同的,長度當然也不相同;但是每一個元素的第一個數據都是u1類型,該字節是個標志位,占一個字節。JVM在解析長量池時,會根據這個u1類型來獲取元素的具體類型。目前,常量池中出現的常量類型有14種,如下表:

有了這張表就可以繼續剖析常量池的內容了,常量池第一個字節就是一個標志位,0x000A=10,說明第一個常量類型是CONSTANT_Methodref_info。這是一個表類型,它對應的結構是:

CONSTANT_Methodref_info{u1tag;u2class_index;u2name_and_type_index;}

可知,該類型常量占1+2+2=5個字節。所以我們從常量池前5個字節就是第一個常量元素了。緊接后面就是第二個常量,同樣的,開始是一個標志位,即0x008=8。可知,第二個常量是CONSTANT_String_info類型。CONSTANT_String_info 用于表示?java.lang.String?類型的常量對象,格式如下:

CONSTANT_String_info{u1tag;u2string_index;}

所以常量池的第二個元素占3個字節。按照這個套路,我們就可以找出每一個常量了。一直數到第46個常量,常量池就結束了。此處是常量池中的?14種常量項的結構總表?。感興趣的可以對照這個表,去把剩下的常量對照出來。

常量項分析

第一個常量是CONSTANT_Methodref_info類型的,它描述了類中方法的符號引用。class_index 項的值必須是對常量池的有效索引,常量池在該索引處的項必須是CONSTANT_Class_info結構,表示一個類或接口。

class_index表示的索引值是0x000A=10。根據之前?javap -verbose??輸出的常量池信息,我們可以知道常量池的#10項是CONSTANT_Class_info類型的常量。該類型常量用于表示類或接口,格式如下:

CONSTANT_Class_info{u1tag;u2name_index;}

name_index 項的值,必須是對常量池的一個有效索引。常量池在該索引處的項必須是CONSTANT_Utf8_info結構,代表一個有效的類或接口二進制名稱的內部形式。

name_index 表示的索引值是43(這里我直接從上面的量池信息讀出,如果從字節碼里看,此處的值為0x002B=43)。所以接著找常量池第43項的常量類型,是CONSTANT_utf8_info類型,用于表示字符串常量的值,結構如下:

CONSTANT_Utf8_info{u1tag;u2length;u1bytes[length];}

其中,length 項的值指明了 bytes[]數組的長度,bytes[]是表示字符串值的byte數組。在這里,我把字節碼常量池中#43處常量的16進制值單獨拿出來來看。下圖有背景色的部分就是完整的CONSTANT_Utf8_info類型常量表示。

第一個字節是標志位,0×0001=1。說明此常量類型是CONSTANT_Utf8_info。后面2個字節是0×0010=16,表示后面bytes[]長度為16。所以往后數16個字節就是整個它表示的字符串常量。

bytes[]第一個字節值,0x006A。根據?ASCII碼對照表?,代表的字符串是”j”。依次的,第二個字節0×0061,代表“a”,等等。把16個字節看完你就得到了字符串常量表示“java/lang/Object”。好了這表示一個類的全限定名。饒了一大圈,終于找到最終要表示的常量信息了。

到此,我們把第一個常量的結構中的class_index就解析完了,還剩一個name_and_type_index。它表示了常量池在該索引處的項必須是 CONSTANT_NameAndType_info結構,它表示當前字段或方法的名字和描述符。后面大家可以根據?常量池中的14種常量項的結構總表?,并結合javap得到的常量池信息,自己去分析每個常量在常量池里是怎么個回事。

總結

這篇文章介紹了,字節碼文件的結構組成,并分析了魔數、次主版本號和常量池。尤其帶大家深入分析了常量池的組成結構,并拿例子中的常量池第一個常量作為案例,完整解析它在常量池中的各項引用。套路都是一樣的,常量池后面的常量,大家可以自己去分析了。你會發現類中有用的信息都存在了我們的常量池里,然后以索引的形式,給代碼使用。這也就是常量池作為class文件的資源倉庫的原因了。

 在此我向大家推薦一個架構學習交流群。交流學習群號:938837867 暗號:555 里面會分享一些資深架構師錄制的視頻錄像:有Spring,MyBatis,Netty源碼分析,高并發、高性能、分布式、微服務架構的原理,JVM性能優化、分布式架構等這些成為架構師必備

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,578評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,701評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,691評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,974評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,694評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,026評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,015評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,193評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,719評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,442評論 3 360
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,668評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,151評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,846評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,255評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,592評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,394評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,635評論 2 380

推薦閱讀更多精彩內容