大家好,由于最近被動態(tài)加載的知識卡住,而動態(tài)加載涉及到j(luò)ava虛擬機(jī)中的加載機(jī)制,因此我決定花一定的時(shí)間來學(xué)習(xí)java虛擬機(jī),特別是類加載部分,主要參照《深入理解java虛擬機(jī)》這本書進(jìn)行學(xué)習(xí),這本書的pdf版請前往鏈接獲取。
http://download.csdn.net/detail/hollow12384/9606072
今天是java虛擬機(jī)的第一門課程,主要講解的內(nèi)容如下:
(1)java虛擬機(jī)的語言無關(guān)性
(2)Class文件結(jié)構(gòu)概述
(3)解剖Class文件每個(gè)字節(jié)的含義
java面世時(shí)就提出一個(gè)口號:”一次編寫,到處運(yùn)行“,這句話表明了java的目標(biāo)是跨平臺運(yùn)行,這個(gè)目標(biāo)最終怎么實(shí)現(xiàn)呢?自然是通過操作系統(tǒng)的
應(yīng)用層實(shí)現(xiàn),而實(shí)現(xiàn)的方式就是虛擬機(jī)了。只要不同平臺的虛擬機(jī)和所有平臺都使用統(tǒng)一的程序存儲格式—–字節(jié)碼格式,就能達(dá)到跨平臺的目的。
這里就涉及到一個(gè)概念,什么是字節(jié)碼格式?
字節(jié)碼(英語:Bytecode)通常指的是已經(jīng)經(jīng)過編譯,但與特定機(jī)器碼無關(guān),需要直譯器轉(zhuǎn)譯后才能成為機(jī)器碼的中間代碼。字節(jié)碼通常不像源碼一樣可以讓人閱讀,而是編碼后的數(shù)值常量、引用、指令等構(gòu)成的序列。
字節(jié)碼主要為了實(shí)現(xiàn)特定軟件運(yùn)行和軟件環(huán)境、與硬件環(huán)境無關(guān)。字節(jié)碼的實(shí)現(xiàn)方式是通過編譯器和虛擬機(jī)器。編譯器將源碼編譯成字節(jié)碼,特定平臺上的虛擬機(jī)器將字節(jié)碼轉(zhuǎn)譯為可以直接執(zhí)行的指令。字節(jié)碼的典型應(yīng)用為Java bytecode。
通俗的說,字節(jié)碼就是源碼經(jīng)過編譯器后出來的東西。
字節(jié)碼本質(zhì)上就是二進(jìn)制碼。
不過,java的強(qiáng)大之處不止在于跨平臺,java不僅具有平臺無關(guān)性,而且具有很強(qiáng)的語言無關(guān)性。什么意思呢?
不管是什么語言,只要對應(yīng)的編譯器能夠?qū)⒊绦虼a編譯成Class文件,java虛擬機(jī)并不在乎到底Class文件是由什么得來的,只要它符合Class文件結(jié)構(gòu)的要求,就能在java虛擬機(jī)中運(yùn)行。
前面我們已經(jīng)介紹了,java虛擬機(jī)運(yùn)行的是Class文件,那么Class文件到底是什么呢?Class文件的結(jié)構(gòu)又該符合什么要求呢?下面講解的就是這個(gè)內(nèi)容。
首先明確一點(diǎn),Class文件是一組以8個(gè)字節(jié)為基礎(chǔ)單位的二進(jìn)制流。Class文件流中沒有分割符,這使得Class文件儲存的數(shù)據(jù)幾乎都是有效的關(guān)鍵的數(shù)據(jù)。有人可能會問,如果儲存的數(shù)據(jù)需要占用8個(gè)字節(jié)以上的空間怎么辦呢?這個(gè)時(shí)候就按照高位在前的方式,分割成若干個(gè)8個(gè)字節(jié)進(jìn)行儲存。
那具體應(yīng)該怎么儲存呢?Class文件格式采用了類似于C語言結(jié)構(gòu)體的偽結(jié)構(gòu)進(jìn)行儲存。這種結(jié)構(gòu)只有兩種數(shù)據(jù)類型:無符號數(shù)和表。后面關(guān)于Class文件的結(jié)構(gòu)解析都要建立在這兩種數(shù)據(jù)類型上,因此先講解這兩種數(shù)據(jù)類型。
無符號數(shù)屬于基本的數(shù)據(jù)類型,以u1,u2,u4,u8分別代表1個(gè)字節(jié),2個(gè)字節(jié),4個(gè)字節(jié)和8個(gè)字節(jié)的無符號數(shù),無符號數(shù)可以用來描述數(shù)字,索引引用,數(shù)量值,或者按照utf-8編碼構(gòu)成字符串值。
表是由多個(gè)無符號數(shù)或其他表作為數(shù)據(jù)項(xiàng)構(gòu)成的復(fù)合數(shù)據(jù)類型。所有的表都習(xí)慣性的用_info結(jié)尾,因此可以用這個(gè)來區(qū)分表和無符號數(shù)。整個(gè)Class本質(zhì)上就是一張表,它由以下的數(shù)據(jù)項(xiàng)構(gòu)成:
無論是無符號數(shù)還是表,如果需要描述同一個(gè)類型并且數(shù)據(jù)量不多,經(jīng)常會使用一個(gè)前置的容量計(jì)數(shù)器加上若干個(gè)數(shù)據(jù)項(xiàng)的形式,這個(gè)時(shí)候稱這一系列連續(xù)的某個(gè)類型的數(shù)據(jù)為某一類型的集合。
第二點(diǎn)說明了Class文件采用無符號數(shù)和表來儲存數(shù)據(jù),但是具體怎么儲存呢?儲存的規(guī)則是什么呢?
在正式講解之前,建議大家先下載安裝一個(gè)軟件WinHex,我們將使用這個(gè)軟件來將.class字節(jié)碼文件轉(zhuǎn)化為16進(jìn)制數(shù)進(jìn)行解析。
接下來先對Class文件解析的規(guī)則進(jìn)行解釋:
整個(gè).class文件轉(zhuǎn)化為16進(jìn)制后就是按照上述這張表進(jìn)行解析。
接下來就耐心地對表里的每一項(xiàng)進(jìn)行詳細(xì)解釋。
在解釋之前,由于我比較傾向于用實(shí)例解釋,因此我用一個(gè)實(shí)在的java項(xiàng)目生成的.class文件進(jìn)行解析。
當(dāng)然,這個(gè)java項(xiàng)目十分簡單,只有一個(gè)類,
這個(gè)類位于這個(gè)包內(nèi):
到這個(gè)包所在的目錄,打開bin,在里面就可以找到.class文件(注意,我們是對.class文件解析而不是.java文件,還記得嗎?進(jìn)入java虛擬機(jī)的是.class文件而不是.java文件)
然后用winHex打開這個(gè).class文件,就可以看到.class文件的16進(jìn)制表示了。
好了,做足了準(zhǔn)備工作,就可以開始解析了。
ps:由于篇幅較長,因此我將這部分分為兩篇博客,這篇博客講解的前四個(gè)屬性,剩余的屬性見JAVA虛擬機(jī)入門(1)———-類文件結(jié)構(gòu)(下)
Class文件的前4個(gè)字節(jié)稱為魔數(shù),是用于確定這個(gè)文件是否是可以被java虛擬機(jī)接受的.class文件,為啥不用后綴名來判斷呢?當(dāng)然是因?yàn)?/p>
后綴名實(shí)在是太容易改了,而文件格式制定者只要采用標(biāo)示文件格式的魔數(shù),并且魔數(shù)沒有被其他人采用,那就可以起到標(biāo)示的作用了。Class文件的魔數(shù)是
CAFE BABE(咖啡寶貝),是否特別好記?看我們解析出來的16機(jī)制文件前四個(gè)字節(jié),正是CAFE BABE!
魔數(shù)后面緊跟著的就是版本號了,包括次版本號和主版本號,各占2個(gè)字節(jié)。在我們的例子中就是0000和0034,0000說明此版本號為0,0034說明主版本號為52,因此編譯器jdk的版本就是52.0。
常量池是Class文件中出現(xiàn)的第一個(gè)表類型數(shù)據(jù)類型,而且占據(jù)著Class文件的最大空間,同時(shí)與其他項(xiàng)目的關(guān)聯(lián)最多。因此,常量池是一個(gè)比較復(fù)雜并且重要的內(nèi)容。
大家也可能注意到,在介紹常量池的時(shí)候,我用了“不定個(gè)字節(jié)”,說明常量池的字節(jié)長度是不確定的。那怎么確定常量池到底是到哪里呢?這取決于常量池
的前兩個(gè)字節(jié)u2,在這個(gè)例子中也就是0016,這表示一共有21個(gè)常量,為什么不是22呢(0016的十進(jìn)制表示就是22)?因?yàn)榈谝粋€(gè)字節(jié)是空出來
的,用于后面指向常量池的索引數(shù)據(jù)在特定情況下表達(dá)“不引用任何一個(gè)常量池項(xiàng)目”的意思,這種情況下將索引值置為0(也就是常量池第一個(gè)字節(jié))就行了。
常量池中的常量主要包括兩大類:Leteral和符號引用(Symbolic Reference)。Literal類似于java描述的常量,如文本字符串,final修飾的類型。符號引用主要包括三類常量:
(1)類和接口的全限定名
(2)字段的名稱和限定符
(3)方法的名稱和限定符
關(guān)于類加載的知識在下節(jié)解析。
這里拋出一個(gè)問題:為什么要使用符號引用?
回到對16進(jìn)制.class文件的解析,常量池中的每一個(gè)常量都是一個(gè)表,一共有11種表結(jié)構(gòu),他們具有共同的特征,就是第一個(gè)字節(jié)(u1)表示的是這種表的類型,接下來的字節(jié)根據(jù)他們各自的類型進(jìn)行解析。主要參考下面的表。
還是舉我們的例子,第9個(gè)字節(jié)和第10個(gè)字節(jié)(0016)表示常量池一共有21個(gè)常量,第一個(gè)常量的標(biāo)志位是07,根據(jù)上面的表得知為
CONSTANT_Class_Info(記得之前說過嗎,以info結(jié)尾的一般就是表結(jié)構(gòu)了),并且u2指定的是全限定名常量項(xiàng)的索引,在這個(gè)例子中
u2是0002,說明全限定名常量項(xiàng)在第二個(gè)常量中。第二個(gè)常量的tag是01,說明是CONSTANT_Utf8_info,u2是0011,說明
utf8字符串占據(jù)的字節(jié)數(shù)是17個(gè)字節(jié),也就是從6A一直到下一行的74,這17個(gè)字節(jié)代表的就是全限定名常量的名字。這里就涉及到怎么翻譯utf8縮
略編碼了。
從“\u0001”到”\u007f”之間的字符用一個(gè)字節(jié)表示,”\u0080”到”\u07ff”用兩個(gè)字節(jié)表示,從”\u0800”到”\uffff”用三個(gè)字節(jié)表示。
在WinHex中,當(dāng)你選中這17個(gè)字節(jié)時(shí),旁邊會自動顯示相應(yīng)的圖形,在這個(gè)例子中顯示的是”javaLearning/test“,正是這個(gè)項(xiàng)目的完整名字!再接下去的解析也是一樣的,就不多說了。
不過這里要重點(diǎn)說明一下,由于Class的方法,字段等都需要引用到01,也即是CONSTANT_Utf8_info,因此
CONSTANT_Utf8_info的數(shù)量直接決定了java中方法和字段的數(shù)量,CONSTANT_Utf8_info指明長度的是length字
段,長度為u2,也就是16個(gè)bit,共有65535種可能排序,因此方法數(shù)的瓶頸就是65535了!如果方法數(shù)超過了這個(gè)數(shù),那么將導(dǎo)致無法編譯。
常量池21個(gè)常量過后就是訪問標(biāo)志了,在這個(gè)例子中是在Offset為000000D0這一行的76 61過后的兩個(gè)字節(jié),也就是00 21。首先先來看訪問標(biāo)志都有什么,以及每個(gè)對應(yīng)的16進(jìn)制數(shù)是什么?
注意表中的每個(gè)屬性不一定與其他屬性相斥,比如ACC_PUBLIC和ACC_FINAL,一個(gè)類既可以聲明為public,也可以同時(shí)聲明為final,這個(gè)時(shí)候的標(biāo)志值就是兩者的或運(yùn)算了(0001 | 0010)。
回歸到我們的例子中,我們的例子只有聲明了public,那是不是就是0001呢?請注意,ACC_SUPER說明了,jdk1.2以后編譯出來的
類都會有這個(gè)標(biāo)志,而根據(jù)前面我們讀到的jdk版本號,肯定大于1.2,因此絕對是帶有這個(gè)標(biāo)志的。所以真正的標(biāo)志位為0001 | 0020 =
0021,和我們讀出來的結(jié)果相符合了。
好了,這就是類文件結(jié)構(gòu)最基本的前面幾個(gè)字節(jié)碼了,感興趣的各位可以看JAVA虛擬機(jī)入門(1)——-類文件結(jié)構(gòu)(下)???