從字節(jié)碼角度剖析Java類文件

1 Java類文件簡析

所謂 Java 類文件,就是通常用 javac 編譯器產(chǎn)生的 .class 文件。這些文件具有嚴(yán)格定義的格式。Java 源文件經(jīng)過 javac 編譯器編譯之后,將會生成對應(yīng)的二進制文件。

Java 能夠?qū)崿F(xiàn)"一次編譯,到處運行”,靠的是 class 文件的功勞。無論是哪種平臺(如:Mac、Windows、Linux 等),只要安裝了虛擬機都可以直接運行字節(jié)碼。

有了字節(jié)碼,也就解除了 Java 虛擬機和 Java 語言之間的耦合。目前 Java 虛擬機已經(jīng)可以支持很多除 Java 語言以外的其他語言了,如 Groovy、JRuby、Jython、Scala 等,因為這些語言經(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ù)和其他表格。

2 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)的空間。具體某一種結(jié)構(gòu)需要占用大多空間,可以參考下圖:

3 實例分析

接下來通過一個 Java 代碼實例來詳細(xì)分析下class 文件結(jié)構(gòu),Test.java代碼如下:

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;
           return num;
      }
}

經(jīng)過 javac 編譯后,得到的類文件Test.class,用vim查看HelloWorld.class

vim HelloWorld.class

打開文件后輸入

:%!xxd

按回車即可看到如下一串串十六進制符號


該文件中是由十六進制符號組成的,每兩個字符代表一個字節(jié),這一段十六進制符號組成的長串是嚴(yán)格遵守 Java 虛擬機規(guī)范。接下來就一步一步看下JVM是如何解析它們的.

魔數(shù) magic number

在class文件開頭的四個字節(jié)是class文件的魔數(shù),它是一個固定的值--0XCAFEBABE。魔數(shù)是class文件的標(biāo)志,也就是說它是判斷一個文件是不是class格式文件的標(biāo)準(zhǔn),如果開頭四個字節(jié)不是 0XCAFEBABE, 那么就說明它不是 class 文件, 不能被 JVM 識別或加載。

版本號

緊跟在魔數(shù)后面的兩個字節(jié)代表當(dāng)前 class 文件的版本號。前兩個字節(jié) 0000 代表次版本號(minor_version),后兩個字節(jié) 0034 是主版本號(major_version),對應(yīng)的十進制值為 52,也就是說當(dāng)前 class 文件的主版本號為 52,次版本號為 0。所以綜合版本號是 52.0,也就是 jdk1.8.0。

常量池
緊跟在版本號之后的是一個叫作常量池的表(cp_info)。在常量池中保存了類的各種相關(guān)信息,比如類的名稱、父類的名稱、類中的方法名、參數(shù)名稱、參數(shù)類型等,這些信息都是以各種表的形式保存在常量池中的。

常量池中的每一項都是一個表,其項目類型共有 14 種,如下表所示:


可以看出,常量池中的每一項都會有一個u1大小的tag值。tag值是表的標(biāo)識,JVM解析class文件時,通過這個值來判斷當(dāng)前數(shù)據(jù)結(jié)構(gòu)是哪一種表。以上14種表都有自己的結(jié)構(gòu),我們就以 CONSTANT_Class_info 和 CONSTANT_Utf8_info 這兩張表舉例說明,因為其他表也基本類似。

首先,CONSTANT_Class_info 表具體結(jié)構(gòu)如下所示:

table CONSTANT_Class_info {
    u1  tag = 7;
    u2  name_index;
}
  • tag:占用一個字節(jié)大小。值為 7,查看上面標(biāo)識位為7的表是CONSTANT_Class_info 類型表。
  • name_index:是一個索引值,可以將它理解為一個指針,指向常量池中索引為 name_index 的常量表。比如 name_index = 2,則它指向常量池中第 2 個常量。

接下來再看 CONSTANT_Utf8_info 表具體結(jié)構(gòu)如下:

table CONSTANT_utf8_info {
    u1  tag;
    u2  length;
    u1[] bytes;
}
  • tag:值為1,表示是 CONSTANT_Utf8_info 類型表。
  • length:length 表示 u1[] 的長度,比如 length=5,則表示接下來的數(shù)據(jù)是 5 個連續(xù)的 u1 類型數(shù)據(jù)。
  • bytes:u1 類型數(shù)組,長度為上面第 2 個參數(shù) length 的值。

引申:String長度最大是多少
在java代碼中聲明的String字符串最終在class文件中的存儲格式就 CONSTANT_utf8_info。因此一個字符串最大長度也就是u2所能代表的最大值65536個,但是需要使用2個字節(jié)來保存 null 值,因此一個字符串的最大長度為 65536 - 2 = 65534(字符串最大長度為65534個字節(jié),并不代表一個字符串中就可以保存65534個字符。因為在utf-8編碼下,一個數(shù)字和一個英文字母占一個字節(jié),但是一個漢字卻可以占用2~4個字節(jié)。因此如果使用字面量的方式聲明中文字符串的長度會遠(yuǎn)遠(yuǎn)小于65534。),這種String長度的限制是編譯期的限制。

