深入理解Java虛擬機(jī)04--類結(jié)構(gòu)文件
一.程序存儲(chǔ)格式
- 統(tǒng)一的程序存儲(chǔ)格式:不同平臺(tái)的虛擬機(jī)于所有平臺(tái)都統(tǒng)一使用程序存儲(chǔ)格式——字節(jié)碼(ByteCode);
- Java 虛擬機(jī)不關(guān)心 Class 文件的來(lái)源,而只和“Class文件"這種二進(jìn)制文件格式關(guān)聯(lián),也就是說(shuō)Java虛擬機(jī)只認(rèn)識(shí)“Class"文件;
- Java 編譯器可以把 Java 程序代碼編譯成虛擬機(jī)所需要的Class 文件;
二.Class 文件結(jié)構(gòu)
Class 文件是以 8 個(gè)字節(jié)為單位的二進(jìn)制流,緊湊排列,中間沒(méi)有空隙;如果想查看一個(gè) Class 文件除了通過(guò) winHex 編譯器看到字節(jié)碼,也可以通過(guò) javap -verbose xxx.Class 輸出字節(jié)碼內(nèi)容,這樣看起來(lái)比較直觀。
1、基本類型
無(wú)符號(hào)數(shù):
- 定義:基本的數(shù)據(jù)類型,u1、u8表示1個(gè)字節(jié),8個(gè)字節(jié)。
- 使用:可以用來(lái)描述數(shù)字、索引、引用,utf-8格式的字符串;
表:
- 定義:多個(gè)無(wú)符號(hào)數(shù)和其他表組成的復(fù)合數(shù)據(jù)類型;通常以“_info” 結(jié)尾。
- 使用:描述有層次關(guān)系的復(fù)合結(jié)構(gòu)數(shù)據(jù);
2、魔數(shù)與版本
- 魔數(shù):每個(gè)Class文件的頭4個(gè)字節(jié),唯一作用就是確定這個(gè)文件是否能被一個(gè)虛擬機(jī)接受的Class文件;
- 次版本號(hào):緊接著魔數(shù)后面的第5和第6個(gè)字節(jié);
- 主版本號(hào):第7和第8個(gè)字節(jié)代表主版本號(hào),比如說(shuō)50對(duì)應(yīng)的就是JDK1.6.
- 可以使用十六進(jìn)制編譯器WinHex打開(kāi)一個(gè)Class文件瞅瞅;
3、常量池
版本號(hào)之后緊跟的就是常量池入口,可以理解為Class文件之中的資源倉(cāng)庫(kù);
- 常量池容量計(jì)數(shù)器:u2類型,代表本Class文件有N-1個(gè)常量(因?yàn)槭菑?開(kāi)始技術(shù)的);
- 0項(xiàng)常量:不引用任何一個(gè)常量池項(xiàng)目
- 常量池放置的內(nèi)容每一項(xiàng)都是一個(gè)表,主要分兩類
- 字面量:文本字符串、final常量值等;
- 符號(hào)引用
- 類和接口的全限定名
- 字段的名稱和描述符
- 方法的名稱和描述符
- length:
- 定義:UTF-8編碼的字符串長(zhǎng)度是多少個(gè)字節(jié);
- 65535限制:Class文件中方法、字段等都需要引用CONSTANT_ Utf8_ info型常量的length為u2類型,最大為65535.如果某個(gè)變量或者方法名超過(guò)了64K,那么這個(gè)length容不下了,當(dāng)然也就無(wú)法編譯了。
4、訪問(wèn)標(biāo)志(access_flags)
- 常量池之后緊跟的就是訪問(wèn)標(biāo)志。主要包括了這個(gè)Class是類or接口,是不是 public,是不是 abstract 類型,是不是 final 類型。
5、類索引、父類索引和接口類集合
- java.lang.Object類索引為0;
- 類的索引其實(shí)就是描述了這個(gè)Class的extends和implements的關(guān)系;
6、字段表集合(field_info)
- 用于描述定義的變量,依次包括了訪問(wèn)標(biāo)志(access_ flags)、名稱索引(name_ index)、描述索引(descriptor_ index)、屬性表集合(attributes)。
- 描述的信息如下:
- 作用域:public、private、protect
- 實(shí)例or類變量:static
- 可變性:final
- 并發(fā)可見(jiàn)性:volatile
- 是否可序列化:transient
- 字段數(shù)據(jù)類型:基本類型、對(duì)象、數(shù)組等
- 字段名稱;
- 字段表集合原則
- 1、不會(huì)列出超類or父類或者父接口繼承而來(lái)的字段;
- 2、有可能列出原本Java代碼中不存在的字段(內(nèi)部類會(huì)自動(dòng)添加指向外部類實(shí)例的字段,才能引用到外部類);
- 3、Java語(yǔ)言中字段是無(wú)法重載的;
7、方法表集合
和字段表集合差不多,方法表集合用來(lái)描述Class文件中的方法,但是訪問(wèn)標(biāo)志和屬性表集合和字段表集合有所區(qū)別;
- 訪問(wèn)標(biāo)志:
- volatile、transient關(guān)鍵字不可以修飾方法,方法表中少了這兩種標(biāo)志;
- synchronized、native、strictfp和abstract可以修復(fù)方法,故方法表增加了這些對(duì)應(yīng)的標(biāo)志;
- Code屬性:
- 方法體中的代碼放在了“Code”屬性里面了;
- 方法表集合原則
- 方法沒(méi)有重寫(xiě)(Override),父類的信息不會(huì)寫(xiě)到子類的方法表中;
- 編譯器有可能自動(dòng)添加一些方法,典型的如類構(gòu)造器的“< clinit >”、方法&實(shí)例構(gòu)造器的“< init >“方法;
- 重載(Overload)一個(gè)方法,需要添加一個(gè)特征簽名,特征簽名就是一個(gè)方法中各個(gè)參數(shù)在常量池中的字段符號(hào)引用的集合;
8、屬性表集合(attribute_info)
上述那些表需要攜帶自己的某些屬性,來(lái)描述自己的特殊環(huán)境信息,比如InnderClasses、LineNumberTable、Code 之類的;
- Code (用語(yǔ)描述代碼)
- max_stack:操作棧深度最大值,JVM 運(yùn)行時(shí)根據(jù)這個(gè)值來(lái)分配棧幀(Stack Frame)中的操作棧深度;
- max_locals:代表了局部變量表所需要的存儲(chǔ)空間。
- Slot:虛擬機(jī)為局部變量分配內(nèi)存的最小單位
- byte、char、float、int、short、boolean、returnAddress 長(zhǎng)度少于32位,占1個(gè)slot
- double、long 64位,占2個(gè)slot
- 當(dāng)代碼超出一個(gè)局部變量的作用域時(shí),這個(gè)局部變量所占用的 slot 可以被其他的局部變量所使用
- Slot:虛擬機(jī)為局部變量分配內(nèi)存的最小單位
- code_length:字節(jié)碼長(zhǎng)度
- code:存儲(chǔ)字節(jié)碼指令
- 65535限制:虛擬機(jī)規(guī)定了一個(gè)方法不允許超過(guò) 65535 條字節(jié)碼,否則編譯不通過(guò);
- 執(zhí)行:執(zhí)行過(guò)程中的數(shù)據(jù)交換、方法調(diào)用等操作都是基于棧(操作棧)的;
- this關(guān)鍵字:在實(shí)例方法中通常可以有個(gè) this 關(guān)鍵字來(lái)引用當(dāng)前對(duì)象的變量,這是因?yàn)?Java 編譯時(shí)在局部變量表中自動(dòng)增加了這個(gè)(this)局部變量。
- LineNumberTable:描述 Java 的源碼行號(hào)和字節(jié)碼行號(hào);
- LocalVariableTable:描述局部變量表中的變量與Java源碼中定義的變量之間的關(guān)系;
三.字節(jié)碼指令
1、字節(jié)碼組成
- 操作碼(Opcode):i(助記符)代表int類型數(shù)據(jù)操作....等等;
- 操作數(shù) (Operands):永遠(yuǎn)都是一個(gè)數(shù)組類型的對(duì)象;
Java虛擬機(jī)采用面向操作數(shù)棧而不是寄存器的架構(gòu),字節(jié)碼指令集是一種指令集架構(gòu)。放棄了操作數(shù)對(duì)齊,省略了填充的符號(hào)和間隔。
2、加載和存儲(chǔ)指令
將數(shù)據(jù)在幀棧中將局部變量表和操作數(shù)棧之間來(lái)回傳輸。
- 將一個(gè)局部變量加載到操作棧;
- 將一個(gè)數(shù)值從操作數(shù)棧存儲(chǔ)到局部變量表;
- 將一個(gè)常量加載到操作數(shù)棧;
- 擴(kuò)充局部變量表的訪問(wèn)索引的指令;
3、運(yùn)算指令
- 將兩個(gè)操作數(shù)棧上的值進(jìn)行某種特定運(yùn)算,并把結(jié)果重新存入到操作棧頂;
- Java沒(méi)有直接支持byte、short、char、boolean類型,都轉(zhuǎn)為int類型進(jìn)行運(yùn)算,使用int的指令代替;
4、類型轉(zhuǎn)換指令
- 寬化轉(zhuǎn)換
- int到long、float、double
- long到float、double
- float到double
- 窄化轉(zhuǎn)換
- 必須顯示的聲明轉(zhuǎn)換
- 有溢出或者丟精的情況,但不會(huì)拋出異常
5、同步指令
- Java虛擬機(jī)支持方法級(jí)同步和方法內(nèi)部一段指令序列同步,這兩種同步都是通過(guò)“管程”來(lái)支持;
- 執(zhí)行線程就要求先成功持有“管程”,然后才能執(zhí)行方法,最后方法執(zhí)行完成后,才釋放“管程”。
- Java虛擬機(jī)通過(guò) monitorenter 和 monitorexit兩個(gè)指令配對(duì)使用,另外編譯器會(huì)自動(dòng)增加一個(gè)異常處理器。當(dāng)出現(xiàn)異常時(shí),這個(gè)異常處理器能夠捕獲到所有的異常,并且釋放“管程”,monitorexit 指令響應(yīng)。這樣的話,保證了 monitorenter 和monitorexit 總是成對(duì)出現(xiàn)的。
三.代碼舉例
1、Java文件:
1 package com.xxx.ccc; 2 public final class InitConfig { 3 public static final InitConfig BFCACCOUNT = new InitConfig(0, "aaa", "AAA"); 4 private int mIndex; 5 private String mData; 6 private String mDescribe; 7 private InitConfig(int indexFlag, String data, String describe) { 8 this.mIndex = indexFlag; 9 this.mData = data; 10 this.mDescribe = describe; 11 } 12 public String getmData() { 13 return this.mData; 14 }
2、Class 文件:
1 Last modified 2017-7-4; size 1050 bytes 2 MD5 checksum 2beb0c10f91b793c3570edcf2d1eff78 3 Compiled from "InitConfig.java" 4 public final class com.xxx.xxx.InitConfig 5 minor version: 0 //次版本號(hào) 6 major version: 51 //主版本號(hào) 7 flags: ACC_PUBLIC, ACC_FINAL, ACC_SUPER //訪問(wèn)標(biāo)志 8 Constant pool: //常量池 9 #1 = Methodref #14.#41 // java/lang/Object."<init>":()V 10 #2 = Fieldref #5.#42 // com/xxx/xxx/InitConfig.mIndex:I 11 #6 = Class #46 // com/xxx/xxx/common/constant/ConstData 12 #7 = String #47 // aaa 13 #23 = Utf8 <init> 14 #24 = Utf8 (ILjava/lang/String;Ljava/lang/String;)V 15 #25 = Utf8 Code 16 #26 = Utf8 LineNumberTable //Java的源碼行號(hào)和字節(jié)碼行號(hào) 17 #27 = Utf8 LocalVariableTable //局部變量表中的變量與Java源碼中定義的變量之間的關(guān)系 18 #28 = Utf8 this 19 #32 = Utf8 getmData 20 #33 = Utf8 ()Ljava/lang/String; 21 #37 = Utf8 <clinit> 22 #38 = Utf8 ()V 23 #40 = Utf8 InitConfig.java 24 #41 = NameAndType #23:#38 // "<init>":()V 25 #45 = Utf8 com/xxx/xxx/InitConfig 26 #46 = Utf8 com/xxx/xxx/common/constant/ConstData 27 #53 = NameAndType #17:#16 // SEAACCOUNT:Lcom/xxx/ccc/InitConfig; 28 #54 = Utf8 java/lang/Object 29 public static final com.xxx.xxx BFCACCOUNT; 30 descriptor: Lcom/xxx/xxx/InitConfig; 31 flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL 32 public java.lang.String getmData(); 33 descriptor: ()Ljava/lang/String; 34 flags: ACC_PUBLIC 35 Code: 36 stack=1, locals=1, args_size=1 37 0: aload_0 38 1: getfield #3 // Field mData:Ljava/lang/String; 39 4: areturn 40 LineNumberTable: //Java的源碼行號(hào)和字節(jié)碼行號(hào) 41 line 36: 0 42 LocalVariableTable: //局部變量表中的變量與Java源碼中定義的變量之間的關(guān)系 43 Start Length Slot Name Signature 44 0 5 0 this Lcom/xxx/xxx/InitConfig; //方法里面默認(rèn)增加了個(gè)this
四.小結(jié)
為什么說(shuō)一些”非Java"語(yǔ)言也是可以在 JVM 上跑,這是因?yàn)?JVM 只認(rèn)識(shí) Class 文件,所以如果某某語(yǔ)言最終編譯出的文件是 Class 文件,那么對(duì)于 JVM 來(lái)說(shuō)沒(méi)有什么區(qū)別,但是得按照 Class 文件的結(jié)構(gòu)來(lái),不然也無(wú)法正常執(zhí)行。Class 定義了許多特定的基本類型和表結(jié)構(gòu),通過(guò)魔數(shù)讓 JVM 認(rèn)識(shí)該文件,版本號(hào)保證可以在要求的 JDK 版本上運(yùn)行,在常量池中定義好常量,訪問(wèn)標(biāo)志位確定訪問(wèn)權(quán)限。索引集合方便與外界的 class 保持聯(lián)系,字段表保存我們定義好的變量,方法表存儲(chǔ)方法的信息,屬性表存儲(chǔ)了上述各種表的一些屬性。其中記住 slot為局部變量分配內(nèi)存的最小單位,當(dāng)程序超出作用域的時(shí)候,slot 可以被其他替換使用。到這里,僅僅是代碼最靜態(tài)的存儲(chǔ)的格式,程序要運(yùn)行起來(lái)。還需要操作指令,也是由字節(jié)碼存儲(chǔ),包括操作碼和操作數(shù)。有加載存儲(chǔ)、運(yùn)算、類型轉(zhuǎn)換、同步指令。
posted on 2018-08-06 11:15 安卓筆記俠 閱讀(472) 評(píng)論(0) 收藏 舉報(bào)
浙公網(wǎng)安備 33010602011771號(hào)