Class字節碼文件


Java文件經過編譯后生產Class字節碼文件。JVM時通過字節碼來執行。對於程序員來說對class的機制熟悉很重要。

1. Class 文件的組成

 

上圖的class文件可以用下圖來表達,U4便是4個無符號字節

 

 

Class文件結構的解析:

1. 魔術:

所有的由Java編譯器編譯而成的class文件的前4個字節都是“0xCAFEBABE” (諧音咖啡寶貝)。 它的作用在於:當JVM在嘗試加載某個文件到內存中來的時候,會首先判斷此class文件有沒有JVM認為 可以接受的“簽名”,即JVM會首先讀取文件的前4個字節,判斷該4個字節是否是“0xCAFEBABE”,如 果是,則JVM會認為可以將此文件當作class文件來加載並使用。

2. 版本號

版本號分為次版本號和主版本號。主版本號和次版本號在class文件中各占兩個字節,副版本號占用第5、6兩個字節,而主版本號則占用 第7,8兩個字節。

JDK1.0 主版本號為45, 1.1 為46, 依次類推,到JDK8的版本號為52, 16進制為0x33.

 一個 JVM實例只能支持特定范圍內的主版本號 (Mi 至Mj) 和 0 至特定范圍內 (0 至 m) 的副版 本號。假設一個 Class 文件的格式版本號為 V, 僅當Mi.0 ≤ v ≤ Mj.m成立時,這個 Class 文件 才可以被此 Java 虛擬機支持。不同版本的 Java 虛擬機實現支持的版本號也不同,高版本號的 Java 虛擬機實現可以支持低版本號的 Class 文件,反之則不成立。 JVM在加載class文件的時候,會讀取出主版本號,然后比較這個class文件的主版本號和JVM本身的版 本號,如果JVM本身的版本號 < class文件的版本號,JVM會認為加載不了這個class文件,會拋出我 們經常見到的" java.lang.UnsupportedClassVersionError: Bad version number in .class file " Error 錯誤;反之,JVM會認為可以加載此class文件,繼續加載此class文件。

 

3. 常量池計數器

常量池是由一組 constant_pool結構體數組組成的,而數組的大小則由常量池計數器指定。常量池計數器 constant_pool_count 的值 =constant_pool表中的成員數+ 1。

constant_pool表的索引值只有在 大於 0 且小於constant_pool_count時才會被認為是有效的。

注意事項: 常量池計數器默認從1開始而不是從0開始: 當constant_pool_count = 1時,常量池中的cp_info個數為0;當constant_pool_count為n時,常 量池中的cp_info個數為n-1。

原因: 在指定class文件規范的時候,將索引#0項常量空出來是有特殊考慮的,這樣當:某些數據在特定的情 況下想表達“不引用任何一個常量池項”的意思時,就可以將其引用的常量的索引值設置為#0來表示。

 

4. 常量池數據區

 

 

 5. 訪問標志

訪問標志,access_flags 是一種掩碼標志,用於表示某個類或者接口的訪問權限及基礎屬性。

 

 6. 類索引

類索引,this_class的值必須是對constant_pool表中項目的一個有效索引值。constant_pool表 在這個索引處的項必須為CONSTANT_Class_info 類型常量,表示這個 Class 文件所定義的類或接 口。 

 

7. 父類索引

父類索引,對於類來說,super_class 的值必須為 0 或者是對constant_pool 表中項目的一個有 效索引值。 如果它的值不為 0,那 constant_pool 表在這個索引處的項必須為CONSTANT_Class_info 類型常 量,表示這個 Class 文件所定義的類的直接父類。當前類的直接父類,以及它所有間接父類的 access_flag 中都不能帶有ACC_FINAL 標記。對於接口來說,它的Class文件的super_class項的 值必須是對constant_pool表中項目的一個有效索引值。constant_pool表在這個索引處的項必須為 代表 java.lang.Object 的 CONSTANT_Class_info 類型常量 。 如果 Class 文件的 super_class的值為 0,那這個Class文件只可能是定義的是 java.lang.Object類,只有它是唯一沒有父類的類。

 

8. 接口計算器

接口計數器,interfaces_count的值表示當前類或接口的【直接父接口數量】。

 

9. 接口信息數據區