那么運行時的最大長度是多少呢?
String內(nèi)部是以char數(shù)組的 value 存儲的,數(shù)組的長度是int類型的 count,那么String允許的最大長度就是Integer.MAX_VALUE(2147483647) 了。java中一個char占2個字節(jié),也就是16位。String的運行時最大占用空間計算公式如下:


運行時大概需要約4GB的內(nèi)存才能存儲最大長度的字符串。


在常量池內(nèi)部的表中也有相互之間的引用。用一張圖來理解 CONSTANT_Class_info 和 CONSTANT_utf8_info 表格之間的關(guān)系,如下圖所示:


理解了常量池內(nèi)部的數(shù)據(jù)結(jié)構(gòu)之后,接下來就看一下實例代碼的解析過程。因為開發(fā)者平時定義的 Java 類各式各樣,類中的方法與參數(shù)也不盡相同。因為開發(fā)者平時定義的Java類各式各樣,類中的方法與參數(shù)也不盡相同。所以常量池的元素數(shù)量也就無法固定,因此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進制后為10,通過查看常量池14種表格圖中,可以查到tag=10的表類型為CONSTANT_Methodref_info,因此常量池中的第一個常量類型為方法引用表。其結(jié)構(gòu)如下:

CONSTANT_Methodref_info {
    u1 tag = 10;
    u2 class_index;        指向此方法的所屬類
    u2 name_type_index;    指向此方法的名稱和類型
}

可以看到 class_index 與 name_type_index 都是 u2 類型的無符號數(shù),也就是說在“0a”之后的 2 個字節(jié)指向這個方法是屬于哪個類,緊接的 2 個字節(jié)指向這個方法的名稱和類型。它們的值分別是:

  • 0006:十進制 6,表示指向常量池中的第 6 個常量。
  • 0015:十進制 21,表示指向常量池中的第 21 個常量。

至此,第 1 個常量就解讀完畢了。緊接著的就是第 2 個常量,如下所示:


tag 09 表示是字段引用表 CONSTANT_FIeldref_info ,其結(jié)構(gòu)如下:

CONSTANT_Fieldref_info{
    u1 tag;
    u2 class_index;        指向此字段的所屬類
    u2 name_type_index;    指向此字段的名稱和類型
}

同樣也是 4 個字節(jié),前后都是兩個索引。

  • 0005:指向常量池中第 5 個常量。
  • 0016:指向常量池中第 22 個常量。

到現(xiàn)在為止我們已經(jīng)解析出了常量池中的兩個常量。剩下的 21 個常量的解析過程也大同小異,這里就不一一解析了。實際上我們可以借助 javap 命令來幫助我們查看 class 常量池中的內(nèi)容:

javap -v Test.class

上述命令執(zhí)行后,顯示結(jié)果如下:


正如我們剛才分析的一樣,常量池中第一個常量是 Methodref 類型,指向下標(biāo) 6 和下標(biāo) 21 的常量。其中下標(biāo) 21 的常量類型為 NameAndType,它對應(yīng)的數(shù)據(jù)結(jié)構(gòu)如下:

CONSTANT_NameAndType_info{
    u1 tag;
    u2 name_index;    指向某字段或方法的名稱字符串
    u2 type_index;    指向某字段或方法的類型字符串
}

而下標(biāo)在21的NameAndType的name_index和type_index分別指向了13和14,也就是“<init>”和“()V”。因此最終解析下來常量池中第 1 個常量的解析過程以及最終值如下圖所示:


仔細(xì)解析層層引用,最后我們可以看出,Test.class 文件中常量池的第 1 個常量保存的是 Object 中的默認(rèn)構(gòu)造器方法。

訪問標(biāo)志(access_flags)

緊跟在常量池之后的常量是訪問標(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é)合)。

類索引、父類索引與接口索引計數(shù)器

在訪問標(biāo)志后的 2 個字節(jié)就是類索引,類索引后的 2 個字節(jié)就是父類索引,父類索引后的 2 個字節(jié)則是接口索引計數(shù)器。如下圖所示:


可以看出類索引指向常量池中的第 5 個常量,父類索引指向常量池中的第 6 個常量,并且實現(xiàn)的接口個數(shù)為 2 個。再回顧下常量池中的數(shù)據(jù):


從圖中可以看出,第5個常量和第6個常量均為CONSTANT_Class_info表類型,并且代表的類分別是“Test”和“Object”。再看接口計數(shù)器,因為接口計數(shù)器的值是2,代表這個類實現(xiàn)了 2 個接口。查看在接口計數(shù)器之后的 4 個字節(jié)分別為:

  • 0007:指向常量池中的第 7 個常量,從圖中可以看出第 7 個常量值為"Serializable"。
  • 0008:指向常量池中的第 8 個常量,從圖中可以看出第 8 個常量值為"Cloneable"。

