Class檔案格式採用一種類似C語言結構體的偽結構來儲存數據,這種偽結構中只有兩種資料類型:「無符號數」和「表」。
·無符號數屬於基本的資料型別,以u1、u2、u4、u8來分別代表1個位元組、2個位元組、4個位元組和8個位元組的無符號數,無符號數可以用來描述數字、索引引用、數量值或依照UTF-8編碼構成字串值。
·表是由多個無符號數或其他表作為資料項構成的複合資料類型,為了便於區分,所有表的命名都習慣性以「_info」結尾。表用來描述有層次關係的複合結構的數據,整個Class檔案本質上也可以視為一張表格
每個Class檔案的頭4個字節稱為魔數(Magic Number),它的唯一作用是確定這個檔案是否為一個能被虛擬機器接受的Class檔案。不只是Class文件,許多文件格式標準都有使用魔數來進行識別的習慣,譬如圖片格式,如GIF或JPEG等在文件頭中都存有魔數。
Class檔案的魔數取得很有“浪漫氣息”,值為0xCAFEBABE(咖啡寶貝?)
緊接著魔數的4個位元組儲存的是Class檔案的版本號:第5和第6個位元組是次版本號(MinorVersion),第7和第8個位元組是主版本號(Major Version)。 Java的版本號是從45開始的,JDK 1.1之後的每個JDK大版本發布主版本號向上加1(JDK 1.0~1.1使用了45.0~45.3的版本號),高版本的JDK能向下兼容以前版本的Class文件,但不能運行以後版本的Class文件,
常數池
緊接著主、次版本號之後的是常量池入口,常數池可以比喻為Class文件裡的資源倉庫,它是Class文件結構中與其他項目關聯最多的數據,通常也是佔用Class文件空間最大的數據項目之一,另外,它還是在Class文件中第一個出現的表類型資料項目。
常數池中主要存放兩大類常數:字面量(Literal)和符號引用(Symbolic References)。字面量比較接近Java語言層面的常數概念,如文字字串、被宣告為final的常數值等。而符號引用則屬於編譯原理方面的概念,主要包括下面幾類常數:
·被模組導出或開放的套件(Package)
·類別和介面的全限定名稱( Fully Qualified Name)
·欄位的名稱和描述符(Descriptor)
#·方法的名稱和描述符
·方法句柄和方法類型(Method Handle、Method Type、Invoke Dynamic)
·動態呼叫點和動態常數(Dynamically-Computed Call Site、Dynamically-Computed Constant)
Java程式碼在進行Javac編譯的時候,並不像C和C 那樣有「連線」這一步驟,而是在虛擬機器載入Class檔案的時候進行動態連線(具體見第7章)。也就是說,在Class文件中不會保存各個方法、字段最終在內存中的佈局信息,這些字段、方法的符號引用不經過虛擬機在運行期轉換的話是無法得到真正的內存入口地址,也就無法直接被虛擬機器使用的。當虛擬機器做類別載入時,將會從常數池取得對應的符號引用,再在類別建立時或執行時解析、翻譯到特定的記憶體位址之中。
常量池中每一項常數都是一個表,最初常量表中共有11種結構各不相同的表結構數據,後來為了更好地支持動態語言調用,額外增加了4種動態語言相關的常數[1] ,為了支援Java模組化系統(Jigsaw),又加入了CONSTANT_Module_info和CONSTANT_Package_info兩個常數,所以截至JDK13,常量表中分別有17種不同類型的常數。
順便提一下,由於Class檔案中方法、欄位等都需要引用CONSTANT_Utf8_info型常數來描述名稱,所以CONSTANT_Utf8_info型常數的最大長度也就是Java中方法、欄位名的最大長度。而這裡的最大長度就是length的最大值,既u2型能表達的最大值65535。所以Java程式中如果定義了超過64KB英文字元的變數或方法名,即使規則和全部字元都是合法的,也會無法編譯。
Classfile /D:/BaiduYunDownload/geekbang-lessons/thinking-in-spring/validation/target/classes/org/geekbang/thinking/in/spring/validation/TestClass.class
Last modified 2020-6-25; size 439 bytes
MD5 checksum 18760ee8065f9fb68d4dab7bd7450c4c
#class# . .validation.TestClass minor version: 0# major version: 52 flags: ACC_PUBLIC, ACC_SUPER##pool:##Constant pool:#1 = Methodref #4.#18 /// 1/lang/Object."< #19 // org/geekbang /thinking/in/spring/validation/TestClass.m:I
#3 = 類別 #20 ## #4 =類別 #21 ///java/lang/Object
#6 = Utf8 I
#7 = Utf8
#8 = Utf8 ()V #9 = Utf8 LineNumberTable# #11 = Utf8 LocalVariableTableLocalVariableTable
# 12 = Utf8 此 #13 = Utf8 # 8 inc #15 = Utf8 ) I #16 = Utf8 SourceFile# #17 = Utf8 # #17 = Utf8 # #17 = U28 #7:#8 // "## #20 = U198 /in/spring/validation/TestClass
#21 = Utf8 java/lang/Object
##{# public org.geekbang.thinking.in.spring.## public org.geekbang.thinking.in.spring. ) V
標誌:ACC_PUBLIC
代碼:
stack=1,loclocals=1,args_size load
# 1: invokespecial #1 // 方法java/lang/Object."## # # 第3 行0
LocalVariableTable:
開始 長度 槽 名詞 0 this Lorg/geekbang/thinking/in/spring/validation/TestClass;
public int inc();
描述子:()I
標誌:ACC_PUBLIC
代碼:
### 代碼:
## ,args_size= 1 0:aload_01: getfield #2 5: iadd
# 6:ireturn
LineNumberTable:
line 7: 0
# LocalVariableTable:
0 7 0 this Lorg/geekbang/thinking/in/spring/validation/ TestClass;
}
SourceFile: "TestClass.java"
#在常數池結束之後,緊接著的2個位元組代表存取標誌(access_flags),這個標誌用於識別一些類別或介面層次的存取訊息,包括:這個Class是類別還是介面;是否定義為public類型;是否定義為abstract類型;如果是類別的話,是否被宣告為final;
類別索引、父類別索引和介面索引集合都依序排列在存取標誌之後,類別索引和父類別索引以兩個u2類型的索引值表示,它們各自指向一個類型為CONSTANT_Class_info的類別描述符常數,透過CONSTANT_Class_info類型的常數中的索引值可以找到定義在CONSTANT_Utf8_info類型的常數中的全限定名字串。
直到JDK 8中Lambda表達式和介面預設方法的出現,InvokeDynamic指令才算在Java語言產生的Class檔案中有了用武之地
所以JDK 8中新增的這個屬性,使得編譯器可以
(編譯時加上-parameters參數)將方法名稱也寫入Class檔案中,而且MethodParameters是方法表的屬
性,與Code屬性平級的,可以在運行時透過反射API取得。
·將一個局部變數載入到操作堆疊:iload
·將一個數值從運算元堆疊到局部變數表:istore
·將一個常數載入到操作數堆疊:bipush
iload_
·加法指令:iadd、ladd、fadd、dadd
##·加法指令:iadd、ladd、fadd、dadd
#·減法指令:isub、lsub、fsub、dsub
·乘法指令:imul、lmul、fmul、dmul
·除法指令:idiv、ldiv、fdiv、ddiv
·求餘指令:irem、lrem、frem、drem·取反指令:ineg、lneg、fneg、dneg
##·位移指令:ishl、ishr、iushr、 lshl、lshr、lushr·位元或指令:ior、lor·位元與指令:iand、land·位元異或指令:ixor、 lxor·局部變數自增指令:iinc#·比較指令:dcmpg、dcmpl、fcmpg、fcmpl、lcmpJDK 1.0.2時改動過invokespecial指令的語意,JDK 7增加了invokedynamic指令,禁止了ret和jsr指令。 #########類別的生命週期### ######載入-> 連線(驗證,準備,解析)->初始化->使用->卸載。 ######載入、驗證、準備、初始化和卸載這五個階段的順序是確定的,類型的載入過程必須按照這種順序按部就班地開始,而解析階段則不一定:它在某些情況下可以在初始化階段之後再開始,這是為了支援Java語言的執行時間綁定特性(也稱為動態綁定或晚期綁定)。 ######public static final int value = 123;######編譯時Javac將會為value產生ConstantValue屬性,在準備階段虛擬機器就會根據Con-stantValue的設定將value賦值為123。 ######雙親委派模型的工作過程是:如果一個類別載入器收到了類別載入的請求,它首先不會自己去嘗試載入這個類,而是把這個請求委派給父類載入器去完成,每一個層次的類別載入器都是如此,因此所有的載入請求最終都應該傳送到最頂層的啟動類別載入器中,只有當父載入器回饋自己無法完成這個載入請求(它的搜尋範圍中沒有找到所需的類別)時,子載入器才會嘗試自己去完成載入#######首先,是擴充類別載入器(Extension Class Loader)被平台類別載入器(Platform Class Loader)取代。這其實是一個很順理成章的變動,既然整個JDK都基於模組化進行構建(原來的rt.jar和tools.jar被拆分成數十個JMOD文件),其中的Java類庫就已天然地滿足了可擴展的需求,那自然無須再保留所有依賴靜態型別來決定方法執行版本的分派動作,都稱為靜態分派。靜態分派最典型的應用表現就是方法重載。靜態分派發生在編譯階段,因此確定靜態分派的動作實際上不是由虛擬機來執行的,這點也是為何一些資料選擇把它歸入“解析”而不是“分派”的原因。
在Java虛擬機器支援以下5個方法呼叫字節碼指令,分別是:
·invokestatic。用於呼叫靜態方法。
·invokespecial。用於呼叫實例建構器
·invokevirtual。用於呼叫所有的虛擬方法。
·invokeinterface。用於呼叫介面方法,在運行時會再確定一個實作該介面的物件。
·invokedynamic。先在執行時間動態解析出呼叫點限定符所引用的方法,然後再執行該方法。前面4條呼叫指令,分派邏輯都固化在Java虛擬機器內部,而invokedynamic指令的分派邏輯是由使用者設定的引導方法決定的。
只要能被invokestatic和invokespecial指令呼叫的方法,都可以在解析階段中確定唯一的呼叫版本,Java語言裡符合這個條件的方法共有靜態方法、私有方法、實例建構器、父類方法4種,再加上被final修飾的方法(儘管它使用invokevirtual指令呼叫),這5種方法呼叫會在類別載入的時候就可以把符號引
用解析為該方法的直接引用。這些方法統稱為「非虛方法」(Non-Virtual Method),與之相反,其他方法就稱為「虛方法」(Virtual Method)。
解析呼叫一定是個靜態的過程,在編譯期間就完全確定,在類別載入的解析階段就會把涉及的符號引用全部轉變為明確的直接引用,不必延遲到運行期再去完成。而另一個主要的方法呼叫形式:分派(Dispatch)呼叫則要複雜許多,它可能是靜態的也可能是動態的,依分派依據的宗量數可分為單分派和多分派 [1] 。這兩類分派方式兩兩組合就構成了靜態單分派、靜態多分派、動態單分派、動態多分派4種分派組合情況,下面我們來看看虛擬機器中的方法分派是如何進行的。
程式碼中故意定義了兩個靜態類型相同,而實際類型不同的變量,但虛擬機(或準確地說是編譯器)在重載時是透過參數的靜態類型而不是實際類型作為
判定依據的。由於靜態類型在編譯期可知,所以在編譯階段,Javac編譯器就根據參數的靜態型別決定了會使用哪個重載版本,因此選擇了sayHello(Human)作為呼叫目標,並且把這個方法的符號引用寫到main()方法裡的兩個invokevirtual指令的參數。
所有依賴靜態型別來決定方法執行版本的分派動作,都稱為靜態分派。 靜態分派最典型的應用表現就是方法重載。靜態分派發生在編譯階段,因此確定靜態分派的動作實際上不是由虛擬機來執行的,這點也是為何一些資料選擇把它歸入“解析”而不是“分派”的原因。
可見變長參數的重載優先權是最低的。字段永遠不參與多態,哪個類別的方法訪問某個名字的字段時,該名字指的就是這個類別能看到的那個字段。
重點
正是因為invokevirtual指令執行的第一步就是在運行期確定接收者的實際類型,所以兩次呼叫中的invokevirtual指令並不是把常數池中方法的符號引用解析到直接引用就結束了,還會根據方法接收者的實際型別來選擇方法版本,這個過程就是Java語言中方法重寫的本質。我們把這種在運行期根據實際型別決定方法執行版本的分派過程稱為動態分派。多態性的根源在於虛方法呼叫指令invokevirtual的執行邏輯,那自然我們得出的結論只會對方法有效,對欄位是無效的,因為欄位不使用這條指令。
Java語言是一門靜態多分派、動態單分派的語言。
為了程式實現方便,具有相同簽名的方法,在父類別、子類別的虛擬方法表中都應當具有相同的索引序號,這樣當類型變換時,僅需要變更查找的虛擬方法表,就可以從不同的虛擬方法表中依索引轉換出所需的入口位址。虛方法表一般在類別載入的連結階段進行初始化,準備了類別的變數初始值後,虛擬機會把該類別的虛擬方法表也一同初始化完畢。
動態類型語言支援
Java虛擬機的字節碼指令集的數量自從Sun公司的第一款Java虛擬機問世至今,二十餘年間只新增過一條指令,它就是隨著JDK 7的發布的字節碼首位新成員-invokedynamic指令。這項新增加的指令是JDK 7的專案目標:實現動態類型語言(Dynamically Typed Language)支援而進行的改進之一,也是為JDK 8裡可以順利實現Lambda表達式而做的技術儲備。
何謂動態型別語言 [1] ?動態類型語言的關鍵特徵是它的類型檢查的主體過程是在運行期而不是編譯期進行的,滿足這個特徵的語言有很多,常用的包括:APL、Clojure、Erlang、Groovy、javaScript、Lisp、Lua 、PHP、Prolog、Python、Ruby、Smalltalk、Tcl,等等。那相對地,在編譯期就進行型別檢查過程的語言,譬如C 和Java等就是最常用的靜態型別語言。變數無型別而變數值才有型別
在Java虛擬機層面上提供動態型別的直接支援就成為Java平台發展必須解決的問題,這就是JDK 7時JSR-292提案中invokedynamic指令以及java.lang.invoke套件出現的技術背景。
JDK 7時新加入的java.lang.invoke套件 [1] 是JSR 292的一個重要組成部分,這個套件的主要目的是在之前單純依靠符號引用來確定在呼叫的目標方法這條路之外,提供一種新的動態確定目標方法的機制,稱為「方法句柄」(Method Handle)。
·Reflection和MethodHandle機製本質上都是在模擬方法調用,但是Reflection是在模擬Java程式碼層次的方法調用,而MethodHandle是在模擬字節碼層次的方法調用。
在Tomcat目錄結構中,可以設定3組目錄(/common/*、/server/*和/shared/*,但預設不一定是開放的,可能只有/lib/*目錄存在)用於存放Java類別庫,另外還應該加上Web應用程式本身的「/WEB-INF/*」目錄,總共4組。把Java類別庫放置在這4組目錄中,每一組都有獨立的意義,分別是:
·放置在/common目錄中。類別庫可被Tomcat和所有的Web應用程式共同使用。
·放置在/server目錄中。類別庫可被Tomcat使用,對所有的Web應用程式都不可見。
·放置在/shared目錄中。類別庫可被所有的Web應用程式共同使用,但對Tomcat自己不可見。
·放置在/WebApp/WEB-INF目錄中。類別庫僅可被該Web應用程式使用,對Tomcat和其他Web應用程式都不可見。
為了支援這套目錄結構,並對目錄裡面的類別庫進行載入和隔離,Tomcat自訂了多個類別載入器,這些類別載入器按照經典的雙親委派模型來實作
Common類別載入器、Catalina類別載入器(也稱為Server類別載入器)、Shared類別載入器和Webapp類別載入器則是Tomcat自己定義的類別載入器,它們分別載入/common/*、/server/*、/shared/*和/WebApp/WEB-INF/*中的Java類別庫。其中WebApp類別載入器和JSP類別載入器通常還會存在多個實例,每一個Web應用程式對應一個WebApp類別載入器,每個JSP檔案對應一個JasperLoader類別載入器。
而JasperLoader的載入範圍只是這個JSP文件所編譯出來的那一個Class文件,它存在的目的就是為了被丟棄:當伺服器偵測到JSP文件被修改時,會替換掉目前的JasperLoader的實例,並且透過再建立一個新的JSP類別載入器來實現JSP檔案的HotSwap功能。
以上是java類別文件的知識點有哪些的詳細內容。更多資訊請關注PHP中文網其他相關文章!