防偽查詢網(wǎng)站吸引人的微信軟文
Java 提供了一種可以在所有平臺上都能使用的一種中間代碼---字節(jié)碼文件(.class文件)。有了字節(jié)碼,無論是那個平臺只要安裝了虛擬機都可以直接運行字節(jié)碼文件。有了虛擬機,解除了 java 虛擬機與 java 代碼之間的耦合。
Java 虛擬機當(dāng)初被設(shè)計出來時就不單單只運行 java 這一種語言,目前 java 虛擬機已經(jīng)可以支持很多除 java 語言以外的其它語言了,比如 Groovy, JRuby, json, skilla等。之所以可以支持其它語言,是因為這些語言經(jīng)過編譯之后,也可以生成能夠被 JVM 解析并執(zhí)行的字節(jié)碼文件。而虛擬機并不關(guān)心字節(jié)碼是由哪一種語言編譯而來,如下圖所示:
class 文件
從縱觀的角度看,class 文件里只有兩種數(shù)據(jù)結(jié)構(gòu):無符號數(shù)和表。
?無符號數(shù):屬于基本的數(shù)據(jù)類型。以 u1,u2,u4,u8來分別代表 1 個字節(jié)、2個字節(jié)、4個字節(jié)和8個字節(jié)的無符號數(shù)。無符號數(shù)可以用來描述數(shù)字、索引引用,數(shù)量值或者字符串(UTF-8編碼)。
?表:表由多個無符號數(shù)或者其它表作為數(shù)據(jù)項構(gòu)成的復(fù)合數(shù)據(jù)類型。class 文件中所有的表都以“_info”結(jié)尾。整個 class 文件本質(zhì)上就是一張表。
表和無符號數(shù)之間的關(guān)心
class 文件結(jié)構(gòu)
無符號數(shù)和表組成了 class 中的各個結(jié)構(gòu),這些結(jié)構(gòu)按照預(yù)先規(guī)定好的順序緊密的從前向后排列,相鄰的項之間沒有任何間隙。當(dāng) JVM 加載某個 class 文件時,JVM 就是根據(jù)上圖中的結(jié)構(gòu)去解析 class 文件。加載 class 文件到內(nèi)存中,并在內(nèi)存中分配相應(yīng)的內(nèi)存空間,具體某種結(jié)構(gòu)需要占用多大的空間,可以參考如下圖:
實例解析:
把 test.java 編譯成 test.class 文件。用十六進制編輯器打開 .class 文件(可以用在線的編輯器 HexD.it)。
package software_test;import java.io.Serializable;public class test implements Serializable, Cloneable {private int num = 1;public int add(int i) {int j = 10;num = num + i + j;return num;}}
打開 test.class 文件的內(nèi)容
1. 魔數(shù)(magic number)
在 class 文件開頭的四個字節(jié)是 class 文件的魔數(shù),它是一個固定的值--0XCAFEBABE。魔數(shù)是 class 文件的標(biāo)志,它是判斷一個文件是不是 class 格式文件的標(biāo)準(zhǔn)。
2. 版本號
前兩個字節(jié) 0000代表次版本號(minor_version),后兩個字節(jié) 0034 是主版本號(major_version),對應(yīng)的十進制值為52,當(dāng)前 class 文件的主版本號為52,次版本號為0,所以綜合版本號是52.0,也就是 jdk1.8.0。
3. 常量池
緊跟在版本號之后的是一個叫做常量池的表(cp_info)。在常量池中保存了類的各種相關(guān)信息,比如類的名稱、父類的名稱、類中的方法名、參數(shù)名稱、參數(shù)類型等。
常量池中的每一項都是一個表,其項目類型共有14種。常量池中的每一項都會有一個 u1 大小的 tag 值,是表的標(biāo)識。
JVM 解析 class 文件時,通過 tag 值來判斷當(dāng)前數(shù)據(jù)結(jié)構(gòu)是哪一個表。例如,CONSTANT_Class_info 表:
tag:占用一個字節(jié)大小,值為為7,說明是?CONSTANT_Class_info 類型表。
name_index:是一個索引值,可以將它理解為一個指針,指向常量池中索引為 name_index 的常量表。比如 name_index = 7,則它指向常量池中第7個常量表(表與表之間是有關(guān)聯(lián)的)。
再例如 CONSTANT_Utf8_info 表:
tag:值為1,表示是 CONSTANT_Utf8_info類型表。
length:表示 u1[]的長度,比如length = 5,則表示接下來的數(shù)據(jù)是 5 個連續(xù)的u1類型數(shù)據(jù)。
bytes: u1 類型數(shù)組,長度為上面第2個參數(shù) length 的值。
面試題:Java 源文件中 String 字符串的長度?有限制嗎?
有(字符串存儲在Class文件的常量池中)。在 Java 代碼中聲明的 String 字符串最終在 class 文件中的存儲格式是 CONSTANT_utf8_info因此一個字符串最大長度也就是 u2 所能代表的最大值 65536(2^16) 個,但是需要使用2個來保存null值,因此一個字符串的最大長度為 65536 - 2 = 65534。
class 文件在常量池的前面使用2個字節(jié)的容量計數(shù)器,用來代表當(dāng)前類中常量池的大小。
001D 轉(zhuǎn)化為十進制為29,即常量計數(shù)器的值為29。其中下標(biāo)為0的常量被 JVM 留作其他特殊用途,因此 Test.class 中時間的常量池大小為這個計數(shù)器的值減1,也就是28個。
第一個常量:
0A 轉(zhuǎn)化為十進制為10,通過查看常量池14種表格圖,可以查到 tag=10的表類型為 CONSTANT_Methodref_info,因此常量池中的第一個常量類型為類的方法引用表。其結(jié)構(gòu)如下:
也就是說,0A之后的兩個直接是指向該方法所屬類,再緊跟的兩個字節(jié)指向此方法的名稱和類型。
0006:十進制為6,表示指向常量池中的第6個常量;
000F:十進制為15,表示指向常量池中的第 15 個常量。
至此,第一個常量解讀完畢!
第二個常量:
09轉(zhuǎn)化為十進制為9,即tag = 9,表示是字段引用表 CONSTANT_Fieldref_info,其結(jié)構(gòu)如下:
同理,
0010:指向常量池中第 16 個常量,0011指向常量池中第17個常量。
至此,我們已經(jīng)解析了常量池中的2個常量,剩下的26個常量也是如此。
4. 訪問標(biāo)志
緊跟在常量池之后的常量是訪問標(biāo)志,占用兩個字節(jié)。訪問標(biāo)志代表類或者接口的訪問信息。比如:該 class 文件是類還是接口,是否被定義成 public,是否是 abstract,如果是類,是否被聲明成 final 等。各種訪問標(biāo)志如下圖所示:
我們定義的 test.java 是一個普通 Java 類,不是接口、枚舉或注解,并且被 public 修飾但沒有被聲明為 final 和 abstract,因此它所對應(yīng)的 access_flags 為 0021(0X0001 和 0X0020相結(jié)合)。
5. 類索引、父類索引和接口索引計時器
訪問標(biāo)志后的2個字節(jié)就是類索引,類索引后的2個字節(jié)就是父類索引,父類索引后的2個字節(jié)則是接口索引計數(shù)器。如下圖所示:
?綜上所述,我們可以得出當(dāng)前類為 Test繼承自 Object 類,并實現(xiàn)了 “Serializable”和“Cloneable”這兩個接口。?
6. 字段表
緊跟在接口索引集合后面的就是字段表,字段表的主要功能是用來描述類或者接口中聲明的變量。這里的字段包含了類級別變量以及實例變量,但不包含方法中的局部變量。其具體結(jié)構(gòu)如下:
字段訪問標(biāo)志
其中,第7和8個常量就 num 和 i。因此可以得出,類中有一個為num,類型為 int? 的變量。
7. 方法表
字段之后跟著的就是方法表常量,方法表常量應(yīng)該也是以一個計數(shù)器開始的,因為一個類中的方法數(shù)量是不固定的。
上圖表示 test.class中有兩個方法,但是我們只在 test.java 中聲明了一個 add 方法,因為默認(rèn)構(gòu)造器方法也被包含在方法表常量中。方法表結(jié)構(gòu)如下:
訪問標(biāo)志
8. 屬性表
在之前解析字段和方法的時候,在它們的具體結(jié)構(gòu)中,都能看到有一個叫做 attributes_info 的表,這就是屬性表。屬性表沒有一個固定結(jié)構(gòu),各種不同的屬性只要滿足以下結(jié)構(gòu)即可:
?