綜上所述,可以得出如下結(jié)論:當(dāng)前類為 Test 繼承自 Object 類,并實現(xiàn)了“Serializable”和“Cloneable”這兩個接口。

字段表

緊跟在接口索引集合后面的就是字段表了,字段表的主要功能是用來描述類或者接口中聲明的變量。這里的字段包含了類級別變量以及實例變量,但是不包括方法內(nèi)部聲明的局部變量。

同樣, 一個類中的變量個數(shù)是不固定的,因此在字段表集合之前還是使用一個計數(shù)器來表示變量的個數(shù),如下所示:

0002 表示類中聲明了 2 個變量(在 class 文件中叫字段),字段計數(shù)器之后會緊跟著 2 個字段表的數(shù)據(jù)結(jié)構(gòu)。

字段表的具體結(jié)構(gòu)如下:

CONSTANT_Fieldref_info{
    u2  access_flags    字段的訪問標(biāo)志
    u2  name_index          字段的名稱索引(也就是變量名)
    u2  descriptor_index    字段的描述索引(也就是變量的類型)
    u2  attributes_count    屬性計數(shù)器
    attribute_info
}

繼續(xù)解析 Text.class 中的字段表,其結(jié)構(gòu)如下圖所示:


字段訪問標(biāo)志

對于 Java 類中的變量,也可以使用 public、private、final、static 等標(biāo)識符進行標(biāo)識。因此解析字段時,需要先判斷它的訪問標(biāo)志,字段的訪問標(biāo)志如下所示:


字段表結(jié)構(gòu)圖中的訪問標(biāo)志的值為0002,代表它是private類型。變量名索引指向常量池中的第9個常量,變量名類型索引指向常量池中第10個常量。第9和第10個常量分別為“num”和“I”,如下所示:


因此可以得知類中有一個名為 num,類型為 int 類型的變量。對于第 2 個變量的解析過程也是一樣,就不再贅復(fù)。

注意:

  • 字段表集合中不會列出從父類或者父接口中繼承而來的字段。
  • 內(nèi)部類中為了保持對外部類的訪問性,會自動添加指向外部類實例的字段。

方法表

字段表之后跟著的就是方法表常量。方法表常量應(yīng)該是以一個計數(shù)器開始的,因為一個類中的方法數(shù)量是不固定的,如圖所示:


上圖表示 Test.class 中有兩個方法,但是我們只在 Test.java 中聲明了一個 add 方法,這是為什么呢?這是因為默認(rèn)構(gòu)造器方法也被包含在方法表常量中。

方法表的結(jié)構(gòu)如下所示:

CONSTANT_Methodref_info{
    u2  access_flags;        方法的訪問標(biāo)志
    u2  name_index;          指向方法名的索引
    u2  descriptor_index;    指向方法類型的索引
    u2  attributes_count;    方法屬性計數(shù)器
    attribute_info attributes;
}

可以看到,方法也是有自己的訪問標(biāo)志,具體如下:


我們主要來看下 add 方法,具體如下:

從圖中我們可以看出 add 方法的以下字段的具體值:

  • access_flags = 0X0001 也就是訪問權(quán)限為 public。
  • name_index = 0X0011 指向常量池中的第 17 個常量,也就是“add”。
  • type_index = 0X0012 指向常量池中的第 18 個常量,也即是 (I)。這個方法接收 int 類型參數(shù),并返回 int 類型參數(shù)。

屬性表

在之前解析字段和方法的時候,在它們的具體結(jié)構(gòu)中我們都能看到有一個叫作 attributes_info 的表,這就是屬性表。

屬性表并沒有一個固定的結(jié)構(gòu),各種不同的屬性只要滿足以下結(jié)構(gòu)即可:

CONSTANT_Attribute_info{
    u2 name_index;
    u2 attribute_length length;
    u1[] info;
}

JVM 中預(yù)定義了很多屬性表,這里重點講一下 Code 屬性表。

我們可以接著剛才解析方法表的思路繼續(xù)往下分析:


可以看到,在方法類型索引之后跟著的就是“add”方法的屬性。0X0001 是屬性計數(shù)器,代表只有一個屬性。
0X000f 是屬性表類型索引,通過查看常量池可以看出它是一個 Code 屬性表,如下所示:


Code 屬性表中,最主要的就是一些列的字節(jié)碼。通過 javap -v Test.class 之后,可以看到方法的字節(jié)碼,如下圖顯示的是 add 方法的字節(jié)碼指令:

JVM 執(zhí)行 add 方法時,就通過這一系列指令來做相應(yīng)的操作。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容