接口表,interfaces[]數組中的每個成員的值必須是一個對constant_pool表中項目的一個有效索引值, 它的長度為 interfaces_count。每個成員interfaces[i] 必須為CONSTANT_Class_info類型常量,其中 【0 ≤ i <interfaces_count】。在interfaces[]數組中,成員所表示的接口順序和對應的源代碼中給定的接口順序(從左至右)一樣,即interfaces[0]對應的是源代碼中最左邊的接口。

 

10. 字段計數器

字段計數器,fields_count的值表示當前 Class 文件 fields[]數組的成員個數。 fields[]數組 中每一項都是一個field_info結構的數據項,它用於表示該類或接口聲明的【類字段】或者【實例字 段】。 

 

11. 字段信息數據區

字段表,fields[]數組中的每個成員都必須是一個fields_info結構的數據項,用於表示當前類或接 口中某個字段的完整描述。 fields[]數組描述當前類或接口聲明的所有字段,但不包括從父類或父接 口繼承的部分

 

12. 方法計數器

方法計數器, methods_count的值表示當前Class 文件 methods[]數組的成員個數。Methods[] 數組中每一項都是一個 method_info 結構的數據項。

 

13. 方法信息數據區

方法表,methods[] 數組中的每個成員都必須是一個 method_info 結構的數據項,用於表示當前類 或接口中某個方法的完整描述。 如果某個method_info 結構的access_flags 項既沒有設置 ACC_NATIVE 標志也沒有設置 ACC_ABSTRACT 標志,那么它所對應的方法體就應當可以被 Java 虛擬機直接從當前類加載,而不需 要引用其它類。 method_info結構可以表示類和接口中定義的所有方法,包括【實例方法】、【類方法】、【實例初始 化方法】和【類或接口初始化方法】。 methods[]數組只描述【當前類或接口中聲明的方法】,【不包括從父類或父接口繼承的方法】。

 

14. 屬性計數器

屬性計數器,attributes_count的值表示當前 Class 文件attributes表的成員個數。 attributes表中每一項都是一個attribute_info 結構的數據項。

 

15. 屬性信息數據區

屬性表,attributes 表的每個項的值必須是attribute_info結構。 在Java 7 規范里,Class文件結構中的attributes表的項包括下列定義的屬性: InnerClasses 、 EnclosingMethod 、 Synthetic 、Signature、SourceFile,SourceDebugExtension 、Deprecated、RuntimeVisibleAnnotations 、RuntimeInvisibleAnnotations以及 BootstrapMethods屬性。 對於支持 Class 文件格式版本號為 49.0 或更高的 Java 虛擬機實現,必須正確識別並讀取 attributes表中的Signature、RuntimeVisibleAnnotations和 RuntimeInvisibleAnnotations屬性。對於支持Class文件格式版本號為 51.0 或更高的 Java 虛擬機實現,必須正確識別並讀取 attributes表中的BootstrapMethods屬性。Java 7 規范 要求 任一 Java 虛擬機實現可以自動忽略 Class 文件的 attributes表中的若干 (甚至全部) 它不可 識別的屬性項。任何本規范未定義的屬性不能影響Class文件的語義,只能提供附加的描述信息 。

 

2. Class中的常量池

常量池的組成:

1.  cp_info:常量池項 

2.  constant_pool_count:常量池計算器

常量池的結構圖:

 

cp_info {

 u1 tag;

   u1 info[]; 

}

 

 JVM是根據tag的值來確定常量池項cp_ino的類型字面量的

 

 

根據tag可以分為如下兩種結構體:

 

 

 

 

 

2. int 和 float在class 文件的存儲結構

 int類型和float 類型的數據類型占用 4 個字節的空間。

 

接下來做個測試,如下類:

public class Constant {
    private final int a = 10;
    private final int b = 10;
    private float c = 11f;
    private float d = 11f;
    private float e = 11f;    
}

用javap -v Constant 或字節碼工具查看,如下圖。常量池里有只有一個float和int常量

 

代碼中所有用到 int 類型 10 的地方,會使用指向常量池的指針值#16個常量池項 (cp_info),即值為 10的結構體CONSTANT_Integer_info,而用到float類型的11f時,也會指向常量池的指針值#7來定位到第#7個常量池項(cp_info) 即值為11f的結構體CONSTANT_Float_info。

