引言
我們都知道java是跨平臺的偎快,原因就在于各個平臺的java虛擬機(jī)可以載入和執(zhí)行同一種平臺無關(guān)的字節(jié)碼文件,也就是說java虛擬機(jī)不與包括Java在內(nèi)的任何語言綁定辆琅,只于Class
文件這種二進(jìn)制格式文件所關(guān)聯(lián)笙什。
基于這樣的設(shè)計即供,到目前為止已經(jīng)出現(xiàn)了很多基于Java虛擬機(jī)的語言
如groovy
最終都會編譯成class
文件
Class文件結(jié)構(gòu)
一個Class文件唯一對應(yīng)一個類或接口
現(xiàn)在讓我們來看下Class
文件的基本結(jié)構(gòu)
Class文件以8位字節(jié)為基本單位的二進(jìn)制文件吨悍,各個數(shù)據(jù)項目嚴(yán)格的按照順序緊湊的排列在Class
文件之中扫茅,中間沒有任何分隔符,這使得整個Class
文件中存儲的內(nèi)容幾乎全部是運(yùn)行時的必要數(shù)據(jù)
Class
文件的二進(jìn)制文件只有兩種數(shù)據(jù)類型:無符號數(shù)和表育瓜,后面的解析都會以這兩種數(shù)據(jù)類型為基礎(chǔ)
- 無符號數(shù)
無符號數(shù)是最基本的數(shù)據(jù)類型葫隙,以u1
,u2
,u4
,u8
來分別代表1個字節(jié),2個字節(jié)躏仇,4個字節(jié)恋脚,8個字節(jié)的無符號數(shù),無符號數(shù)用來描述數(shù)字焰手,索引引用糟描,數(shù)量值,以及以UTF-8編碼的字符串
- 表
表則是由多個無符號數(shù)或其他表組成的復(fù)合數(shù)據(jù)結(jié)構(gòu)书妻,表的名稱一般以_info
結(jié)尾蚓挤。所以整個Class
文件其實就是一張?zhí)厥獾谋?/strong>
下面表中所列的就是一個Class
按順序排列的數(shù)據(jù)結(jié)構(gòu)
類型 | 名稱 | 數(shù)量 |
---|---|---|
u4 | magic 魔數(shù) 標(biāo)識Class文件 | 1 |
u2 | minor_version 次版本號 | 1 |
u2 | major_version 主版本號 | 1 |
u2 | constant_pool_count 常量表集合數(shù)量 | 1 |
cp_info | constant_pool 常量表 | constant_pool_count - 1 |
u2 | access_flag 訪問標(biāo)識 | 1 |
u2 | this_class 類索引 | 1 |
u2 | super_class 父類索引 | 1 |
u2 | interfaces_count 接口索引數(shù)量 | 1 |
u2 | interfaces 接口索引 | interfaces_count |
u2 | fields_count 字段表集合數(shù)量 | 1 |
field_info | fields 字段表 | fields_count |
u2 | methods_count 方法表集合數(shù)量 | 1 |
method_info | methods 方法表 | methods_count |
u2 | attributes_count 屬性表集合數(shù)量 | 1 |
attribute_info | attributes 屬性表 | attributes_count |
下面依次來解讀表中每個類型
魔數(shù)和版本號
頭4個字節(jié)稱為Class
文件的魔數(shù),魔數(shù)的作用是標(biāo)識此文件能被Java虛擬機(jī)接受的Class
文件驻子,其實不止Class
文件有魔數(shù)這個概念,包括其他很多文件格式出于安全的考慮也都會有魔數(shù)這個概念估灿,魔數(shù)都是固定不變的崇呵,如Class
文件的魔數(shù)就是cafebabe
緊接著魔數(shù)之后的是版本號,第5 6個字節(jié)表示的是次版本號馅袁,第7 8個字節(jié)表示的是主版本號域慷。版本號都是向下兼容的
常量池表
讀懂常量池表對于閱讀Class字節(jié)碼非常重要,下面我們將以大篇幅分析常量池表
常量池是Class
文件中出現(xiàn)的第一個表結(jié)構(gòu)類型,同時也是占用Class
文件最大空間的類型之一犹褒。由于常量池表的數(shù)量不是固定的抵窒,所以在常量池的入口有一項u2
類型的數(shù)據(jù),來代表常量池的數(shù)量叠骑。并且常量池比較特殊李皇,容量計數(shù)是從1開始而不是從0開始,所以實際的常量池數(shù)量是constant_pool_count - 1
常量池中主要存放兩大類變量: 字面量和符號引用宙枷。字面量類似常量的概念掉房,而符號引用則引至編譯原理的概念,包括三類(類和接口的全限定名慰丛,字段的名稱和描述符卓囚,方法的名稱和描述符),這里要注意的是诅病,Java在javac
編譯的時候不會進(jìn)行Class
文件的動態(tài)連接哪亿,只有在運(yùn)行時才會進(jìn)行具體的Class
文件的解析操作
常量池表結(jié)構(gòu)
上文兩大類的常量池類型細(xì)分之后,到JDK1.7之后增加到了14種贤笆。之所以說常量池是最復(fù)雜的結(jié)構(gòu)蝇棉,就是因為這14種不同的類型都有不同的表結(jié)構(gòu),下面我們來簡單看下這14種結(jié)構(gòu)
每種常量類型的起始位都有一個u1
類型的tag標(biāo)識符苏潜,用于標(biāo)識當(dāng)前的常量類型
訪問標(biāo)志
訪問標(biāo)志用于識別類或接口層面的信息银萍,標(biāo)識是否為public
,abstract
恤左,final
贴唇,注解
,枚舉
等
類索引 父類索引 接口索引
類索引和父類索引都是一個u2
類型的數(shù)據(jù)飞袋,而接口索引則是一組u2
類型的集合戳气,所以接口索引入口的第一項為一個u2
類型的計數(shù),表示有幾個接口索引
類索引和接口索引的具體值是一個u2
的數(shù)據(jù)項巧鸭,并且指向一個CONSTANT_Class_info
常量池表類型在常量池中的偏移量
字段表集合
字段表用于描述接口或類中聲明的變量瓶您,包括類級變量和實際級變量,不包括在方法內(nèi)部聲明的局部變量
包含的信息主要有這幾種: 字段作用域(private,protact,public)
纲仍,是否static修飾
呀袱,可變性
,volatile 修飾
郑叠,可否被序列化
夜赵,字段類型(基本類型,引用乡革,數(shù)組)
寇僧,字段名稱
字段表結(jié)構(gòu)
類型 | 名稱 | 數(shù)量 |
---|---|---|
u2 | access_flas | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | 屬性表 | attributes_count |
access_flas
我們來看下字段access_flas
訪問標(biāo)識可選的類型
標(biāo)志名稱 | 標(biāo)志值 | 含義 |
---|---|---|
ACC_PUBLIC | 0x0001 | 字段是否public |
ACC_PRIVARE | 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 | 字段由編譯器自動產(chǎn)生 |
ACC_ENUM | 0x4000 | 字段是否為枚舉類型 |
看個例子摊腋,如果access_flas
為0x0019
,則標(biāo)識了ACC_PUBLIC
嘁傀,ACC_STATIC
兴蒸,ACC_FINAL
三種類型
name_index和descriptor_index
跟在access_flas
之后是name_index(簡單名稱)
和descriptor_index(描述符)
。包括之前出現(xiàn)的全限定名
细办,這里解釋一下這幾個名稱橙凳。全限定名稱一般來說是指org/xxx/TestClass
這種類型的名稱,可以理解為類的路徑蟹腾,簡單名稱就更加容易理解了痕惋,例如方法inc()
的簡單名稱值的就是inc
描述符相對于上面的兩種稍復(fù)雜一些,描述符的作用是用來描述字段的數(shù)據(jù)類型娃殖,方法的參數(shù)列表和返回值值戳,根據(jù)描述符規(guī)則,基本數(shù)據(jù)類型(byte,char,int,long,float,double,short,boolean)以及代表無返回值的void
類型都用一個大寫字符來表示炉爆,而對象類型則用L
加對象的全限定名來表示
具體的列在了下表中
標(biāo)識字符 | 含義 |
---|---|
B | byte |
C | char |
D | double |
F | float |
I | int |
J | long |
S | short |
Z | boolean |
V | void |
L | 對象類型 如 Ljava/lang/Object |
對于數(shù)組類型堕虹,每一緯度使用一個前置的[
來表示,如定義一個java.lang.String []
的數(shù)組類型芬首,將被記錄為[[Ljava/lang/String;
用描述符來描述方法時赴捞,按照先參數(shù)列表后返回值的順序描述,參數(shù)列表嚴(yán)格按照順序放在()
內(nèi)郁稍,如方法void inc()
的描述符為()V
赦政,方法int inc(int i, double)
的描述符為(ID)I
在描述符之后,緊跟著是一個屬性表集合耀怜,屬性表集合可以為空恢着,
方法表
方法表的組成與屬性表的組成是完全一致的,訪問標(biāo)識符的取值略有不同
標(biāo)志名稱 | 標(biāo)志值 | 含義 |
---|---|---|
ACC_PUBLIC | 0x0001 | 方法是否public |
ACC_PRIVARE | 0x0002 | 方法是否private |
ACC_PROTECTED | 0x0004 | 方法是否protected |
ACC_STATIC | 0x0008 | 方法是否static |
ACC_FINAL | 0x0010 | 方法是否final |
ACC_SYNCHRONIZED | 0x0020 | 方法是否同步 |
ACC_BRIDGE | 0x0040 | 方法是否由編譯器產(chǎn)生的橋接方法 |
ACC_VARARGS | 0x0080 | 方法是否接受不定蠶食 |
ACC_NATIVE | 0x0100 | 方法是否為native |
ACC_ABSTRACT | 0x0400 | 方法是否為abstract |
ACC_STRICTFP | 0x0800 | 方法是否為strictfp |
SYNTHETIC | 0x1000 | 方法由編譯器自動產(chǎn)生 |
那么這里大家可能會有疑問财破,方法里的java代碼去哪了呢掰派? 答案就是在方法表的屬性表集合中,有一個code
屬性左痢,那里存放了編譯成字節(jié)碼的Java代碼靡羡。對于屬性表,在下文會提到
屬性表
屬性表俊性,前文已經(jīng)提到了多次略步。包括Class
文件本身,方法表
定页,字段表
都有攜帶自己的屬性表集合纳像,用于描述專有場景信息
并且屬性表與Class
文件其他數(shù)據(jù)項要求不同,各個屬性不要求嚴(yán)格的順序拯勉,并且只要不與已有屬性名重復(fù)竟趾,任何人實現(xiàn)的編譯器都可以向?qū)傩员碇袑懭胱约憾x的屬性信息
下面我們來看幾個關(guān)鍵屬性
code 屬性
java程序方法體中的代碼javac
編譯器處理后,最終變?yōu)樽止?jié)碼子令存儲在Code
屬性內(nèi)
Exceptions 屬性
列舉方法可能會拋出的異常
LineNumberTable 屬性
描述java源碼行數(shù)和字節(jié)碼行數(shù)的對應(yīng)關(guān)系宫峦,前者是字節(jié)碼行岔帽,后者是源碼行
LocalVariableTable 屬性
描述棧局部變量和源碼中定義的變量的關(guān)系.這項是可選的,可使用javac -g:none
或javac -g:vars
命令關(guān)閉生成這項信息
SourceFile 屬性
用于記錄Class源碼文件的文件名稱导绷,這個屬性是可選的犀勒。可使用javac -g:none
或javac -g:source
命令關(guān)閉生成這項信息
ConstantValue 屬性
通知虛擬機(jī)為靜態(tài)變量賦值
InnerClasses 屬性
用于記錄內(nèi)部類與宿主類之間的關(guān)系
Deprecated Synthetic 屬性
Deprecated
標(biāo)識某個類妥曲,字段或方法過期
Synthetic
標(biāo)識此字段不由Java源碼直接產(chǎn)生贾费,由編譯器自動添加
StackMapTable 屬性
這個屬性會在虛擬機(jī)加載完字節(jié)碼后的驗證階段被使用
Signature 屬性
Signature
在JDK1.5
之后被添加,用于記錄泛型簽名信息檐盟。之所以要用這么一個屬性去記錄泛型信息褂萧,是因為Java語言的泛型采用的是擦除法實現(xiàn)的偽泛型,在Code
屬性中葵萎,泛型信息在編譯之后統(tǒng)統(tǒng)都被擦除掉了导犹。使用的擦除法的原因是這樣子實現(xiàn)比較簡單,只需要修改javac
編譯器就可以實現(xiàn)了羡忘,運(yùn)行時也可以節(jié)省一些空間谎痢。壞處就是運(yùn)行時無法拿到泛型信息。Signature
就是為了彌補(bǔ)這個缺陷而設(shè)置的卷雕,現(xiàn)在的Java API反射能夠獲取到的泛型信息也來自這個屬性
BootStrapMathods 屬性
BootStrapMathods
是JDK 1.7之后增加到規(guī)范中的节猿,這個屬性用于保存invokedynamic
指令引用的引導(dǎo)方法限定符。本篇文章暫不贅述這個指令