Class類文件結(jié)構(gòu):
- Class文件是一組以8字節(jié)為基礎(chǔ)單位的二進(jìn)制流,
- 各個數(shù)據(jù)項目嚴(yán)格按照順序緊湊排列在class文件中老充,
- 中間沒有任何分隔符衔彻,這使得class文件中存儲的內(nèi)容幾乎是全部程序運(yùn)行的程序尘盼。
Java虛擬機(jī)規(guī)范規(guī)定,Class文件格式采用類似C語言結(jié)構(gòu)體的偽結(jié)構(gòu)來存儲數(shù)據(jù)兼吓,這種結(jié)構(gòu)只有兩種數(shù)據(jù)類型:無符號數(shù)和表臂港。
無符號數(shù)
屬于基本數(shù)據(jù)類型,主要可以用來描述數(shù)字视搏、索引符號审孽、數(shù)量值或者按照UTF-8編碼構(gòu)成的字符串值,大小使用u1浑娜、u2佑力、u4、u8分別表示1字節(jié)筋遭、2字節(jié)打颤、4字節(jié)和8字節(jié)暴拄。
表
是由多個無符號數(shù)或者其他表作為數(shù)據(jù)項構(gòu)成的復(fù)合數(shù)據(jù)類型,所有的表都習(xí)慣以“_info”結(jié)尾编饺。表主要用于描述有層次關(guān)系的復(fù)合結(jié)構(gòu)的數(shù)據(jù)乖篷,比如方法、字段透且。需要注意的是class文件是沒有分隔符的那伐,所以每個的二進(jìn)制數(shù)據(jù)類型都是嚴(yán)格定義的。具體的順序定義如下:
魔數(shù)
- 每個Class文件的頭4個字節(jié)稱為魔數(shù)(Magic Number)
- 唯一作用是用于確定這個文件是否為一個能被虛擬機(jī)接受的Class文件石蔗。
- Class文件魔數(shù)的值為0xCAFEBABE罕邀。如果一個文件不是以0xCAFEBABE開頭,那它就肯定不是Java class文件养距。
很多文件存儲標(biāo)準(zhǔn)中都使用魔數(shù)來進(jìn)行身份識別诉探,譬如圖片格式,如gif或jpeg等在文件頭中都存有魔數(shù)棍厌。使用魔術(shù)而不是使用擴(kuò)展名是基于安全性考慮的——擴(kuò)展名可以隨意被改變!!!
例如:
Class文件的版本號
緊接著魔數(shù)的4個字節(jié)是Class文件版本號肾胯,版本號又分為:
- 次版本號(minor_version): 前2字節(jié)用于表示次版本號
- 主版本號(major_version): 后2字節(jié)用于表示主版本號。
這個的版本號是隨著jdk版本的不同而表示不同的版本范圍的耘纱。Java的版本號是從45開始的敬肚。如果Class文件的版本號超過虛擬機(jī)版本,將被拒絕執(zhí)行束析。
0X0034(對應(yīng)十進(jìn)制的50):JDK1.8
0X0033(對應(yīng)十進(jìn)制的50):JDK1.7
0X0032(對應(yīng)十進(jìn)制的50):JDK1.6
0X0031(對應(yīng)十進(jìn)制的49):JDK1.5
0X0030(對應(yīng)十進(jìn)制的48):JDK1.4
0X002F(對應(yīng)十進(jìn)制的47):JDK1.3
0X002E(對應(yīng)十進(jìn)制的46):JDK1.2
ps:0X表示16進(jìn)制
即:
常量池
緊接著魔數(shù)與版本號之后的是常量池入口.常量池簡單理解為class文件的資源從庫
- 是Class文件結(jié)構(gòu)中與其它項目關(guān)聯(lián)最多的數(shù)據(jù)類型
- 是占用Class文件空間最大的數(shù)據(jù)項目之一
- 是在文件中第一個出現(xiàn)的表類型數(shù)據(jù)項目
由于常量池中常量的數(shù)量是不固定的艳馒,所以在常量池的入口需要放置一項u2類型的數(shù)據(jù),代表常量池容量計數(shù)值(constant_pool_count)员寇。
從1開始計數(shù)弄慰。Class文件結(jié)構(gòu)中只有常量池的容量計數(shù)是從1開始的,第0項騰出來滿足后面某些指向常量池的索引值的數(shù)據(jù)在特定情況下需要表達(dá)"不引用任何一個常量池項目"的意思蝶锋,這種情況就可以把索引值置為0來表示(留給JVM自己用的)陆爽。但盡管constant_pool列表中沒有索引值為0的入口,缺失的這一入口也被constant_pool_count計數(shù)在內(nèi)扳缕。例如慌闭,當(dāng)constant_pool中有14項,constant_poo_count的值為15躯舔。
常量池之中主要存放兩大類常量:
- 字面量: 比較接近于Java語言層面的常量概念驴剔,如文本字符串、被聲明為final的常量值等
- 符號引用: 屬于編譯原理方面的概念庸毫,包括了下面三類常量:
- 類和接口的全限定名
- 字段的名稱和描述符
- 方法的名稱和描述符
constant_pool_count:占2字節(jié)仔拟,常量池的計數(shù)是從1開始的,其它集合類型均從0開始飒赃,索引值為1~n利花。第0項常量具有特殊意義科侈,如果某些指向常量池索引值的數(shù)據(jù)在特定情況下需要表達(dá)“不引用任何一個常量池項目”的含義,這種情況可以將索引值置為0來表示
constant_pool:表類型數(shù)據(jù)集合炒事,即常量池中每一項常量都是一個表臀栈,共有14種(JDK1.7前只有11種)結(jié)構(gòu)各不相同的表結(jié)構(gòu)數(shù)據(jù)。這14種表都有一個共同的特點挠乳,即均由一個u1類型的標(biāo)志位開始权薯,可以通過這個標(biāo)志位來判斷這個常量屬于哪種常量類型,常量類型及其數(shù)據(jù)結(jié)構(gòu)如下表所示:
ps:
這14種表都有一個共同的特點睡扬,就是表開始的第一位是一個u1類型的標(biāo)識位盟蚣,取值見上表,代表當(dāng)前這個常量屬于哪種常量類型卖怜。
例如:
常量池的計數(shù)是從1開始的,這就代表常量池中有28項常量
0A:
CONSTANT_Methodref_info (類中方法的符號引用)
0x0006 #6---指向常量池#6號索引
0x000F #15---指向常量池#15號索引
09:
CONSTANT_Fieldref (對一個字段的符號引用)
0x0010 #16---指向常量池#16號索引
0x0011 #17---指向常量池#17號索引
通過命令即可驗證
javap -verbose HelloTest.class
Classfile /Users/liuboyu/Desktop/HelloTest.class
Last modified 2018-9-7; size 426 bytes
MD5 checksum 3820cf8768d7c58bbf1e4242cf82dd18
Compiled from "HelloTest.java"
public class HelloTest
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#15 // java/lang/Object."<init>":()V
#2 = Fieldref #16.#17 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #18 // Hello world!!!
#4 = Methodref #19.#20 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #21 // HelloTest
#6 = Class #22 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 main
#12 = Utf8 ([Ljava/lang/String;)V
#13 = Utf8 SourceFile
#14 = Utf8 HelloTest.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = Class #23 // java/lang/System
#17 = NameAndType #24:#25 // out:Ljava/io/PrintStream;
#18 = Utf8 Hello world!!!
#19 = Class #26 // java/io/PrintStream
#20 = NameAndType #27:#28 // println:(Ljava/lang/String;)V
#21 = Utf8 HelloTest
#22 = Utf8 java/lang/Object
#23 = Utf8 java/lang/System
#24 = Utf8 out
#25 = Utf8 Ljava/io/PrintStream;
#26 = Utf8 java/io/PrintStream
#27 = Utf8 println
#28 = Utf8 (Ljava/lang/String;)V
{
public HelloTest();
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 1: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Hello world!!!
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 3: 0
line 4: 8
}
SourceFile: "HelloTest.java"
訪問標(biāo)志(2字節(jié))
常量池之后的數(shù)據(jù)結(jié)構(gòu)是訪問標(biāo)志(access_flags),這個標(biāo)志主要用于識別一些類或接口層次的訪問信息屎开,主要包括:
例如:
查看我們的字節(jié)碼文件,可以得知
標(biāo)識位為:ACC_PUBLIC 马靠,ACC_SUPER
驗證結(jié)果:
類索引沮焕、父類索引和接口索引集合
這三項數(shù)據(jù)主要用于確定這個類的繼承關(guān)系除秀。
其中類索引(this_class)和父類索引(super_class)都是一個u2類型的數(shù)據(jù)匈仗,而接口索引(interface)集合是一組u2類型的數(shù)據(jù)供炎。(多實現(xiàn)單繼承)
類索引(this_class)
用于確定這個類的全限定名,占2字節(jié)
父類索引(super_class)
用于確定這個類父類的全限定名(Java語言不允許多重繼承妙啃,故父類索引只有一個档泽。除了java.lang.Object類之外所有類都有父類,故除了java.lang.Object類之外彬祖,所有類該字段值都不為0)茁瘦,占2字節(jié)
接口索引計數(shù)器(interfaces_count)
占2字節(jié)品抽。如果該類沒有實現(xiàn)任何接口储笑,則該計數(shù)器值為0,并且后面的接口的索引集合將不占用任何字節(jié)圆恤,
接口索引集合(interfaces)突倍,一組u2類型數(shù)據(jù)的集合。用來描述這個類實現(xiàn)了哪些接口盆昙,這些被實現(xiàn)的接口將按implements語句(如果該類本身為接口羽历,則為extends語句)后的接口順序從左至右排列在接口的索引集合中
this_class、super_class與interfaces按順序排列在訪問標(biāo)志之后淡喜,它們中保存的索引值均指向常量池中一個CONSTANT_Class_info類型的常量秕磷,通過這個常量中保存的索引值可以找到定義在CONSTANT_Utf8_info類型的常量中的全限定名字符串
在我們的例子中:
0x0005:指向常量池 #5
0x0006:指向常量池 #6
0x0000:沒有接口
一目了然~
類的全限定名:HelloTest
父類的全限定名:java/lang/Object
字段表集合
fields_count:字段表計數(shù)器,即字段表集合中的字段表數(shù)據(jù)個數(shù)炼团,占2字節(jié)澎嚣。
fields:字段表集合疏尿,一組字段表類型數(shù)據(jù)的集合。字段表用于描述接口或類中聲明的變量易桃,包括類級別(static)和實例級別變量褥琐,不包括在方法內(nèi)部聲明的變量
在Java中一般通過如下幾項描述一個字段:字段作用域(public、protected晤郑、private修飾符)敌呈、是類級別變量還是實例級別變量(static修飾符)、可變性(final修飾符)造寝、并發(fā)可見性(volatile修飾符)磕洪、可序列化與否(transient修飾符)、字段數(shù)據(jù)類型(基本類型诫龙、對象褐鸥、數(shù)組)以及字段名稱。在字段表中赐稽,變量修飾符使用標(biāo)志位表示叫榕,字段數(shù)據(jù)類型和字段名稱則引用常量池中常量表示。
- access_flags:是一個 u2的數(shù)據(jù)類型姊舵。
- name_index 索引值: 對常量池的引用晰绎,代表著字段的簡單名稱。
- descriptor_index 索引值: 對常量池的引用括丁,代表字段和方法的描述符荞下。
重新寫個有字段的類作為例子講解。
0x0003: fields_count(字段表數(shù)量)說明有三個字端表
-
access_flags:0x0009 ---- public static
name_index 索引值: 0x000A ---- 指向常量池#10
descriptor_index 索引值:0x000B --- 指向常量池#11
attributes_count 屬性表:0x0000 --- 沒有屬性表
證明:
則該字段的 name_index 索引值: 0x000A ---- 名字為a
則該字段的 descriptor_index 索引值:0x000B --- I類型,由下圖可知史飞,該類型為int類型
-
access_flags:0x0002 ---- private
name_index 索引值: 0x000C ---- 指向常量池#12
descriptor_index 索引值:0x000D --- 指向常量池#13
attributes_count 屬性表:0x0000 --- 沒有屬性表
所以該字段為:
private String str;
- access_flags:0x0001 ---- public
name_index 索引值: 0x000E ---- 指向常量池#14
descriptor_index 索引值:0x000F --- 指向常量池#15
attributes_count 屬性表:0x0000 --- 沒有屬性表
所以該字段為:
public double d
方法表集合
methods_count:方法表計數(shù)器尖昏,即方法表集合中的方法表數(shù)據(jù)個數(shù)。占2字節(jié)
methods:方法表集合构资,一組方法表類型數(shù)據(jù)的集合抽诉。方法表結(jié)構(gòu)和字段表結(jié)構(gòu)一樣。
JVM中堆方法表的描述與字段表是一致的吐绵,包括了:訪問標(biāo)志迹淌、名稱索引、描述符索引己单、屬性表集合唉窃。方法表的結(jié)構(gòu)與字段表是一致的,區(qū)別在于訪問標(biāo)志的不同纹笼。在方法中不能用volatile和transient關(guān)鍵字修飾纹份,所以這兩個標(biāo)志不能用在方法表中。在方法中添加了字段不能使用的訪問標(biāo)志廷痘,比如方法可以使用synchronized蔓涧、native削咆、strictfp、abstract關(guān)鍵字修飾蠢笋,所以在方法表中就增加了相應(yīng)的訪問標(biāo)志拨齐。
要注意的是,如果父類方法沒有在子類中重寫昨寞,那么在方法中不會自動出現(xiàn)來自父類的方法信息瞻惋。同樣的,有可能添加編譯器自動增加的方法援岩,比如方法歼狼。
該示例一共有三個方法:init(自帶構(gòu)造器),main,add
方法1: init方法(構(gòu)造器方法)
0x0001: ACC_PUBLIC
0x0010: 指向常量池#16,查看上面文件可知 #16 = Utf8 <init>
0x0011: 指向常量池#17享怀,查看上面文件可知 #17 = Utf8 ()V羽峰,為 void
0x0001: 有一個屬性
0x0012: 指向常量池#18,查看上面文件可知#18 = Utf8 Code添瓷,說明此屬性是方法的字節(jié)碼描述
main,add方法同理梅屉,下面我們看一下code屬性
屬性表集合
在 Class 文件、字段表鳞贷、方法表都可以攜帶子機(jī)的屬性表集合坯汤,以用于描述某些場景專有的信息。
與 Class 文件中其他的數(shù)據(jù)項目要求嚴(yán)格的順序搀愧、長度和內(nèi)容不同惰聂,屬性表集合的限制稍微寬松了一些,不再要求各個屬性表具有嚴(yán)格順序咱筛,并且只要不與已有屬性名重復(fù)搓幌,任何人實現(xiàn)的編譯器都可以向?qū)傩员碇袑懭胱约憾x的屬性信息,
Java 虛擬機(jī)運(yùn)行時會忽略掉它不認(rèn)識的屬性迅箩。為了能正確解析 Class 文件溉愁,《Java 虛擬機(jī)規(guī)范(第 2 版)》中預(yù)定義了 9 項虛擬機(jī)實現(xiàn)應(yīng)當(dāng)能識別的屬性,而在最新的《Java 虛擬機(jī)規(guī)范(Java SE 7)》版中沙热,
預(yù)定義屬性已經(jīng)增加到 21 項叉钥,具體內(nèi)容見表下表
對于每個屬性篙贸,它的名稱需要從常量池中引用一個 CONSTANT_Utf8_info 類型的常量來表示,而屬性值的結(jié)構(gòu)則是完全自定義的枫疆,只需要通過一個 u4 的長度屬性去說明屬性值所占用的位數(shù)即可爵川。一個符合規(guī)則的屬性表應(yīng)該滿足下表中所定義的結(jié)構(gòu)。
Code屬性
Java程序方法體中的代碼經(jīng)過Javac編譯器處理后息楔,最終變?yōu)樽止?jié)碼指令存儲在Code屬性內(nèi)寝贡。Code屬性出現(xiàn)在方法表的屬性集合中扒披,但并非所有的方法都不許存在這個屬性,譬如接口或者抽象類中的方法就不存在Code屬性圃泡,如果
方法表有Code屬性存在碟案,那么它的結(jié)構(gòu)如表所示。
attribute_name_index 是一項指向 CONSTANT_Utf8_info 型常量的索引颇蜡,常量值固定為“Code”价说,它代表了該屬性的屬性名稱,attribute_length 指示了屬性值的長度风秤,由于屬性名稱索引與屬性長度一共為 6 字節(jié)鳖目,所以屬性值的長度固定為整個屬性表長度減去 6 個字節(jié)。
max_stack 代表了操作數(shù)棧(Operand Stacks)深度的最大值缤弦。在方法執(zhí)行的任意時刻领迈,操作數(shù)棧都不會超過這個深度。虛擬機(jī)運(yùn)行的時候需要根據(jù)這個值來分配棧幀(Stack Frame)中的操作棧深度碍沐。
max_locals 代表了局部變量表所需的存儲空間狸捅,在這里,max_locals 的單位是 Slot累提,Slot 是虛擬機(jī)為局部變量分配內(nèi)存所使用的最小單位薪贫。對于 byte、char刻恭、float瞧省、int、short鳍贾、boolean 和 returnAddress 等長度不超過 32 位的數(shù)據(jù)類型鞍匾,每個局部變量占用 1 個 Slot,
而 double 和 long 這兩種 64 位的數(shù)據(jù)類型則需要兩個 Slot 來存放骑科。方法參數(shù)(包括實例方法中的隱藏參數(shù) “this”)橡淑、顯式異常處理器的參數(shù)(Exception Handler Parameter,就是 try-catch 語句中 catch 塊所定義的異常)咆爽、方法體中定義的局部變量都需要使用局部變量表來存放梁棠。
另外,并不是在方法中用到了多少個局部變量斗埂,就把這些局部變量所占 Slot 之和作為 max_locals 的值符糊,原因是局部變量表中的 Slot 可以重寫,當(dāng)代碼執(zhí)行超出一個局部變量的作用域時呛凶,這個局部變量所占的 Slot 可以被其他局部變量所使用男娄,
Javac 編譯器會根據(jù)變量的作用域來分配 Slot 給各個變量使用,然后計算出 max_locals 的大小。
code_length 和 code 用來存儲 Java 源程序編譯后生成的字節(jié)碼指令模闲。code_length 代表字節(jié)碼長度,code 是用于存儲字節(jié)碼指令的一系列字節(jié)流尸折。既然叫字節(jié)碼指令啰脚,那么每個指令就是一個 u1 類型的單字節(jié),當(dāng)虛擬機(jī)讀取到 code 中的一個字節(jié)碼時实夹,
就可以對應(yīng)找出這個字節(jié)碼代表的是什么指令橄浓,并且可以知道到這條指令后面是否需要跟隨參數(shù),以及參數(shù)應(yīng)當(dāng)如何理解收擦。我們知道一個 u1 數(shù)據(jù)類型的取值范圍為 0x00 ~ 0xFF贮配,對應(yīng)十進(jìn)制的 0 ~ 255,也就是一共可以表達(dá) 256 條指令塞赂,目前泪勒,Java 虛擬機(jī)規(guī)范已經(jīng)定義了其中約 200 條編碼值對應(yīng)的指令含義。
關(guān)于 code_length宴猾,有一件值得注意的事情圆存,雖然它是一個 u4 類型的長度值,理論上最大值可以達(dá)到 2^23-1仇哆,但是虛擬機(jī)規(guī)范中明確限制了一個方法不允許超過65535 條字節(jié)碼指令沦辙,即它實際只使用了 u2 的長度,如果超過這個限制讹剔,Javac 編譯器也會拒絕編譯油讯。一般來講,編寫 Java 代碼時只要不是刻意去編寫一個超長的方法來為難編譯器延欠,是不太可能超過這個最大值的限制陌兑。但是,某些特殊情況由捎,例如在編譯一個很復(fù)雜的 JSP 文件時兔综,某些 JSP 編譯會把 JSP 內(nèi)容和頁面輸出的信息歸并于一個方法之中,就可能因為方法生成字節(jié)碼超長的原因而導(dǎo)致編譯失敗狞玛。
Code 屬性是 Class 文件中最重要的一個屬性软驰,如果把一個 Java 程序中的信息分為代碼(Code,方法體里面的 Java 代碼)和元數(shù)據(jù)(Metadata心肪,包括類锭亏、字段、方法定義及其他信息)量部分蒙畴,那么在整個 Class 文件中贰镣,Code 屬性用于描述代碼呜象,所有的其他數(shù)據(jù)項目都用于描述元數(shù)據(jù)膳凝。了解 Code 屬性是學(xué)習(xí)后面關(guān)于字節(jié)碼執(zhí)行引擎內(nèi)容的必要基礎(chǔ)碑隆,能直接閱讀字節(jié)碼也是工作中分析 Java 代碼語義問題的必要工具和基本技能,因此筆者準(zhǔn)備了一個比較詳細(xì)的實例來講解虛擬機(jī)是如何使用這個屬性的蹬音。
繼續(xù)以上面的代碼清單 HelloTest.class 文件為例上煤,如下圖所示
前面6個字節(jié)(名稱索引2字節(jié)+屬性長度4字節(jié))已經(jīng)解析過了,所以接下來就是解析剩下的56-6=50字節(jié)即可著淆。
max_stack:0x0003 操作數(shù)棧的最大深度3
max_locals:0x0001 本地變量表的容量1
code_length:0x0000 000C == 12,字節(jié)碼區(qū)域所占空間的長度12
虛擬機(jī)讀取到字節(jié)碼區(qū)域的長度后劫狠,按照順序依次讀入緊隨的 12 個字節(jié),并根據(jù)字節(jié)碼指令表翻譯出所對應(yīng)的字節(jié)碼指令永部。翻譯“2A B7 00 01 2A 14 00 02 B5 00 04 B1”
code代碼独泞,可以通過虛擬機(jī)字節(jié)碼指令進(jìn)行查找。
Java字節(jié)碼指令
- 0x2A : aload_0 將引用類型本地變量推送至棧頂
- 0xB7 : invokespecial 調(diào)用超類構(gòu)造方法苔埋,實例初始化方法懦砂,私有方法
- 0x00 : 什么都不做
- 0x01 : aconst_null 將null推送至棧頂
- 0x2A : aload_0 將引用類型本地變量推送至棧頂
- 0x14 : ldc2_w 將long或do le型常量值從常量池中推送至棧頂(寬索引)
- 0x00 : 什么都不做
- 0x02 : iconst_m1 將int型-1推送至棧頂
- 0xB5 : putfield 為指定的類的實例域賦值
- 0x00 : 什么都不做
- 0x04 : iconst_1 將int型1推送至棧頂
- 0xB1 : return 從當(dāng)前方法返回void
對照用javap命令計算機(jī)字節(jié)碼指令生成的代碼,我們分析的很正確~
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: ldc2_w #2 // double 1.2d
8: putfield #4 // Field d:D
11: return
LineNumberTable:
line 2: 0
line 5: 4
意外收獲:
做android開發(fā)的同學(xué)可能有遇到過 65535 這個問題组橄,看了jvm發(fā)現(xiàn)method_countde 最大值是兩個字節(jié)的數(shù)量也就是0xFFFF,即65535
ps:
本文為小弟本人所理解荞膘,如果不對的地方,忘多多指正玉工,共同進(jìn)步~~~謝謝大家