3.  long和 double數據類型的常量在常量池中是怎樣表示和存儲 的?

Java語言規范規定了 long 類型和 double類型的數據類型占用8 個字節的空間。那么存在於class 字節碼文件中的該類型的常量是如何存儲的呢?

 

 看如下的列子:

public class Constant {
    private long k = -6076574518398440533L;
    private long m = -6076574518398440533L;
    private long n = -6076574518398440533L;
    private double o = 10.1234567890D;
    private double p = 10.1234567890D;
    private double q = 10.1234567890D;
}

用字節碼工具查看,發現#08,#9表示一個Long類型的常量, #13,#14表示一個Double類型的常量。

 

 

 

 2.3 String類型的字符串常量在常量池中是怎樣表示和存儲的?

對於字符串而言,JVM會將字符串類型的字面量以UTF-8 編碼格式存儲到在class字節碼文件中。這么 說可能有點摸不着北,我們先從直觀的Java源碼中中出現的用雙引號"" 括起來的字符串來看,在編譯器編譯的時候,都會將這些字符串轉換成CONSTANT_String_info結構體,然后放置於常量池中。其結構如下所示:

 

 而字符串的utf-8編碼數據就在這個結構體CONSTANT_Utf8_info

 

 

 看如下的列子:CONSTANT_String_info 指向了地址#28,而#28是CONSTANT_Utf8_info存儲的真正的字符串常量

 

 

 2.4 類文件中定義的類名和類中使用到的類在常量池中是怎樣被組織和存儲的?

JVM會將某個Java 類中所有使用到了的類的完全限定名以二進制形式的完全限定名封裝成 CONSTANT_Class_info結構體中,然后將其放置到常量池里。CONSTANT_Class_info 的tag值為 7.

 

 

Note :類的完全限定名和二進制形式的完全限定名在某個Java源碼中,我們會使用很多個類,比如我們定義了一個 TestClass的類,並把它放到 test包下,則 TestClass類的完全限定名為test.ClassTest,將JVM編譯 器將類編譯成class文件后,此完全限定名在class文件中,是以二進制形式的完全限定名存儲的,即它會把完全限定符的"."換成"/" ,即在class文件中存儲的 TestClass類的完全限定名稱是"test/ClassTest"。因為這種形式的完全限定名是放在了class二進制形式的字節碼文件中,所以就稱之為二進制形式的完全限定名。

 

 請看下面的列子:

package Test;

import java.util.Date;
public class TestClass {
    private Date date = new Date();
}

javap -v Test.TestClass, 可以發現常量池里有3個CONSTANT_Class_info結構體,一個是Test/TestClass, 一個是java/lang/Object,還有一個是java/util/Date. 他們分別指向了地址#21,#22和#19的CONSTANT_Utf8_info字符串。

 

 

為什么有3個類呢?首先Test/TestClass是當前類,在常量池出現毋庸置疑。JVM規定所有類都是Object的子類,所以JVM在編譯后都會把java/lang/Object加上。至於java/util/Date是因為程序里引進了此類,並且使用此類創建了對象,所以會出現在常量池。

注意點: 對於某個類而言,其class文件中至少要有兩個CONSTANT_Class_info常量池項,用來表示自己的類 信息和其父類信息。(除了java.lang.Object類除外,其他的任何類都會默認繼承自 java.lang.Object)如果類聲明實現了某些接口,那么接口的信息也會生成對應的 CONSTANT_Class_info常量池項。除此之外,如果在類中使用到了其他的類,只有真正使用到了相應的類,JDK編譯器才會將類的信息組 成CONSTANT_Class_info常量池項放置到常量池中。

 

如果把代碼修改為

import java.util.Date;
public class TestClass {
    private Date date;
}

 

javap 后java/util/Date類就不見了。因為Date類只是申明了變量,沒有真正實例化和使用,將類信息放置到常量池中的目的,是為了在后續的代碼中有可能會反復用到它。很顯然,JDK在編譯TestClass類的時候,解析到Date類有沒有用到,發現該類在代碼中就沒有用到過,所以就認為沒有必要將它的信息放置到常量 池中了。

 

總結:

