Java Class文件結構解析 及 實例分析驗證
在文章《Java三種編譯方式:前端編譯 JIT編譯 AOT編譯》中瞭解到了它們各有什麼優點和缺點,以及前端編譯+JIT編譯方式的運作過程;在《Java前端編譯:Java源代碼編譯成Class文件的過程》瞭解到javac編譯的大體過程。
Class文件是JVM執行引擎的數據入口,也是Java技術體系的基礎構成之一;瞭解Class文件的結構對後面進一步瞭解JVM執行引擎有很重要的意義。
下面我們詳細瞭解Class文件:先對Class文件結構有個大體瞭解,並瞭解Class文件結構裏的一些名稱定義;而後再詳細說明結構中每一項數據的含義,並用測試程序編譯Class文件來分析驗證Class文件結構。
1、Class文件概述
兩種常見的程序編譯存儲文件格式分類:
1、編譯成"01..."二進制格式的存儲機器碼文件格式,直接運行在操作系統上;
2、編譯成與操作系統和機器指令集無關的格式作爲存儲文件格式,運行在隔離硬件平臺的虛擬機之上;
C/C++編譯出來的程序屬於第一種,而Class文件屬於第二種;
1-1、什麼是Class文件?
Class文件是經過前端編譯(如javac編譯)後生成的文件(以.class爲後綴),一個Class文件對應一個類或一個接口。
Class文件存儲的內容稱爲字節碼(ByteCode),包含了JVM指令集和符號表以及若干其他輔助信息。
JVM規範定義了Class文件結構格式,每種JVM實現必須滿足規範定義,這樣JVM實例才能加載Class文件,運行字節碼內容。但JVM的實現可以在JVM規範的約束下對具體實現做出修改和優化(如自定義屬性信息,JVM會忽略不認識的屬性表)。
從數據類型看,Class文件是一組以8位字節(8-bit bytes)爲基礎的二進制字節流構成,8位以上的數據以大端(Big-Endian)的高位在前的順序進行存儲,中間沒有添加任何分隔符。
從文件形式看,JVM加載的Class文件不一定來磁盤,還可以來自網絡數據,甚至在運行時直接編譯代碼字符串生成 。
1-2、Class文件的作用(優點)是什麼?
JVM規範定義Class文件格式來存儲字節碼,是虛擬機實現平臺無關性、以及語言無關性的關鍵。
1、平臺無關性
Java語言有"一次編寫,到處運行(Write Once,Run Anywhere)"特性。
通過針對不同平臺實現的虛擬機作爲字節碼執行引擎,它在多種操作系統和架構上提供 Java 運行時環境。
2、語言無關性
虛擬機只與字節碼關聯,不與任何語言綁定。
字節碼和虛擬機也是語言無關性的基礎,因爲不只Java語言程序可以編譯生成字節碼,其他很多語言(如JRuby、Javascript等)也可以通過相應的編譯器編譯成JVM規範規定的字節碼格式。
可以看出,字節碼的語義描述能力比Java語言更強大。
目前JDK7和JDK8的JVM已經可以支持一些其他語言運行了,如Croovy、JRuby、Jython、Scala等。
1-3、Class文件結構的發展
相對於Java語言、API和其他方面的變化,Class文件結構一直處於比較穩定的狀態,因爲作爲一種JVM規範,變化太多會增大實現難度,出現版本不兼容的情況。
Class文件的結構在幾個版本中的變化不大,Class文件的主體結構、字節碼指令的語義和數量幾乎沒有出現過變動,只是爲適應新特性,增加了幾個標記和屬性。
即幾乎所有Class文件格式的改進都集中在訪問標誌、屬性表這些設計上,就可擴展的數據結構中添加內容,如:
訪問標誌新加入5個ACC_SYNTHETIC、ACC_ANNOTATION、ACC_ENUM、ACC_BRIDGE、ACC_VARARGS標誌;
屬性表集合中,JDK1.5到JDK1.7增加12項新的屬性,JDK1.8又增3項;
這些屬性大部分用於支持Java語言的新特性,如泛型、枚舉、變長參數、動態註解等;
還有一些爲支持性能改進和調試信息,如JDK1.6的新類型校驗器StackMapTable屬性和對非Java代碼用到的SourceDebugExtension屬性;
字節碼指令也只有幾次輕微的變動,如:
JDK1.0.2時改動過invokespecial指令的語義,JDK1.7增加了invokedynamic指令,禁止了ret和jsr指令。
2、Class文件結構相關定義說明
我們先對Class文件結構有個大體瞭解,並瞭解Class文件結構裏的一些名稱定義,後面再詳細說明結構中每一項數據的含義。
2-1、Class文件的結構是怎樣的?
Class文件結構根據《Java虛擬機規範》定義的結構進行存儲,類似於C語言的結構體的僞結構。
僞結構中各個數據都有相應的含義,並且各個數據項必須嚴格按規定的先後順序排列,它們之間沒有任何分隔符和空隙。
所以,存儲的幾乎都是運行需要數據(和指令),結構及說明如下:
類型 |
名稱 |
數量 |
說明 |
u4 |
magic |
1 |
魔數:確定一個文件是否是Class文件 |
u2 |
minor_version |
1 |
Class文件的次版本號 |
u2 |
major_version |
1 |
Class文件的主版本號:一個JVM實例只能支持特定範圍內版本號的Class文件(可以向下兼容)。 |
u2 |
constant_pool_count |
1 |
常量表數量 |
cp_info |
constant_pool |
constant_pool_count-1 |
常量池:以理解爲Class文件的資源倉庫,後面的其他數據項可以引用常量池內容。 |
u2 |
access_flags |
1 |
類的訪問標誌信息:用於表示這個類或者接口的訪問權限及基礎屬性。 |
u2 |
this_class |
1 |
指向當前類的常量索引:用來確定這個類的的全限定名。 |
u2 |
super_class |
1 |
指向父類的常量的索引:用來確定這個類的父類的全限定名。 |
u2 |
interfaces_count |
1 |
接口的數量 |
u2 |
interfaces |
interfaces_count |
指向接口的常量索引:用來描述這個類實現了哪些接口。 |
u2 |
fields_count |
1 |
字段表數量 |
field_info |
fields |
fields_count |
字段表集合:描述當前類或接口聲明的所有字段。 |
u2 |
methods_count |
1 |
方法表數量 |
method_info |
methods |
methods_count |
方法表集合:只描述當前類或接口中聲明的方法,不包括從父類或父接口繼承的方法。 |
u2 |
attributes_count |
1 |
屬性表數量 |
attributes_info |
attributes |
attributes_count |
屬性表集合:用於描述某些場景專有的信息,如字節碼的指令信息等等。 |
這裏先對Class文件結構有個大體的瞭解,後面會詳細說明每一項數據,下面先來了解Class文件結構裏的一些名稱定義。
2-2、Class文件結構的數據類型是什麼?
結構中只有兩類數據類型:無符號數和表。
1、無符號數
無符號數屬於基本的數據類型,以u1、u2、u4、u8來表示一個字節、兩個字節...的無符號數;
無符號數用來描述數字、索引引用、數量值或UTF-8編碼構成的字符串值。
2、表
表是由多個無符號數或其他表作爲數據項構成的複合數據類型,一般以"_info"結尾;
表用來描述有層次關係的複合結構的數據;
表中的項長度不固定;
整個Class文件本質上就是一個表。
2-3、全限定名稱、非全限定名稱、描述符以及簽名
它們是在Class文中結構中的字段、方法、類、接口都可能用到的表示,都存儲在常量池,爲CONSTANT_Utf8_info類型常量UTF-8字符串。
其他數據項中通過索引引用,如當前類索引(this_class)都指向CONSTANT_Class_info常量類型數據,而CONSTANT_Class_info裏通過索引指向CONSTANT_Utf8_info常量類型數據,這樣就可以找到當前類全限定名。
1、全限定名稱(Fully Qualified Name)
全限定名是在整個JVM中的絕對名稱,可以表示Class文件結構中的類或接口的名稱。
都通過全限定形式(Fully Qualified Form)來表示,這被稱作它們的"二進制名稱"(JLS §13.1);但用來分隔各個標識符的符號不在是ASCII 字符點號('.'),而是被 ASCII 字符斜槓('/')所代替。
如,類 Thread 的正常的二進制名是"java.lang.Thread",在 Class 文件面,對該類的引用是通過來一個代表字符串"java/lang/Thread"的CONSTANT_Utf8_info 結構來實現的。
2、非全限定名稱(Unqualified Names)
也稱爲簡單名稱;
方法名、字段名和局部變量名都被使用非全限定名進行存儲;
如,"java.lang.Object"表示爲"Object"。
3、描述符(Descriptor)
描述符是一個描述字段或方法的類型的字符串。
(A)、字段描述符(Field Descriptor):
是一個表示類、實例或局部實例變量的語法符號;
由下面語法產生的字符序列:
(B)、方法描述符(Method Descriptor)
描述一個方法所需的參數和返回值信息,即包括參數描述符(ParameterDescriptor)和返回描值述符(ReturnDescriptor);
由下面語法產生的字符序列:
一個方法無論是靜態方法還是實例方法,它的方法描述符都是相同的;
方法額外傳遞參數this,不是由法描述符來表達的;
而是由 Java 虛擬機實現在調用實例方法所使用的指令中實現的隱式傳遞;
(C)、描述符中基本類型表示字符如下:
字符 |
類型 |
含義 |
B |
byte |
有符號字節型數 |
C |
char |
Unicode 字符,UTF-16 編碼 |
D |
double |
雙精度浮點數 |
F |
float |
單精度浮點數 |
I |
int |
整型數 |
J |
long |
長整數 |
S |
short |
有符號短整數 |
Z |
boolean |
布爾值:true/false |
L Classname; |
reference |
一個名爲<Classname>的實例 |
[ |
reference |
一個一維數組 |
V |
void |
void返回值(其實不屬於基本類型,而是VoidDescriptor) |
例如:
描述int實例變量的描述符是"I";
java.lang.Object實例描述符是"Ljava/lang/Object;";
double的三維數組"double d[][][];"的描述符爲"[[[D";
Object mymethod(int i, double d, Thread t)方法描述符是"(IDLjava/lang/Thread;)Ljava/lang/Object";
更多請參考:http://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.3
4、簽名(Signature)
簽名是用於描述字段、方法和類型定義中的泛型信息的字符串,這應該是JDK1.5引入泛型(類型變量或參數化類型)後的而出現的。
Java編譯器必須爲聲明使用類型變量或參數化類型的任何類、接口、構造函數、方法或字段發出簽名。類型變量或參數化類型在編譯時經過類型擦除變爲原始類型,所以它們都是不在Java虛擬機中使用的類型,而Java編譯器需要這類信息來實現(或輔助實現)反射(reflection)和跟蹤調試功能。
HotSpot VM實現在加載和鏈接時,並不校驗Class文件的簽名內容,直到被反射方法調用時纔會校驗。
在Java語言中,任何類、接口、初始化方法或成員的泛型簽名,如果包含了類型變量(Type Variables)或參數化類型(Parameterized Types),則該字段在字段表集合中(field_info fields[fields_count])對應的字段信息(field_info),或該方法在方法表集合(method_info methods[methods_count])對應的方法信息(method_info),存在Signature屬性會爲它記錄泛型簽名信息,Signature屬性存在指向CONSTANT_Utf8_info常量類型數據的索引,這樣就可以找到相應的簽名字符串。
(A)、類簽名(Class Signature)
作用是把Class申明的類型信息編譯成對應的簽名信息;
描述當前類可能包含的所有的(泛型類型的)形式類型參數,包括直接父類和父接口;
由 ClassSignature 定義:
(B)、字段類型簽名(Field Type Signature)
作用是將字段、參數或局部變量的類型編譯成對應的簽名信息;
由 JavaTypeSignature定義,包括基本類型和引用類型的簽名:
(C)、方法簽名(Method Signature)
作用是將方法中所有的形式參數的類型編譯成相應的簽名信息(或將它們參數化);
由 MethodTypeSignature 定義:
計算方法的特徵簽名在前文《Java前端編譯:Java源代碼編譯成Class文件的過程》"2-2、填充符號表 第4點、計算方法的特徵簽名"時曾提到過;
更多關於簽名請參考:http://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.7.9
更多關於泛型編譯類型擦除請參考上文:《Java前端編譯:Java源代碼編譯成Class文件的過程》4-2、解語法糖
2-4、描述符、簽名以及特徵簽名的區別
1、描述符和簽名的區別
通過上面描述符與簽名的詳細說明,我們知道:
描述符:
描述符是一個描述字段或方法的類型的字符串;
簽名:
描述字段、方法和類型定義中的泛型信息的字符串;
區別:
(A)、範圍不同
描述符不能描述類,所以沒有類描述符;
簽名是JDK1.5引入泛型(類型變量或參數化類型)後的而出現的;
簽名需要(字段、方法和類中)有類型變量或參數化類型的時候纔會出現。
(B)、對於方法來說
描述符 = 參數類型 + 參數順序 + 返回值類型;
簽名 = 描述符 + FormalTypeParametersopt + ThrowsSignature;
簽名還包括類型變量或參數化類型編譯未擦除類型前的信息(FormalTypeParametersopt),和拋出的異常信息(ThrowsSignature);
也就是說,當一個方法沒有類型變量或參數化類型,也沒有拋出異常時,簽名和描述符是一樣的,不過這時候也就沒有簽名信息存在了。
2、方法的描述符(簽名)和特徵簽名的區別
方法特徵簽名:
用於區分兩個不同方法的語法符號;
是和上面說的簽名是兩個不同的概念;
這個在Java語言層面和JVM層面是不同的,這個問題在前文《Java前端編譯:Java源代碼編譯成Class文件的過程》"2-2、填充符號表 第4點、計算方法的特徵簽名"時曾提到過;
(A)、Java語言層面的方法特徵簽名
特徵簽名 = 方法名 + 參數類型 + 參數順序;
更多請參考:http://docs.oracle.com/javase/specs/jls/se8/html/jls-8.html#jls-8.4.2
(B)、JVM層面的方法特徵簽名
特徵簽名 = 方法名 + 參數類型 + 參數順序 + 返回值類型;
即特徵簽名 = 方法名 + 描述符;
如果存在類型變量或參數化類型,還包括類型變量或參數化類型編譯未擦除類型前的信息(FormalTypeParametersopt),和拋出的異常信息(ThrowsSignature),即方法名+簽名;
Java語言重載(Overload)一個方法,需要ava語言層面的方法特徵簽名不同,即不包括方法返回值;而Class文件中有兩個同名同參數(類型、順序都相同),但返回值類型不一樣,也是允許的,可以正常運行,因爲JVM層面的方法特徵簽名包括返回值類型。
同樣的,對字段來說,Java語言規定字段無法重載,名稱必須不一樣;但對Class文件來說,只要兩個字段描述(類型)不一樣,名稱一樣也是可以的。
(篇幅有限,後面有時間再來驗證)
3、Class文件結構分析驗證
上面我們對Class文件結構有個大體瞭解,並瞭解Class文件結構裏的一些名稱定義;下面將詳細說明結構中每一項數據的含義,同時,先用javac編譯測試程序ClassFileTest.java,對照編譯出來的Class文件進行說明,測試程序如下:
import java.util.HashMap; import java.util.Map; public class ClassFileTest { public String mString = "blog.csdn.net/tjiyu"; private static String mStaticString = "hello"; private final static String mFinalStaticString = "java"; public Map<String, String> getMap() { Map<String, String> map = new HashMap<String, String>(); map.put(mStaticString, mFinalStaticString); return map; } }
編譯命令如下:
javac ClassFileTest.java
然後用javap反編譯ClassFileTest.class文件,並保存到ClassFileTest.txt文件,方便分析:
javap -verbose ClassFileTest > ClassFileTest.txt
注意,這裏使用JDK8。
3-1、魔數
Class文件開始是4個字節定義爲魔數(Magic Number);
唯一作用:確定一個文件是否是Class文件;
魔數可以自由選擇,只要沒有廣泛使用而且不會引起混淆的即可,這樣就不會因爲擴展名改變而無法識別;其他許多文件類型格式頭都存在魔數,如gif、jpeg等。
Class文件的魔數爲"0xCAFEBABE"(咖啡寶貝),比照ClassFileTest.class如下:
3-2、Class文件的版本
魔數之後的4個字節,第5、6這兩個字節是次版本號(Minor Version),如m;第7、8這兩個字節是主版本號(Major Version),如M,構成版本號M.m,大小的順序爲:49.5 < 50.0 < 50.1。
一個Java虛擬機實例只能支持特定範圍內版本號的Class文件,高版本號的Java虛擬機實現可以向下兼容低版本號的Class文件,反之則不成立。
從45開始,JDK1.0.2中的Oracle Java虛擬機支持版本45.0~45.3;
JDK1.1.*支持45.0~45.65535;
JDK1.k(k≥2)時,對應的Class文件格式版本號的範圍是45.0至44+k.0。
在ClassFileTest.class中爲"00000034",如下:
即minor_version爲0,major_version爲52(0034對應的十進制),版本爲52.0(符合JDK8),反編譯信息ClassFileTest.txt中也明確列出,如下:
3-3、常量池
Class文件的版本號之後,即從第9個字節開始;
先是常量池數量計數值,接着是常量池(表);
1、常量池數量計數值
因爲常量池數量不固定,所以需要一個數量計數值;
從1開始,第0項空出,後面其他數據項引用常量池第0項(即爲索引值0時),可以表示"不引用任何一個常量池項內容";
只有常量池是從1開始,後面的其他集合類型的計數值都是從0開始;
注意,CONSTANT_Long_info 或 CONSTANT_Double_info常量結構佔兩個常量表項的空間,其他都佔一個空間,即如果一個CONSTANT_Long_info 或 CONSTANT_Double_info結構的項在常量池中的索引爲n,則常量池中下一個有效的項的索引爲 n+2。
在ClassFileTest.class中爲"002A",如下:
即常量項數量爲42(2A對應的十進制),反編譯信息ClassFileTest.txt中也明確列出常量池各項,可以看到第一項索引爲1,且最大項索引爲41,一共42項(加上索引爲0的項,且沒Double和Long佔兩個索引的結構),如下:
2、常量池
常量池每一項常量都是一個表;
包含Class文件結構及其子結構中引用的所有字符串常量、類或接口名、字段名和其它常量;
主要存放兩大類常量:字面量(Literal)和符號引用(Symbolic References);
(A)、字面量:常見的常量,如文本字符串、聲明爲final的常量值等;
(B)、符號引用:需要編譯原理的概念,主要包括三類常量:
(I)、類和接口的全限定名(Full Qualified Name);
(II)、字段的名稱和描述符(Descriptor);
(III)、方法的名稱和描述符;
Class文件不會像C/C++編譯後保存各個方法、字段的內存佈局信息;JVM運行時,需要從常量池獲得對應的符號引用,再在類創建時或運行時解析、翻譯到具體的內存地址中;
所以,常量池可以理解爲Class文件的資源倉庫,後面的其他數據項可以引用常量池內容;
所有的常量池項都具有如下通用格式:
tag項開頭,一個字節的標誌位;
tag的標識了後面info[]項的內容,也就是這個常量屬於哪種類型;
JDK1.7前有11種的常量類型結構,JDK1.7爲更好支持動態語言調用,又增加了額外的3種(CONSTANT_MethodHandle、CONSTANT_MethodType、CONSTANT_InvokeDynamic),到了JDK1.8也是14種,14種常量類型對應的tag項說明如下:
14種常量類型,結構各不同;
注意,從《Java前端編譯:Java源代碼編譯成Class文件的過程》可以知道,編譯器會自動生成一些常量,如字段、方法的描述符(<clinit>)等;
對測試程序,反編譯信息ClassFileTest.txt中也明確列出常量池各項如下:
(1)、CONSTANT_Fieldref_info、CONSTANT_Methodref_info 和CONSTANT_InterfaceMethodref_info結構
從上面看到,反編譯信息第一項常量結構爲CONSTANT_Methodref_info,下面先來了解它。
字段,方法和接口方法由類似的結構表示;
它們的結構裏面的三項都一樣,結構如下:
tag:
字段爲9,方法爲10,接口方法爲11(從上面表格中可以看到);
class_index:
是對CONSTANT_Class _info類型常量的一個有效索引,代表一個類或接口,而當前字段或方法是這個類或接口的成員;
name_and_type_index:
是對CONSTANT_NameAndType_info類型常量的一個有效索引,代表當前字段或方法的名字和描述符(名字和描述符定義看前面)。
從上面看到,反編譯信息第一項常量結構爲CONSTANT_Methodref_info,從後面註釋的信息看,該方法是java/lang/Object類的實例初始化函數<init>(),如下:
在ClassFileTest.class文件中字節流數據爲常量項數量"002A"後面的5個字節—"0A000B001C",如下:
Tag:對應"0A"即爲10;
class_index:對應"000B"爲11,索引到第11項常量,可以看到爲CONSTANT_ Class _info結構類型,該結構中又索引到第38項常量—"java/lang/Object",表示該方法是java/lang/Object的成員,如下
class_index:對應"001C"爲28,可以看到爲CONSTANT_ NameAndType _info結構類型,該結構中又索引到第17項和第18項常量,分別表示該方法的名稱"<init>"和描述符"()V",如下:
另外,通過反編譯信息發現,該方法只在javac編譯器自動添加的ClassFileTest類實例初始化函數(實際名稱也是<init>())中通過invokespecial指令調用(即super()函數調用),如下:
(2)、CONSTANT_Class_info結構
該結構在上面字段,方法和接口方法結構中已經提到過了(class_index項);
用於表示一個類或接口;
結構如下:
tag:
爲7;
name_index:
是對CONSTANT_Utf8_info類型常量的一個有效索引,代表一個類或接口的全限定名(全限定名定義看前面)。
字節碼指令 anewarray 和 multianewarray 創建的引用型數組對象,可以通過常量池中的CONSTANT_Class_info結構來引用類數組;對於這些數組,類的名字就是數組類型的描述符;例如:表示一維 Thread 數組類型"Thread[]"的名字是:“[Ljava/lang/Thread;”;
這裏我們來看下ClassFileTest類的常量表示,先通過反編譯信息找到ClassFileTest類的位置,可以看到在常量池第7項,相關常量如下:
tag爲7即07(u1類型),而且索引值爲#33對應十六進制爲0021(u2類型),所以在ClassFileTest.class中找出實際數據"070021"(當然還得驗證前後的數據,可以看到前面"09070020"是對應第6項反編譯信息的),如下:
另外,反編譯信息中看到,只有"mString"和"mStaticString"兩個字段引用ClassFileTest類結構常量(#7),而"mFinalStaticString"字段和"getMap()"方法並沒有相關CONSTANT_Fieldref_info、CONSTANT_Methodref_info定義,這是什麼呢?
對於"getMap()"方法沒定義很容易理解,因爲程序中並沒有調用它,所以並不需要定義CONSTANT_Methodref _info來引用;但相關常量信息還是必須有的,因爲在方法表集合中存儲"getMap()"方法表需要引用相關常量(詳情見後面的方法表集合介紹),一些相關常量信息如下:
對於"mFinalStaticString"字段,在getMap()"方法中存在訪問調用,但爲什麼又沒有CONSTANT_Fieldref_info定義呢?這因爲該字段被"final static "修飾,javac編譯時字段值將被分配爲ConstantValue屬性存儲在字段表field_info結構的屬性表中(詳情見後面的字段表集合和ConstantValue屬性介紹),而getMap()"方法中直接訪問相應的常量(和"mStaticString"訪問方式不同),如下:
(3)、CONSTANT_Utf8_info結構
該結構在上面兩個結構介紹中已經提到過了(name_index項),Class文件中出現的頻率極高;
用於表示字符串常量的值;
結構如下:
tag:
爲1;
length:
該值指明瞭bytes[]數組的長度,也即字符串長度;
字段、方法名都需要引用該常量類型,所以字段、方法名最大長度是65535;
bytes[]:
UTF-8縮略編碼表示的字符串。
注意,改進的UTF-8縮略編碼與UTF-8編碼的區別:
在範圍'\u0001'至'\u007F'內的字符用1個單字節表示;
字符爲'\u0000'(表示字符'null'),或者在範圍'\u0080'至'\u07FF'的字符用兩字節表示;
在範圍'\u0800'至'\uFFFF'中的字符像普通UTF-8編碼一樣用3個字節表示;
更多UTF-8縮略編碼信息請參考:http://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.4.7
接着前面的ClassFileTest類的常量分析,它索引到第33項Utf8常量類型,數據表示字符串"ClassFileTest",不過可以看到ClassFileTest.class中有兩個位置存儲該字符串,另一個是第27項的"ClassFileTest.java",如下:
我們找到第二個並且驗證前面的"01000D",tag爲"01"符合,length爲"000D"表示後面長度爲13也符合,如下:
上面只介紹了三種常量結構,更多常量結構請參考:http://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.4
3-4、訪問標誌
用於表示這個類或者接口的訪問權限及基礎屬性;
可以用16位掩碼標誌,目前只使用8種(JDK1.5增加後面三種),沒使用的需要設置爲0;
相應標誌及含義如下:
標記名 |
值 |
含義 |
ACC_PUBLIC |
0x0001 |
可以被包的類外訪問。 |
ACC_FINAL |
0x0010 |
不允許有子類。 |
ACC_SUPER |
0x0020 |
當用到 invokespecial 指令時,需要特殊處理的父類方法。 |
ACC_INTERFACE |
0x0200 |
標識定義的是接口而不是類。 |
ACC_ABSTRACT |
0x0400 |
不能被實例化。 |
ACC_SYNTHETIC |
0x1000 |
標識並非 Java 源碼生成的代碼。 |
ACC_ANNOTATION |
0x2000 |
標識註解類型 |
ACC_ENUM |
0x4000 |
標識枚舉類型 |
帶有ACC_INTERFACE標誌的類,意味着它是接口而不是類,反之是類而不是接口;
如果一個 Class 文件被設置了ACC_INTERFACE標誌,那麼同時也得設置ACC_ABSTRACT標誌(JLS §9.1.1.1),同時它不能再設置ACC_FINAL、ACC_SUPER和ACC_ENUM標誌;
ACC_SUPER 標誌用於確定該 Class 文件裏面的 invokespecial 指令(在JDK1.0.2發生過改變)使用的是哪一種執行語義;目前 Java 虛擬機的編譯器都應當設置這個標誌。
在ClassFileTest.class常量池42項數據後面就是訪問標誌數據"0021",對應的標誌爲ACC_PUBLIC和ACC_SUPER,反編譯信息中也明確指出,如下:
3-5、類索引、父類索引與接口索引集合
這三項數據確定這個類的繼承關係;
這些索引都指向CONSTANT_Class_info常量類型數據,我們知道CONSTANT_Class_info裏通過索引指向CONSTANT_Utf8_info常量類型數據,這樣就可以找到當前類、父類、實現接口的全限定名。
1、類索引
用來確定這個類的的全限定名;
在ClassFileTest.class中訪問標誌數據後面就是類索引數據"0007",即引用到ClassFileTest類常量數據,如下:
2、父類索引
用來確定這個類的父類的全限定名;
因爲Java語言不允許多生繼承,所以父類索引只有一個;並且除java.lang.Object外,其他所有類的父類索引都不能爲0;
對於接口來說,索引指向的項必須爲代表java.lang.Object的CONSTANT_Class_info類型常量;
在ClassFileTest.class中類索引數據後面就是父類索引數據"000B",即引用到java/lang/Object類常量數據,如下:
3、接口索引集合
用來描述這個類實現了哪些接口;
implements(接口extends)後實現的按順序從左到右排列在接口索引集合;
如果沒有實現任何接口,interfaces_count爲0,後面不再有interfaces[interfaces_count];
在ClassFileTest.class中父類索引數據後面就是接口索引數據"0000",即沒實現任何接口,如下:
3-6、字段表集合
字段表集合描述當前類或接口聲明的所有字段;
field_info用於表示當前類或接口中某個字段的完整描述,包括類字段(static字段)或實例字段,不包括局部變量,但不包括從父類或父接口繼承的部分字段;
對內部類,編譯器可能自動添加對外部類實例的字段;
field_info 結構格式如下:
access_flags:
是用於定義字段被訪問權限和基礎屬性的掩碼標誌;
和前面類的訪問標誌一樣,沒使用的需要設置爲0;
有些標記是互斥的,如不能同時設置標誌 ACC_FINAL 和 ACC_VOLATILE;
具體標誌及含義如下:
標記名 |
值 |
說明 |
ACC_PUBLIC |
0x0001 |
public,表示字段可以從任何包訪問 |
ACC_PRIVATE |
0x0002 |
private,表示字段僅能該類自身調用 |
ACC_PROTECTED |
0x0004 |
protected,表示字段可以被子類調用 |
ACC_STATIC |
0x0008 |
static,表示靜態字段 |
ACC_FINAL |
0x0010 |
final,表示字段定義後值無法修改(JLS§17.5) |
ACC_VOLATILE |
0x0040 |
volatile,表示字段是易變的 |
ACC_TRANSIENT |
0x0080 |
transient,表示字段不會被序列化 |
ACC_SYNTHETIC |
0x1000 |
表示字段由編譯器自動產生 |
ACC_ENUM |
0x4000 |
enum,表示字段爲枚舉類型 |
name_index:
指向CONSTANT_Utf8_info類型常量的索引;
表示當前字段的非全限定名(簡單名稱);
descriptor_index:
指向CONSTANT_Utf8_info類型常量的索引;
表示當前字段的描述符;
attributes_count:
表示當前字段的附加屬性的數量;
attributes[attributes_count]:
表示當前字段的一些額外的屬性信息;
如"final static"修飾的字段,可能存在ConstantValue的屬性;
泛型字段存在Signature屬性;
詳見下面屬性表集合;
另外前面曾說過,Java語言規定字段無法重載,名稱必須不一樣;但對Class文件來說,只要兩個字段描述(類型)不一樣,名稱一樣也是可以的。
在ClassFileTest.class中接口索引集合數據後面就是字段表集合數據,先是字段表數量數據"0003"表示定義有三個字段"mString"、"mStaticString"以及"mFinalStaticString",如下:
1、mString字段
public String mString = "blog.csdn.net/tjiyu";
字段表數量數據後面就是mString字段數據,如下:
access_flags:"0001"表示ACC_PUBLIC 標誌,即"public"修飾;
name_index:"000C"表示索引第12項的常量,即字段名稱爲"mString",如下:
descriptor_index:"000D"表示索引第13項的常量,即字段描述符爲"Ljava/lang/String;",如上圖;
attributes_count:"0000"表示字段附加屬性的數量爲0,即後面沒有屬性數據了。
2、mStaticString字段
private static String mStaticString = "hello";
mString字段表數據後面就是mStaticString字段數據,如下:
access_flags:"000A"表示ACC_PRIVATE和ACC_STATIC標誌,即被"private"和"static"修飾;
name_index:"000E"表示索引第14項的常量,即字段名稱爲"mStaticString",如圖:
descriptor_index:"000D"表示索引第13項的常量,即字段描述符爲"Ljava/lang/String;",如上圖;
attributes_count:"0000"表示字段附加屬性的數量爲0,即後面沒有屬性數據了。
3、mFinalStaticString字段
private final static String mFinalStaticString = "java";
mStaticString字段表數據後面就是mFinalStaticString字段數據,如下:
access_flags:"001A"表示ACC_FINAL 、ACC_PRIVATE以及ACC_STATIC標誌,即同時被"final"、"private"和"static"修飾;
name_index:"000F"表示索引第16項的常量,即字段名稱爲"mFinalStaticString",如圖:
descriptor_index:"000D"表示索引第13項的常量,即字段描述符爲"Ljava/lang/String;",如上圖;
attributes_count:"0001"表示字段附加屬性的數量爲1,即後面有一項屬性數據;
attributes[]:這就是在上面介紹常量池最後分析說到的ConstantValue屬性;ConstantValue屬性包括兩個u2和一個u4類型數據,即爲"0010 00000002 0008",ConstantValue屬性詳見後面介紹。
更多字段表集合信息請參考:http://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.5
3-7、方法表集合
methods[]數組只描述當前類或接口中聲明的方法;
包括實例方法、類方法、實例初始化方法方法和類或接口初始化方法方法;但不包括從父類或父接口繼承的方法;
可能存在編譯自動添加的方法,如類構造器"<clinit>"和實例構造器"<init>";
method_info用於表示當前類或接口中某個方法的完整描述;
如attributes[]中可能存在"Code"屬性,表示方法邏輯代碼編譯後的字節碼指令;
method_info 結構格式如下:
access_flags:
用於定義當前方法的訪問權限和基本屬性的掩碼標誌;
和前面類的訪問標誌一樣,沒使用的需要設置爲0;
有些標記是互斥的,如不能同時設置標誌 ACC_FINAL 和 ACC_ABSTRACT;
具體標誌及含義如下:
標記名 |
值 |
說明 |
ACC_PUBLIC |
0x0001 |
public,方法可以從包外訪問 |
ACC_PRIVATE |
0x0002 |
private,方法只能本類中訪問 |
ACC_PROTECTED |
0x0004 |
protected,方法在自身和子類可以訪問 |
ACC_STATIC |
0x0008 |
static,靜態方法 |
ACC_FINAL |
0x0010 |
final,方法不能被重寫(覆蓋) |
ACC_SYNCHRONIZED |
0x0020 |
synchronized,方法由管程同步 |
ACC_BRIDGE |
0x0040 |
bridge,方法由編譯器產生 |
ACC_VARARGS |
0x0080 |
表示方法帶有變長參數 |
ACC_NATIVE |
0x0100 |
native,方法引用非java語言的本地方法 |
ACC_ABSTRACT |
0x0400 |
abstract,方法沒有具體實現 |
ACC_STRICT |
0x0800 |
strictfp,方法使用FP-strict浮點格式 |
ACC_SYNTHETIC |
0x1000 |
方法在源文件中不出現,由編譯器產生 |
name_index:
指向CONSTANT_Utf8_info類型常量的索引;
表示當前方法的非全限定名(簡單名稱);
descriptor_index:
指向CONSTANT_Utf8_info類型常量的索引;
表示當前方法的描述符;
attributes_count:
表示當前方法的附加屬性的數量;
attributes[attributes_count]:
表示當前方法的一些屬性信息;
如重要的"Code"屬性,表示方法編譯後的字節碼指令;
有泛型參數存在Signature屬性;
詳見下面屬性表集合;
另外,前面曾說過,java語言重載(Overload)一個方法,需要特徵簽名不同;而Class文件中特徵簽名範圍更大,包括方法返回值。
在ClassFileTest.class中字段表集合的mFinalStaticString字段數據後面就是方法表集合數據,先是方法表數量數據"0003"表示有三個方法:類構造器"<clinit>"、實例構造器"<init>"以及自定義的"getMap"方法,如下:
1、實例構造器"<init>"
先是實例構造器"<init>()"方法,這是在前文《Java前端編譯:Java源代碼編譯成Class文件的過程》提到過的:對於實例構造器<init>(),如果程序代碼中定義有構造函數,它在解析的語法分析階段被重命名爲<init>();如果沒有定義構造函數,則實例構造器<init>()是在填充符號表時添加的;並把需要初始化的變量以及需要執行的語句塊添加到相應的構造器中。
方法表數量數據後面就是實例構造器"<init>"數據,如下:
access_flags:"0001"表示ACC_PUBLIC 標誌,即"public"修飾;
name_index:"0011"表示索引第17項的常量,即方法名稱爲"<init>",如下:
descriptor_index:"0012"表示索引第18項的常量,即字段描述符爲"()V",如上圖;
attributes_count:"0001"表示字段附加屬性的數量爲1,即後面有一項屬性數據;
attributes[]:這就是重要的"Code"屬性,表示方法編譯後的字節碼指令
"Code"屬性詳見後面屬性集合介紹,這裏看下該方法的反編譯信息,可以看到Code屬性中除了字節碼指令,後面還包含程序行號信息LineNumberTable屬性,如下:
(A)、"super()"調用"
可以看到前面介紹第一項常量時說過的"invokespecial #1",先調用Object類型的實例構造器"<init>",這相當於"super()"調用(前面文章也說過是編譯器自動添加的);
(B)、初始化"mString"字段
"ldc #2":加載第2項常量字符串"blog.csdn.net/tjiyu"到操作棧;
"putfield #3":初始化"mString"字段--將操作棧頂的字符串"blog.csdn.net/tjiyu"賦值給字段"mString";
(C)、對實例對象的操作
至於"aload_0"表示把局部變量表中的第0項"this"實例加載到操作棧,也即"<init>"中的"super()"調用"和初始化字段"mString"都是對該實例對象進行的操作,所以叫做實例構造器;
關於JVM指令集,篇幅有限,後面有時間另外介紹,可以先參考:
《JVM規範 JavaSE8版》2.11 字節碼指令集簡介:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.11
《JVM規範 JavaSE8版》第6章 Java虛擬機指令集:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html
2、"getMap"方法
這是程序中自定義的方法,實例構造器"<init>"數據後面就是"getMap"方法數據,如下:
access_flags:"0001"表示ACC_PUBLIC標誌,即被"public"修飾;
name_index:"0015"表示索引第21項的常量,即方法名稱爲"getMap",如下:
descriptor_index:"0016"表示索引第22項的常量,即字段描述符爲"()Ljava/util/Map;",如上圖;
attributes_count:"0002"表示字段附加屬性的數量爲2,即後面有兩項屬性數據;
attributes[]:包括重要的"Code"屬性--表示方法編譯後的字節碼指令,以及"Signature"屬性--表示該方法是一個泛型方法,記錄的是被編譯器擦除的泛型簽名信息。
"Code"屬性和"Signature"屬性詳見後面屬性集合介紹,這裏先看下該方法的反編譯信息,可以看到Code屬性中除了字節碼指令,後面還包含程序行號信息LineNumberTable屬性,如下:
(A)、對兩個字段變量的訪問方式不同
這裏需要注意的是:getMap()"方法中"map.put(mStaticString, mFinalStaticString);"對兩個字段變量的訪問方式不同:對"mStaticString"需要通過"getstatic #6"來將該字段的值取出,並推入到操作數棧頂;而對"mFinalStaticString"則通過"ldc #8"直接將第8項常量"java"字符串加載到操作棧頂;然後再執行"invokeinterface #9, 3"即"map.put()"接口方法;
爲什麼對"mFinalStaticString"的訪問直接引用到"java"字符串?因爲"final"的不變性。
方法其他指令的執行也不說了,以後有時間再另寫文章詳細介紹。
3、類構造器"<clinit>"
對於類構造器"<clinit>"方法,也在前文《Java前端編譯:Java源代碼編譯成Class文件的過程》"4-3、字節碼生成"提到過的:類構造器"<clinit>"在轉換字節碼前由編譯自動添加,提前是有需要初始化執行的類變量和塊(static)。
"getMap"方法表數據後面就是類構造器"<clinit>""數據(結束),如下:
access_flags:"0008"表示ACC_STATIC標誌,即被"static"修飾,是靜態方法;
name_index:"0019"表示索引第25項的常量,即方法名稱爲"<clinit>",如下:
descriptor_index:"0012"表示索引第18項的常量,即字段描述符爲"()V",如上圖;
attributes_count:"0001"表示字段附加屬性的數量爲1,即後面有一項屬性數據;
attributes[]:這就是重要的"Code"屬性,表示方法編譯後的字節碼指令
"Code"屬性詳見後面屬性集合介紹,這裏看下該方法的反編譯信息,如下:
(A)、初始化"mStaticString"字段
"ldc #10":加載第2項常量字符串"hello"到操作棧;
"putstatic #6":初始化"mStaticString"字段--將操作棧頂的字符串"hello"賦值給字段"mStaticString";
(B)、對類變量和塊(static)的操作
這裏並不需要像在實例構造器"<init>"中的"aload_0",因爲"<clinit>"不是對實例對象進行操作,而是對類變量和塊(static)的操作,所以叫做類構造器;
"mFinalStaticString"字段的初始化問題:
另外需要注意,"mString"字段變量在實例構造器"<init>"中初始化,而"mStaticString"字段變量在類構造器"<clinit>"中初始化,那"mFinalStaticString"字段變量的初始化呢?從前面的描述和反編譯信息中都沒有看到;這就要從"mFinalStaticString"字段擁有的ConstantValue屬性說起了,詳見後面的ConstantValue屬性介紹。
所以,前面一直說的類構造器"<clinit>"中對類變量和塊操作,這類變量除需要是"static"外,還需"非final"。
更多方法表集合信息請參考:http://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.6
到這裏ClassFileTest.class的介紹基本結束了,但是在上面很多結構中最後存在"attribute_info attributes[]",這並沒有太詳細介紹;下面來看看屬性表集合都有些什麼。
3-8、屬性表集合
用於描述某些場景專有的信息;
集合中各屬性表沒有嚴格的順序;
可以自定義屬性信息,JVM會忽略不認識的屬性表;
在Class文件的 ClassFile結構、字段表、方法表中都可以存儲放自己的屬性表集合,所以並不像最前面那Class文件結構那麼直觀,即屬性不都是放在Class文件的最後,而各屬性可以存放的位置如下:
JDK1.7中增加到20項屬性類型,JDK1.8中增加到23項屬性類型(RuntimeVisibleTypeAnnotations、RuntimeInvisibleTypeAnnotations、MethodParameters),如下:
五個屬性對於Java虛擬機正確解釋Class文件至關重要:
ConstantValue、Code、StackMapTable、Exceptions、BootstrapMethods;
十二個屬性對於通過Java SE平臺的類庫來正確解釋Class文件至關重要:
InnerClasses、EnclosingMethod、Synthetic、Signature、RuntimeVisibleAnnotations、RuntimeInvisibleAnnotations、RuntimeVisibleParameterAnnotations、RuntimeInvisibleParameterAnnotations、RuntimeVisibleTypeAnnotations、RuntimeInvisibleTypeAnnotations、AnnotationDefault、MethodParameters;
六個屬性對於通過Java虛擬機或Java SE平臺的類庫來正確解釋Class文件並不重要,但對於工具非常有用:
SourceFile、SourceDebugExtension、LineNumberTable、LocalVariableTable、LocalVariableTypeTable、Deprecated;
屬性(attribute_info)的通用格式如下:
attribute_name_index:
指向CONSTANT_Utf8_info常量的索引,表示屬性的名稱;
attribute_length:
給出了跟隨其後的屬性信息的字節長度;
info:
屬性信息,各種屬性結構不同;
1、Code屬性
Java方法體中的代碼經過javac編譯處理,生成字節碼的指令信息;
出現在方法表的屬性集合中,也可能沒有(如方法被聲明爲native或者abstract類型);
Code 屬性的格式如下:
attribute_name_index:
指向CONSTANT_Utf8_info類型常量的索引,常量中固定字符串"Code";
attribute_length:
表示當前屬性的長度,不包括開始的 6 個字節;
max_stack:
給出了當前方法的操作數棧的最大深度(在運行執行的任何時間點都不超過),JVM運行時根據這個值分配棧幀(Stack Frame)中的操作棧深度;
max_locals:
給出了分配在當前方法引用的局部變量表中的局部變量個數,包括調用此方法時用於傳遞參數的局部變量;
其實是局部變量的存儲空間大小,單位是Slot(JVM爲局部變量分配內存的最小單位);
long和double類型的局部變量的佔兩個Slot,其它類型的局部變量的只佔一個.
執行超出局部變量作用域後,變量佔的Slot空間可以被其他變量重用;
code_length:
code_length項給出了當前方法的 code[]數組的字節數;
code_length雖然是u4類型,但0<code_length<65536,即code[]數組不能爲空,也不能超過65536,所以程序中方法體不能寫得過長;
code[]:
存儲實現當前方法的JVM字節碼指令;
一個code是u1類型,最大可以表示256個指令;
目前JVM規範已經定義了其中約200條編譯值對應的指令含義;
詳見後面的字節碼指令簡介;或官方指令說明請參考:http://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.9
exception_table_length:
exception_table[exception_table_length]:
數組的每個成員表示code[]數組中的一個異常處理器(Exception Handler);
start_pc 和 end_pc:
兩項的值表明了異常處理器在 code[]數組中的有效範圍;
handler_pc:
表示一個異常處理器的起點,它的值必須同時是一個對當前 code[]數組中某一指令的操作碼的有效索引;
catch_type:
指向CONSTANT_Class_info類型常量的索引,表示要捕獲異常的類型;
爲0表示捕獲所有類型的異常,可以用來實現finally語句;
attributes_count:
attributes[attributes_count]:
一個Code屬性可以有任意數量的可選屬性與之關聯;
只能是LineNumberTable(程序行號信息)、LocalVariableTable(棧上變量信息)、LocalVariableTypeTable和StackMapTable屬性;
在前面方法表集合中說到在ClassFileTest.class文件中的三個方法都存在Code屬性,這裏拿自定義的"getMap"方法來說明,其中attributes_count:"0002"表示後面有兩項屬性數據,前面的是"Code"屬性,後面的是"Signature"屬性,"Code"屬性數據如下;
attribute_name_index:"0013"索引第19項的常量,即屬性名稱爲"Code";
attribute_length:"00000036"表示當前屬性後面內容的長度爲54;
max_stack:"0003"表示當前方法的操作數棧的最大深度爲3;
max_locals:"0002"表示分配在當前方法引用的局部變量表中的局部變量個數爲2("this"和"map"變量,"this"作爲參數傳入);
code_length:"00000016"當前方法的code[]數組的字節數爲22;
code[]:22個字節的當前方法的JVM字節碼指令數據如下:
(A)、"BB0004":其中"BB"查詢JVM指令集表可以看到,"new"指令後面接一個操作數,即表示指令"new #4";
(B)、"59":查表得知表示"dup"指令,後面沒有操作數;
(C)、"B70005":其中"B7"查指令集表得知爲"invokeinterface",後面接一個操作數,即表示指令"invokespecial #5";
同理,這些都可以在反編譯信息中清楚看到…..
exception_table_length:"0000"表示沒有異常處理器,後面沒有exception_table[];
attributes_count:"0001"表示當前Code屬性中有一個附加屬性;
attributes[]:通過反編譯信息看到這個附加屬性是LineNumberTable,表示程序行號信息;LineNumberTable屬性就不再多說了(下面有介紹),表示的數據如下:
再次給出""方法的反編譯信息,可以驗證上面的數據說明,如下:
2、Signature屬性
JDK1.5後引入泛型後增加的,僅僅對泛型類型有意義;
可以位於類、方法或字段表的屬性集合中;
任何類、接口、初始化方法或字段成員,如果包含了類型變量(Type Variables)或參數化類型(Parameterized Types) ,則Signature屬性會爲它記錄泛型簽名信息;
因爲泛型在編譯階段類型被擦除,像字段表、方法表中的描述符記錄的是類型擦除後的信息,使得無法在運行階段做反射時獲得實際類型;
Signature 屬性格式如下:
attribute_name_index:
指向CONSTANT_Utf8_info類型常量的索引,表示字符串"Signature";
attribute_length:
必須爲2;
signature_index:
指向CONSTANT_Utf8_info類型常量的索引,表示類簽名或方法類型簽名或字段類型簽名;
這裏接着上面的"getMap"方法分析(三個方法也只有"getMap"是泛型方法),前面分析了"Code"屬性,後面的"Signature"屬性數據如下;
attribute_name_index:"0017"表示索引第23項常量,即屬性名稱爲"Signature",如下圖:
attribute_length:"00000002"必須爲2;
signature_index:"0018"表示索引第24項常量,表示未擦除類型前的方法類型簽名"()Ljava/util/Map<Ljava/lang/String;Ljava/lang/String;>;",如上圖;而擦除後的在前面方法表介紹的方法描述符中,爲"()Ljava/util/Map;",如上圖第22項;
更多關於泛型編譯類型擦除請參考上文:《Java前端編譯:Java源代碼編譯成Class文件的過程》4-2、解語法糖
前面有關於簽名與描述符之於泛型的說明;或參考官方說明:http://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.7.9.1
3、ConstantValue屬性
表示一個常量字段的值;
只有static關鍵字修飾(對javac編譯還得有"final")的變量纔有這個屬性;
位於field_info結構的屬性表中;
存在則說明這個field_info結構表示的常量字段值將被分配爲它的ConstantValue屬性表示的值;
用於通知JVM自動爲靜態變量賦值;
使用ConstantValue屬性賦值過程發生在引用類或接口的類初始化方法<clinit>()執行之前;
ConstantValue 屬性的格式如下:
attribute_name_index:
指向CONSTANT_Utf8_info類型常量的索引,表示字符串"ConstantValue";
attribute_length:
固定爲2;
constantvalue_index:
必須是一個對常量池的有效索引;
常量池在該索引處的項給出該屬性表示的常量值;
常量池的項的類型表示的字段類型,可以是CONSTANT_Long、CONSTANT_FloatCONSTANT_Double、CONSTANT_Integer(int,short,char,byte,boolean)、CONSTANT_String;
從上面字段表集合和方法表集合中對於測試程序中的三個變量的分析,可以知道只有"mFinalStaticString"字段存在ConstantValue屬性,實際數據是""0010 00000002 0008""(見3-6字段表集合第3點):
attribute_name_index:"0010"表示索引第16項常量,即屬性名爲"ConstantValue";
attribute_length:"00000002"固定爲2;
constantvalue_index:"0008"表示索引第8項常量,給出該屬性的常量值"java",如下圖:
總結前面這些描述(包括在字段表、方法表集合的分析),得出:
對於非static變量(實例變量"mString"),其賦值是在實例構造器<init>方法中進行的;
對於static變量(類變量"mStaticString"和"mFinalStaticString"),有兩種賦值方式:
(1)、使用類構造器<clinit>
沒有使用"final"修飾,或不是基本類型、String類型的變量,如"mStaticString";
(2)、使用ConstantValue屬性
需要同時使用"final static"修飾的基本類型或String類型的變量,如"mFinalStaticString";
JVM規範並沒有要求使用"final"修飾才能使用ConstantValue屬性,這是javac編譯器的限制;
關於使用ConstantValue屬性初始化請參考類加載的初始化過程第6點:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-5.html#jvms-5.5
4、Exceptions屬性
注意與前面Code屬性中的exception_table項的區別;
指出了一個方法需要檢查的可能拋出的異常;
一個method_info結構中最多只能有一個Exceptions屬性;
異常類型的要求沒有在JVM中進行強制檢查,它們只在編譯時進行強制檢查;
Exceptions 屬性格式如下:
attribute_name_index:
指向CONSTANT_Utf8_info類型常量的索引,表示字符串"Exceptions";
number_of_exceptions:
一個方法可能拋出多個異常;
exception_index_table[number_of_exceptions]:
一個成員是一個指向CONSTANT_Class_info類型常量的索引,表示要捕獲異常的類型;
5、LineNumberTable屬性
用於描述Java源碼行號與字節碼偏移量之間的對應關係;
位於Code屬性的屬性表集合中,可以按照任意順序出現;
不是運行必須的;
默認不生成到Class文件中,可以javac -g:none取消或-g:lines要求生成;
不生成的話影響調試,如拋出異常時堆棧中不顯示出錯行號,也無法設置斷點;
LineNumberTable 屬性格式如下:
attribute_name_index:
指向CONSTANT_Utf8_info類型常量的索引,表示字符串"LineNumberTable";
line_number_table:
每個成員都表明源文件中行號的變化,在code[]數組中都會有對應的標記點;
start_pc:
必須是code[]數組的一個索引,code[]數組在該索引處的字符表示源文件中新的行的起點;
line_number:
必須與源文件的行號相匹配;
6、LocalVariableTable屬性及LocalVariableTypeTable屬性
用於描述棧幀中局部變量表中的變量與Java源碼定義的變量之間的關係;
被調試器用於確定方法在執行過程中局部變量的信息;
位於Code屬性的屬性表集合中,可以按照任意順序出現;
每個局部變量最多只能有一個LocalVariableTable屬性;
不是運行必須的;
JDK8默認不會生成到Class文件中,可以javac -g:none取消或-g:vars要求生成;
不生成的話,對編寫代碼影響較大,如其他人引用這個方法時,所有參數名稱會被如arg0、arg1之類的點位符代替;而且在調試期間無法根據參數名稱從上下文中獲得參數值;
LocalVariableTable 屬性格式如下:
attribute_name_index:
指向CONSTANT_Utf8_info類型常量的索引,表示字符串"LocalVariableTable";
local_variable_table[]:
數組的每一個成員表示一個局部變量的值在code[]數組中的偏移量範圍(作用域); 它同時也是用於從當前幀的局部變量表找出所需的局部變量的索引;
start_pc, length:
局部變量作用域範圍在Code屬性的[start_pc, start_pc+length)偏移量中;
name_index:
指向CONSTANT_Utf8_info類型常量的索引,表示一個局部變量的有效的非全限定名;
descriptor_index:
指向CONSTANT_Utf8_info類型常量的索引,表示源程序中局部變量類型的字段描述符;
index:
爲此局部變量在當前棧幀的局部變量表中的索引(Slot),long或double類型,則佔用兩個Slot空間;
有一個相似的屬性LocalVariableTypeTable;
LocalVariableTypeTable:
JDK1.5後引入泛型後增加的,僅僅對泛型類型有意義;
因爲LocalVariableTable屬性只能描述泛型類型擦除後的信息;
泛型類型的局部變量,屬性會同時存在LocalVariableTable屬性和LocalVariableTypeTable屬性;
與LocalVariableTable屬性不同之處在於:提供簽名信息而不是描述符信息;
LocalVariableTypeTable 屬性格式如下:
signature_index:
指向CONSTANT_Utf8_info類型常量的索引,表示源程序中局部變量泛型類型的字段簽名;
7、SourceFile屬性
用於記錄生成這個Class文件的源碼文件名稱;
不包括源文件所在目錄的目錄名,也不包括源文件的絕對路徑名;
不是運行必須的;
默認生成到Class文件中,可以javac -g:none取消或-g:source要求生成;
通常類名和文件名是一致的,但內部類不生成的話,拋出異常時堆棧不顯示出錯代碼所屬的文件名;
SourceFile 屬性格式如下:
attribute_name_index:
指向CONSTANT_Utf8_info類型常量的索引,表示字符串"SourceFilee";
sourcefile_index:
指向CONSTANT_Utf8_info類型常量的索引,表示源碼文件名稱;
8、InnerClasses屬性
在JDK 1.1中爲了支持內部類和內部接口而引入的;
用於記錄內部類與宿主類之間的關聯;
位於ClassFile結構的屬性表;
如果一個類定義了內部類,編譯器會爲它以及它的內部類生成InnerClasses屬性;
內部類在的常量池中對應的CONSTANT_Class_info不屬於任何一個包,而是在InnerClasses屬性中引用;
所以,內部類被定義的外部類(Enclosing Class)中都會包含有它們的InnerClasses信息;
InnerClasses屬性 屬性的格式如下:
attribute_name_index:
指向CONSTANT_Utf8_info類型常量的索引,表示字符串"InnerClasses";
number_of_classes:
內部類的數量;
classes[]:
內部類與宿主類之間的關聯信息;
inner_class_info_index:
指向CONSTANT_Class_info類型常量的索引,表示內部類的符號引用;
outer_class_info_index:
指向CONSTANT_Class_info類型常量的索引,表示外部類的符號引用;
inner_name_index:
指向CONSTANT_Utf8_info類型常量的索引,表示這個內部類的簡單名稱,如果爲匿名內部類,值爲0;
inner_class_access_flags:
一個掩碼標誌,表示內部類的訪問權和基本屬性;
標記名 |
值 |
含義 |
ACC_PUBLIC |
0x0001 |
源文件定義public |
ACC_PRIVATE |
0x0002 |
源文件定義private |
ACC_PROTECTED |
0x0004 |
源文件定義protected |
ACC_STATIC |
0x0008 |
源文件定義static |
ACC_FINAL |
0x0010 |
源文件定義final |
ACC_INTERFACE |
0x0200 |
源文件定義interface |
ACC_ABSTRACT |
0x0400 |
源文件定義abstract |
ACC_SYNTHETIC |
0x1000 |
聲明synthetic,非源文件定義 |
ACC_ANNOTATION |
0x2000 |
聲明annotation |
ACC_ENUM |
0x4000 |
聲明enum |
9、Deprecated及Synthetic屬性
標誌類型的布爾屬性,沒有屬性值;
Deprecated屬性:
在JDK 1.1爲了支持註釋中的關鍵詞@deprecated而引入的;
表示當前這個類、字段或方法,被程序作者定爲不再推薦使用;
可以在代碼中使用@deprecated註釋設置;
Synthetic屬性:
Synthetic 屬性是在JDK 1.1中爲了支持內部類或接口而引入的;
表示當前這個字段或方法不是由源碼產生的,而是由編譯器自動添加的;
由編譯器自動添加的還可以設置訪問標誌的ACC_SYNTHETIC標誌位,這兩項(ACC_SYNTHETIC標誌位和Synthetic屬性)至少有一項;
Deprecated和Synthetic屬性的格式如下:
attribute_name_index:
指向CONSTANT_Utf8_info類型常量的索引,表示字符串"Deprecated"("Synthetic");
attribute_length:
值固定爲0;
10、StackMapTable屬性
在JDK1.6增加到規範;
位於Code屬性中;
在編譯階段記錄一系列的驗證類型信息到Class文件中;
在虛擬機類加載的類型階段被使用;
使JVM類加載器可以使用新的性能更好的類型檢查驗證器(Type Checker),代替原來比較消耗性能的數據流分析的類型推導器;
StackMapTable 屬性包含 0 至多個棧映射幀(Stack Map Frames),每個棧映射幀都顯式或隱式地指定了一個字節碼偏移量,用於表示局部變量表和操作數棧的驗證類型(Verification Types);
類型檢測器(Type Checker)會檢查和處理目標方法的局部變量和操作數棧所需要的類型;
StackMapTable 屬性的格式如下:
attribute_name_index:
指向CONSTANT_Utf8_info類型常量的索引,表示字符串"StackMapTable";
entries[]:
給出了當前方法所需的 stack_map_frame 結構;
更多棧映射幀信息,請參考:http://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.7.4
11、BootstapMethods屬性
JDK1.7唯一新增的屬性,爲支持動態類型語言;
用於保存invokedynamic指令引用的引導方法限定符;
位於 ClassFile結構的屬性表中;
如果常量池中有至少一個CONSTANT_InvokeDynamic_info,就必須有一個BootstrapMethods屬性,但最多只能有一個;
BootstrapMethods 屬性格式如下:
attribute_name_index:
指向CONSTANT_Utf8_info類型常量的索引,表示字符串"BootstrapMethods";
bootstrap_methods[]:
每個成員代表了一個引導方法及其靜態參數的序列;
bootstrap_method_ref:
指向CONSTANT_MethodHandle_info類型常量(其中的reference_kind項應爲6或8)的索引,表示引導方法的句柄;
bootstrap_arguments[]:
表示靜態參數的序列,可以是指向以下類型常量的索引:CONSTANT_String_info,CONSTANT_Class_info、CONSTANT_Integer_info,CONSTANT_Long_info、CONSTANT_Float_info,CONSTANT_Double_info、CONSTANT_MethodHandle_info 或 CONSTANT_MethodType_info;
更多屬性說明,請參考:http://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.7
到這裏,我們大體瞭解Java Class文件結構,並用測試程序編譯Class文件來分析驗證了Class文件結構。
後面我們將分別去了解: Class文件方法表"Code"屬性中的JVM指令集、以及JIT編譯--在運行時把Class文件字節碼編譯成本地機器碼的過程……
【參考資料】
1、《The Java Virtual Machine Specification》Java SE 8 Edition:https://docs.oracle.com/javase/specs/jvms/se8/html/index.html
2、《深入理解Java虛擬機:JVM高級特性與最佳實踐》第二版 第6章
3、《The Java Language Specification》Java SE 8 Edition:https://docs.oracle.com/javase/specs/jls/se8/html/index.html
4、《深入分析Java Web技術內幕》修訂版 第5章