1. 對於某個類或接口而言,其自身、父類和繼承或實現的接口的信息會被直接組裝成 CONSTANT_Class_info常量池項放置到常量池中;

2. 類中或接口中使用到了其他的類,只有在類中實際使用到了該類時,該類的信息才會在常量池中有 對應的CONSTANT_Class_info常量池項;

3. 類中或接口中僅僅定義某種類型的變量,JDK只會將變量的類型描述信息以UTF-8字符串組成 CONSTANT_Utf8_info常量池項放置到常量池中,上面在類中的private Date date;JDK編譯器 只會將表示date的數據類型的“Ljava/util/Date”字符串放置到常量池中。

 

2.5 哪些字面量會進入常量池中?

1. final類型的8種基本類型的值會進入常量池。

2. 非final類型(包括static的)的8種基本類型的值,只有double、float、long的值會進入常量池。

3. 常量池中包含的字符串類型字面量(雙引號引起來的字符串值)。

 

 

public class TestConstant {
    private int int_num = 110;
    private char char_num = 'a';
    private short short_num = 120;
    private float float_num = 130.0f;
    private double double_num = 140.0;
    private byte byte_num = 111;
    private long long_num = 3333L;
    private long long_delay_num;
    private boolean boolean_flage = true;
    public void init() {
    this.long_delay_num = 5555L;
}

 

 

2.6 class文件中的引用和特殊字符串

        符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能夠無歧義的定位到目標即可。 例如,在Class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、 CONSTANT_Methodref_info等類型的常量出現。符號引用與虛擬機的內存布局無關,引用的目標並不一定加載到內存中。

       在Java中,一個java類將會編譯成一個class文件。在編譯時,java類並不知道所引用的類的實際地 址,因此只能使用符號引用來代替。 比如 org.simple.People類 引用了 org.simple.Language類 ,在編譯時People類並不知道Language 類的實際內存地址,因此只能使用符號 org.simple.Language (假設是這個,當然實際中是由類似於 CONSTANT_Class_info的常量來表示的)來表示Language類的地址。 各種虛擬機實現的內存布局可能有所不同,但是它們能接受的符號引用都是一致的,因為符號引用的字 面量形式明確定義在Java虛擬機規范的Class文件格式中。

1)直接引用

直接引用可以是:

1. 直接指向目標的指針(比如,指向“類型”【Class對象】、類變量、類方法的直接引用可能是指 向方法區的指針)

2. 相對偏移量(比如,指向實例變量、實例方法的直接引用都是偏移量)

3. 一個能間接定位到目標的句柄

直接引用是和虛擬機的布局相關的,同一個符號引用在不同的虛擬機實例上翻譯出來的直接引用一般不會相同。如果有了直接引用,那引用的目標必定已經被加載入內存中了。

 

引用替換的時機

符號引用替換為直接引用的操作發生在類加載過程(加載 -> 連接(驗證、准備、解析) -> 初始化)中的解析階段,會將符號引用轉換(替換)為對應的直接引用,放入運行時常量池中。

 

2)特殊字符串字面量

特殊字符串包括三種: 類的全限定名, 字段和方法的描述符, 特殊方法的方法名。

2.1) 類的全限定名 Object類,在源文件中的全限定名是 java.lang.Object 。 而class文件中的全限定名是將點號替換成“/” 。 也就是 java/lang/Object 。 源文件中一個類的名字, 在class文件中是用全限定名表述的。

2.2) 描述符:對於字段的數據類型,其描述符主要有以下幾種

  • 基本數據類型(byte、char、double、float、int、long、short、boolean):除 long 和 boolean,其他基本數據類型的描述符用對應單詞的大寫首字母表示long 用 J 表示, boolean 用 Z 表示
  • void:描述符是 V。
  • 對象類型:描述符用字符 L 加上對象的全限定名+;表示,如 String 類型的描述符為 Ljava/lang/String; 。
  • 數組類型:每增加一個維度則在對應的字段描述符前增加一個 [ ,如一維數組 int[] 的描述 符為 [I ,二維數組 String[][] 的描述符為 [[Ljava/lang/String 。

 

 

字段描述符:字段的描述符就是字段的類型所對應的字符或字符串。

int i 中, 字段i的描述符就是 I
Object o中, 字段o的描述符就是 Ljava/lang/Object;
double[][] d中, 字段d的描述符就是 [[D

 

方法描述符: 方法的描述符比較復雜, 包括所有參數的類型列表和方法返回值。 它的格式是這樣的:(參數1類型 參數2類型 參數3類型 ...) 返回值類型

不管是參數的類型還是返回值類型, 都是使用對應字符和對應字符串來表示的, 並且參數列表使用小括號括起來, 並且各個參數類型之間沒有空格,參數列表和返回值類型之間也沒有空格。

 

 

特殊方法的方法名

首先要明確一下, 這里的特殊方法是指的類的構造方法和類型初始化方法。 構造方法就不用多說了,至於類型的初始化方法對應到源碼中就是靜態初始化塊。 也就是說, 靜態初始化塊, 在class文件中是以一個方法表述的, 這個方法同樣有方法描述符和方法名,

具體如下:

  • 類的構造方法的方法名使用字符串
  • 表示 靜態初始化方法的方法名使用字符串 表示。
  • 除了這兩種特殊的方法外, 其他普通方法的方法名, 和源文件中的方法名相同

 

總結

1. 方法和字段的描述符中, 不包括字段名和方法名, 字段描述符中只包括字段類型, 方法描述符中只包括參數列表和返回值類型。

2. 無論method()是靜態方法還是實例方法,它的方法描述符都是相同的。盡管實例方法除了傳遞自身定義的參數,還需要額外傳遞參數this,但是這一點不是由方法描述符來表達的。參數this 的傳遞,是由Java虛擬機實現在調用實例方法所使用的指令中實現的隱式傳遞。

 

3. javap指令

javap是jdk自帶的反解析工具。它的作用就是根據class字節碼文件,反解析出當前類對應的code區 (匯編指令)、本地變量表、異常表和代碼行偏移量映射表、常量池等等信息。

javap的用法格式:javap <option> <class>

options如下:

-help --help -? 輸出此用法消息
-version 版本信息,其實是當前javap所在jdk的版本信息,不是class在哪個jdk下生成的。
-v -verbose 輸出附加信息(包括行號、本地變量表,反匯編等詳細信息)
-l 輸出行號和本地變量表
-public 僅顯示公共類和成員
-protected 顯示受保護的/公共類和成員
-package 顯示程序包/受保護的/公共類 和成員 (默認)
-p -private 顯示所有類和成員
-c 對代碼進行反匯編
-s 輸出內部類型簽名
-sysinfo 顯示正在處理的類的系統信息 (路徑, 大小, 日期, MD5 散
列)
-constants 顯示靜態最終常量
-classpath <path> 指定查找用戶類文件的位置
-bootclasspath <path> 覆蓋引導類文件的位置

 

一般常用的是 -v -l -c三個選項。

javap -v classxx,不僅會輸出行號、本地變量表信息、反編譯匯編代碼,還會輸出當前類用 到的常量池等信息。

javap -l 會輸出行號和本地變量表信息。

javap -c 會對當前class字節碼進行反編譯生成匯編代碼。

 

總結

1、通過javap命令可以查看一個java類反匯編、常量池、變量表、指令代碼行號表等等信息。

2、平常,我們比較關注的是java類中每個方法的反匯編中的指令操作過程,這些指令都是順序執行 的,可以參考官方文檔查看每個指令的含義,很簡單: https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.areturn 3、通過對前面兩個例子代碼反匯編中各個指令操作的分析,可以發現,一個方法的執行通常會涉及下 面幾塊內存的操作:

(1)java棧:局部變量表、操作數棧。這些操作基本上都值操作。

(2)java堆:通過對象的地址引用去操作。

(3)常量池。

(4)其他如幀數據區、方法區(jdk1.8之前,常量池也在方法區)等部分,測試中沒有顯示出來,這 里說明一下。 在做值相關操作時: 一個指令,可以從局部變量表、常量池、堆中對象、方法調用、系統調用中等取得數據,這些數據(可 能是指,可能是對象的引用)被壓入操作數棧。 一個指令,也可以從操作數數棧中取出一到多個值(pop多次),完成賦值、加減乘除、方法傳參、系 統調用等等操作。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM