Class 文件結構
計算機只認識 0 和 1,這個稱之爲本地機器 NativeCode
Jvm 的無關性
與平臺無關性是建立在操作系統上,虛擬機廠商提供了許多可以運行在各種不同平臺的虛擬機,它們都可以載入和執行字節碼,從而實現程序的“一次 編寫,到處運行” JDK下載 各種不同平臺的虛擬機與所有平臺都統一使用的程序存儲格式——字節碼(ByteCode)是構成平臺無關性的基石,也是語言無關性的基礎。Java 虛擬機不 和包括 Java 在內的任何語言綁定,它只與“Class 文件”這種特定的二進制文件格式所關聯,Class 文件中包含了 Java 虛擬機指令集和符號表以及若干其他 輔助信息。
JVM的無關性:
- 平臺無關性:一次編寫,到處運行
- 語言無關性:字節碼(Byte-Code)
Class 類文件(瞭解即可)
任何一個 Class 文件都對應着唯一一個類或接口的定義信息,但反過來說,Class 文件實際上它並不一定以磁盤文件的形式存在。 Class 文件是一組以 8 位字節爲基礎單位的二進制流。
Class類文件(字節碼):
- Class文件是一組以8位字節爲基礎單位的二進制流
- 類似於結構體的僞結構來存儲數據
- 只有兩種數據類型:無符號數和表
- 無符號數屬於基本的數據類型,以u1、u2、u4、u8
- 表是由多個無符號數或者其他表作爲數據項構成的複合數據類型
Class 文件格式
各個數據項目嚴格按照順序緊湊地排列在 Class 文件之中,中間沒有添加任何分隔符,這使得整個 Class 文件中存儲的內容幾乎全部是程序運行的必要數 據,沒有空隙存在。 Class 文件格式採用一種類似於 C 語言結構體的僞結構來存儲數據,這種僞結構中只有兩種數據類型:無符號數和表。 無符號數屬於基本的數據類型,以 u1、u2、u4、u8 來分別代表 1 個字節、2 個字節、4 個字節和 8 個字節的無符號數,無符號數可以用來描述數字、索 引引用、數量值或者按照 UTF-8 編碼構成字符串值。 表是由多個無符號數或者其他表作爲數據項構成的複合數據類型,所有表都習慣性地以“_info”結尾。表用於描述有層次關係的複合結構的數據,整個 Class 文件本質上就是一張表。
Class 文件格式詳解
Class 的結構不像 XML 等描述語言,由於它沒有任何分隔符號,所以在其中的數據項,無論是順序還是數量,都是被嚴格限定的,哪個字節代表什麼含義, 長度是多少,先後順序如何,都不允許改變。 按順序包括:
-
魔數與 Class 文件的版本
每個 Class 文件的頭 4 個字節稱爲魔數(MagicNumber),它的唯一作用是確定這個文件是否爲一個能被虛擬機接受的 Class 文件。使用魔數而不是擴展 名來進行識別主要是基於安全方面的考慮,因爲文件擴展名可以隨意地改動。文件格式的制定者可以自由地選擇魔數值,只要這個魔數值還沒有被廣泛
採用過同時又不會引起混淆即可。( ) 緊接着魔數的 4 個字節存儲的是 Class 文件的版本號:第 5 和第 6 個字節是次版本號(MinorVersion),第 7 和第 8 個字節是主版本號(MajorVersion)。 Java 的版本號是從 45 開始的,JDK1.1 之後的每個 JDK 大版本發佈主版本號向上加 1 高版本的 JDK 能向下兼容以前版本的 Class 文件,但不能運行以後版
本的 Class 文件,即使文件格式並未發生任何變化,虛擬機也必須拒絕執行超過其版本號的 Class 文件。
代表 JDK1.8 -
常量池
常量池中常量的數量是不固定的,所以在常量池的入口需要放置一項 u2 類型的數據,代表常量池容量計數值(constant_pool_count)。與 Java 中語言習 慣不一樣的是,這個容量計數是從 1 而不是 0 開始的
常量池中主要存放兩大類常量:字面量(Literal)和符號引用(SymbolicReferences)。
字面量比較接近於 Java 語言層面的常量概念,如文本字符串、聲明爲 final 的常量值等。
而符號引用則屬於編譯原理方面的概念,包括了下面三類常量: 類和接口的全限定名(FullyQualifiedName)、字段的名稱和描述符(Descriptor)、方法的名稱和描述符 -
訪問標誌
用於識別一些類或者接口層次的訪問信息,包括:這個 Class 是類還是接口;是否定義爲 public 類型;是否定義爲 abstract 類型;如果是類的話,是否被 聲明爲 final 等 -
類索引、父類索引與接口索引集合
這三項數據來確定這個類的繼承關係。類索引用於確定這個類的全限定名,父類索引用於確定這個類的父類的全限定名。由於 Java 語言不允許多重繼承, 所以父類索引只有一個,除了 java.lang.Object 之外,所有的 Java 類都有父類,因此除了 java.lang.Object 外,所有 Java 類的父類索引都不爲 0。接口索引 集合就用來描述這個類實現了哪些接口,這些被實現的接口將按 implements 語句(如果這個類本身是一個接口,則應當是 extends 語句)後的接口順序 從左到右排列在接口索引集合中 -
字段表集合
描述接口或者類中聲明的變量。字段(field)包括類級變量以及實例級變量。
而字段叫什麼名字、字段被定義爲什麼數據類型,這些都是無法固定的,只能引用常量池中的常量來描述。
字段表集合中不會列出從超類或者父接口中繼承而來的字段,但有可能列出原本 Java 代碼之中不存在的字段,譬如在內部類中爲了保持對外部類的訪問 性,會自動添加指向外部類實例的字段。 -
方法表集合
描述了方法的定義,但是方法裏的 Java 代碼,經過編譯器編譯成字節碼指令後,存放在屬性表集合中的方法屬性表集合中一個名爲“Code”的屬性裏面。 與字段表集合相類似的,如果父類方法在子類中沒有被重寫(Override),方法表集合中就不會出現來自父類的方法信息。但同樣的,有可能會出現由編 譯器自動添加的方法,最典型的便是類構造器“<clinit>”方法和實例構造器“<init>” -
屬性表集合
存儲 Class 文件、字段表、方法表都自己的屬性表集合,以用於描述某些場景專有的信息。如方法的代碼就存儲在 Code 屬性表中。
字節碼指令
Java 虛擬機的指令由一個字節長度的、代表着某種特定操作含義的數字(稱爲操作碼,Opcode)以及跟隨其後的零至多個代表此操作所需參數(稱爲操 作數,Operands)而構成。
由於限制了 Java 虛擬機操作碼的長度爲一個字節(即 0~255),這意味着指令集的操作碼總數不可能超過 256 條。 大多數的指令都包含了其操作所對應的數據類型信息。例如:
iload 指令用於從局部變量表中加載 int 型的數據到操作數棧中,而 fload 指令加載的則是 float 類型的數據。 大部分的指令都沒有支持整數類型 byte、char 和 short,甚至沒有任何指令支持 boolean 類型。大多數對於 boolean、byte、short 和 char 類型數據的操作, 實際上都是使用相應的 int 類型作爲運算類型
加載和存儲指令
用於將數據在棧幀中的局部變量表和操作數棧之間來回傳輸,這類指令包括如下內容。
- 將一個局部變量加載到操作棧:iload、iload_<n>、lload、lload_<n>、fload、fload_<n>、dload、dload_<n>、aload、aload_<n>。
- 將一個數值從操作數棧存儲到局部變量表:istore、istore_<n>、lstore、lstore_<n>、fstore、fstore_<n>、dstore、dstore_<n>、astore、astore_<n >。
- 將一個常量加載到操作數棧:bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、iconst_<i>、lconst_<l>、fconst_<f>、dconst_<d>。
- 擴充局部變量表的訪問索引的指令:wide。
運算或算術指令
用於對兩個操作數棧上的值進行某種特定運算,並把結果重新存入到操作棧頂。
- 加法指令:iadd、ladd、fadd、dadd。
- 減法指令:isub、lsub、fsub、dsub。
- 乘法指令:imul、lmul、fmul、dmul 等等
類型轉換指令
可以將兩種不同的數值類型進行相互轉換
Java 虛擬機直接支持以下數值類型的寬化類型轉換(即小範圍類型向大範圍類型的安全轉換):
- int 類型到 long、float 或者 double 類型。
- long 類型到 float、double 類型。
- float 類型到 double 類型。 處理窄化類型轉換(NarrowingNumericConversions)時,必須顯式地使用轉換指令來完成,這些轉換指令包括:i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l 和 d2f。
創建類實例的指令
new。
創建數組的指令
newarray、anewarray、multianewarray
訪問字段指令
getfield、putfield、getstatic、putstatic。
數組存取相關指令
- 把一個數組元素加載到操作數棧的指令:baload、caload、saload、iaload、laload、faload、daload、aaload。
- 將一個操作數棧的值存儲到數組元素中的指令:bastore、castore、sastore、iastore、fastore、dastore、aastore。
- 取數組長度的指令:arraylength。
檢查類實例類型的指令
instanceof、checkcast。
操作數棧管理指令
如同操作一個普通數據結構中的堆棧那樣,Java 虛擬機提供了一些用於直接操作操作數棧的指令,包括:
- 將操作數棧的棧頂一個或兩個元素出棧:pop、 pop2。
- 複製棧頂一個或兩個數值並將複製值或雙份的複製值重新壓入棧頂:dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2。
- 將棧最頂端的兩個數值互換:swap。
控制轉移指令
控制轉移指令可以讓 Java 虛擬機有條件或無條件地從指定的位置指令而不是控制轉移指令的下一條指令繼續執行程序,從概念模型上理解,可以認爲控 制轉移指令就是在有條件或無條件地修改 PC 寄存器的值。控制轉移指令如下。
- 條件分支:ifeq、iflt、ifle、ifne、ifgt、ifge、ifnull、ifnonnull、if_icmpeq、if_icmpne、if_icmplt、if_icmpgt、if_icmple、if_icmpge、if_acmpeq 和 if_acmpne。
- 複合條件分支:tableswitch、lookupswitch。
- 無條件分支:goto、goto_w、jsr、jsr_w、ret。
方法調用指令
invokevirtual 指令用於調用對象的實例方法,根據對象的實際類型進行分派(虛方法分派),這也是 Java 語言中最常見的方法分派方式。
- invokeinterface 指令用於調用接口方法,它會在運行時搜索一個實現了這個接口方法的對象,找出適合的方法進行調用。
- invokespecial 指令用於調用一些需要特殊處理的實例方法,包括實例初始化方法、私有方法和父類方法。
- invokestatic 指令用於調用類方法(static 方法)。
- invokedynamic 指令用於在運行時動態解析出調用點限定符所引用的方法,並執行該方法,前面 4 條調用指令的分派邏輯都固化在 Java 虛擬機內部,而 invokedynamic 指令的分派邏輯是由用戶所設定的引導方法決定的。 方法調用指令與數據類型無關。
方法返回指令
是根據返回值的類型區分的,包括 ireturn(當返回值是 boolean、byte、char、short 和 int 類型時使用)、lreturn、freturn、dreturn 和 areturn,另外還有 一條 return 指令供聲明爲 void 的方法、實例初始化方法以及類和接口的類初始化方法使用。
異常處理指令
在 Java 程序中顯式拋出異常的操作(throw 語句)都由 athrow 指令來實現
同步指令
有 monitorenter 和 monitorexit 兩條指令來支持 synchronized 關鍵字的語義
虛擬機棧再認識
運行時數據區域
這個是抽象概念,內部實現依賴寄存器、高速緩存、主內存(具體要分析 JVM 源碼 C++語言實現,沒必要看) 計算機的運行=指令+數據,指令用於執行方法的,數據用於存放數據和對象的。
虛擬機棧----執行 java 方法、本地方法棧—執行本地方法、程序計數器—程序執行的計數器 Java 中的數據:變量、常量、對象、數組相關。
棧楨詳解
當前棧幀有效:一個線程的方法調用鏈可能會很長,這意味着虛擬機棧會被壓入很多棧幀,但在線程執行的某個時間點只有位於棧頂的棧幀纔是有效的, 該棧幀稱爲“當前棧幀”,與這個棧幀相關聯的方法稱爲“當前方法”。
局部變量表
- 局部變量表的容量以變量槽(VariableSlot,下稱 Slot)爲最小單位,虛擬機規範中導向性地說到每個 Slot 都應該能存放一個 boolean、byte、char、short、 int、float、double、long 8 種數據類型和 reference ,可以使用 32 位或更小的物理內存來存放。
- 對於 64 位的數據類型,虛擬機會以高位對齊的方式爲其分配兩個連續的 Slot 空間。 Java 語言中明確的(reference 類型則可能是 32 位也可能是 64 位) 64 位的數據類型只有 long 和 double 兩種。
操作數棧
- 操作數棧(OperandStack)也常稱爲操作棧,它是一個先進後出(FirstInLastOut,FILO)棧。 同局部變量表一樣, 操作數棧的每一個元素可以是任意的 Java 數據類型,包括 long 和 double。 32 位數據類型所佔的棧容量爲 1,64 位數據類型所佔的棧容量爲 2。
- 當一個方法剛剛開始執行的時候,這個方法的操作數棧是空的,在方法的執行過程中,會有各種字節碼指令往操作數棧中寫入和提取內容,也就是出棧/ 入棧操作。 例如,在做算術運算的時候是通過操作數棧來進行的,又或者在"調用其他方法的時候是通過操作數棧來進行參數傳遞的"。 java 虛擬機的解釋執行引擎稱爲“基於棧的執行引擎”,其中所指的“棧”就是操作數棧。
數據重疊優化
虛擬機概念模型中每二個棧幀都是相互獨立的,但在實際應用是我們知道一個方法調用另一個方法時,往往存在參數傳遞,這種做法在虛擬機實現過程 中會做一些優化,
具體做法如下:令兩個棧幀出現一部分重疊。讓下面棧幀的一部分操作數棧與上面棧幀的部分局部變量表重疊在一起,進行方法調用 時就可以共用一部分數據,無須進行額外的參數複製傳遞。
動態連接
- 可理解爲“多態” ,執行方法,那麼我們需要知道當前棧幀執行的是哪個方法,棧幀中會持有一個引用(符號引用),該引用指向某個具體方法。
- 符號引用是一個地址位置的代號,在編譯的時候我們是不知道某個方法在運行的時候是放到哪裏的,這時我用代號 com/enjoy/pojo/User.Say:()V 指代某個 類的方法,將來可以把符號引用轉換成直接引用進行真實的調用。
- 用符號引用轉化成直接引用的解析時機,把解析分爲兩大類 :
靜態解析:符號引用在類加載階段或者第一次使用的時候就直接轉換成直接引用。
動態連接:符號引用在每次運行期間轉換爲直接引用,即每次運行都重新轉換。
方法返回地址
方法退出方式有:正常退出與異常退出
- 理論上,執行完當前棧幀的方法,需要返回到當前方法被調用的位置,所以棧幀需要記錄一些信息,用來恢復上層方法的執行狀態。正常退出,上層方 法的 PC 計數器可以做爲當前方法的返回地址,被保存在當前棧幀中。“異常退出時,返回地址是要通過異常處理器表來確定的,棧幀中一般不會保存這部 分信息”
- 方法退出時會做的操作:恢復上次方法的局部變量表、操作數棧,把當前方法的返回值,壓入調用者棧幀的操作數棧中,使用當前棧幀保存的返回地址 調整 PC 計數器的值,當前棧幀出棧,隨後,執行 PC 計數器指向的指令。
附加信息
- 虛擬機規範允許實現虛擬機時增加一些額外信息,例如與調試相關的信息。
- 一般把動態連接、方法返回地址、其他額外信息歸成一類,稱爲棧幀信息。
基於棧的字節碼解釋執行引擎
Java 編譯器輸出的指令流,基本上是一種基於棧的指令集架構,指令流中的指令大部分都是零地址指令,它們依賴操作數棧進行工作。與 基於寄存器的指令集,最典型的就是 x86 的二地址指令集,說得通俗一些,就是現在我們主流 PC 機中直接支持的指令集架構,這些指令依賴寄存器進行 工作。
基於棧的指令集
舉個最簡單的例子,分別使用這兩種指令集計算“1+1”的結果,基於棧的指令集會是這樣子的:
iconst_1
iconst_1
iadd istore_0
兩條 iconst_1 指令連續把兩個常量 1 壓入棧後,iadd 指令把棧頂的兩個值出棧、相加,然後把結果放回棧頂,最後 istore_0 把棧頂的值放到局部變量表的 第 0 個 Slot 中。
特點在於移植性較高,但相比於寄存器指令集速度較慢
基於寄存器的指令集
如果基於寄存器,那程序可能會是這個樣子:
moveax,1 addeax,1 mov 指令把 EAX 寄存器的值設爲 1,然後 add 指令再把這個值加 1,結果就保存在 EAX 寄存器裏面。 基於棧的指令集主要的優點就是可移植,寄存器由硬件直接提供,程序直接依賴這些硬件寄存器則不可避免地要受到硬件的約束。棧架構指令集的主要 缺點是執行速度相對來說會稍慢一些。所有主流物理機的指令集都是寄存器架構也從側面印證了這一點。
特點在於速度較快,但是缺點在於移植性較差
關於字節碼執行引擎——方法調用詳解
解析
- 調用目標在程序代碼寫好、編譯器進行編譯時就必須確定下來。這類方法的調用稱爲解析。
在 Java 語言中符合“編譯期可知,運行期不可變”這個要求的方法,主要包括靜態方法和私有方法兩大類,前者與類型直接關聯,後者在外部不可被訪 問,這兩種方法各自的特點決定了它們都不可能通過繼承或別的方式重寫其他版本,因此它們都適合在類加載階段進行解析。
靜態分派
- 多見於方法的重載。
- “Human”稱爲變量的靜態類型(StaticType),或者叫做的外觀類型(ApparentType),後面的“Man”則稱爲變量的實際類型(ActualType),靜態類 型和實際類型在程序中都可以發生一些變化,區別是靜態類型的變化僅僅在使用時發生,變量本身的靜態類型不會被改變,並且最終的靜態類型是在編 譯期可知的;而實際類型變化的結果在運行期纔可確定,編譯器在編譯程序的時候並不知道一個對象的實際類型是什麼。
代碼中定義了兩個靜態類型相同但實際類型不同的變量,但虛擬機(準確地說是編譯器)在重載時是通過參數的靜態類型而不是實際類型作爲判定依據 的。並且靜態類型是編譯期可知的,因此,在編譯階段,Javac 編譯器會根據參數的靜態類型決定使用哪個重載版本,所以選擇了 sayHello(Human)作 爲調用目標。所有依賴靜態類型來定位方法執行版本的分派動作稱爲靜態分派。靜態分派的典型應用是方法重載。靜態分派發生在編譯階段,因此確定 靜態分派的動作實際上不是由虛擬機來執行的。
動態分派
- 靜態類型同樣都是 Human 的兩個變量 man 和 woman 在調用 sayHello()方法時執行了不同的行爲,並且變量 man 在兩次調用中執行了不同的方法。導 致這個現象的原因很明顯,是這兩個變量的實際類型不同。
在實現上,最常用的手段就是爲類在方法區中建立一個虛方法表。虛方法表中存放着各個方法的實際入口地址。如果某個方法在子類中沒有被重寫,那 子類的虛方法表裏面的地址入口和父類相同方法的地址入口是一致的,都指向父類的實現入口。如果子類中重寫了這個方法,子類方法表中的地址將會 替換爲指向子類實現版本的入口地址。 PPT 圖中, Son 重寫了來自 Father 的全部方法,因此 Son 的方法表沒有指向 Father 類型數據的箭頭。但是 Son 和 Father 都沒有重寫來自 Object 的方法,所以它們的方法表中所有從 Object 繼承來的方法都指向了 Object 的數據類型。
類加載機制
概述
類從被加載到虛擬機內存中開始,到卸載出內存爲止,它的整個生命週期包括:加載(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、 初始化(Initialization)、使用(Using)和卸載(Unloading)7 個階段。其中驗證、準備、解析 3 個部分統稱爲連接(Linking)
初始化
初始化的 5 種情況
初始化階段,虛擬機規範則是嚴格規定了有且只有 5 種情況必須立即對類進行“初始化”(而加載、驗證、準備自然需要在此之前開始):
- 遇到 new、getstatic、putstatic 或 invokestatic 這 4 條字節碼指令時,如果類沒有進行過初始化,則需要先觸發其初始化。生成這 4 條指令的最常見的 Java 代碼場景是:使用 new 關鍵字實例化對象的時候、讀取或設置一個類的靜態字段(被 final 修飾、已在編譯期把結果放入常量池的靜態字段除外)的 時候,以及調用一個類的靜態方法的時候。
- 使用 java.lang.reflect 包的方法對類進行反射調用的時候,如果類沒有進行過初始化,則需要先觸發其初始化。
- 當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。
- 當虛擬機啓動時,用戶需要指定一個要執行的主類(包含 main()方法的那個類),虛擬機會先初始化這個主類。
- 當使用 JDK1.7 的動態語言支持時,如果一個 java.lang.invoke.MethodHandle 實例最後的解析結果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法 句柄,並且這個方法句柄所對應的類沒有進行過初始化,則需要先觸發其初始化。
簡述哪五種情況下會進行類初始化:
1.遇到 new、getstatic、pubstatic、invokestatic字節碼的時候
2.反射:reflect
3.初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。
4.JVM啓動時,指定執行主類的(main的那個類),JVM初始化
5.JDK1.7 的動態語言支持時,java.lang.invoke.MethodHandle 實例化類
舉例
1.對於靜態字段,只有直接定義這個字段的類纔會被初始化,因此通過其子類來引用父類中定義的靜態字段,只會觸發父類的初始化而不會觸發子類的初 始化,如下代碼:
/**
* @author dujiayu
* 類加載--父類
*/
public class SuperClazz {
/**
*被動使用類字段演示一:
*通過子類引用父類的靜態字段,不會導致子類初始化
**/
static {
System.out.println("SuperClass init!");
}
public static int value = 123;
public static final String HELLOWORLD = "hello world king";
public static final int WHAT = value;
}
/**
* @author dujiayu
* 類加載--子類
*/
public class SubClaszz extends SuperClazz {
static {
System.out.println("SubClass init!");
}
}
/**
*被動使用類字段演示一:
*通過子類引用父類的靜態字段,不會導致子類初始化
**/
public class NotInitialization {
public static void main(String[]args){
//直接打印子類中的 靜態變量:
//如果通過子類引用父類中的靜態字段,只會觸發父類的初始化,而不會觸發子類的初始化
//System.out.println(SubClaszz.value);
//使用數組的方式, 會不會打印初始化
//SuperClazz[]sca = new SuperClazz[10];
//打印 一個常量(不會進行初始化)
//常量池\ class編譯後,常量池的信息也放入class類中
//System.out.println(SuperClazz.HELLOWORLD);//hello world寫入、
//如果使用常量去引用另外一個常量(會進行初始化)
System.out.println(SuperClazz.WHAT);
//System.out.println(SubClaszz.value);
//SuperClazz[]sca = new SuperClazz[10];
//System.out.println(SuperClazz.HELLOWORLD);
//System.out.println(SuperClazz.WHAT);
}
}
2. 數組形式的 new(而不是構造方法)不會觸發類初始化
3.直接打印類的常量會不會觸發類的初始化:(坑:項目中有可能常量改了,關聯使用的類不重新編譯就會還是原來的值) 常量 HELLOWORLD,但其實在編譯階段通過常量傳播優化,已經將此常量的值“helloworld”存儲到了 NotInitialization 類的常量池中,以後 NotInitialization 對常量 ConstClass.HELLOWORLD 的引用實際都被轉化爲 NotInitialization 類對自身常量池的引用了。 也就是說,實際上 NotInitialization 的 Class 文件之中並沒有 ConstClass 類的符號引用入口,這兩個類在編譯成 Class 之後就不存在任何聯繫了。
4.如果使用常量去引用另外一個常量,這個時候編譯階段無法進行優化,所以纔會觸發類的初始化。
加載階段
虛擬機需要完成以下 3 件事情:
1)通過一個類的全限定名來獲取定義此類的二進制字節流。
2)將這個字節流所代表的靜態存儲結構轉化爲方法區的運行時數據結構。 3)在內存中生成一個代表這個類的 java.lang.Class 對象,作爲方法區這個類的各種數據的訪問入口。、
驗證階段
是連接階段的第一步,這一階段的目的是爲了確保 Class 文件的字節流中包含的信息符合當前虛擬機的要求,並且不會危害虛擬機自身的安全。
但從整體上看,驗證階段大致上會完成下面 4 個階段的檢驗動作:
文件格式驗證、元數據驗證、字節碼驗證、符號引用驗證。
準備階段
是正式爲類變量分配內存並設置類變量初始值的階段,這些變量所使用的內存都將在方法區中進行分配。這個階段中有兩個容易產生混淆的概念需要強 調一下,首先,這時候進行內存分配的僅包括類變量(被 static 修飾的變量),而不包括實例變量,實例變量將會在對象實例化時隨着對象一起分配在 Java 堆中。其次,這裏所說的初始值“通常情況”下是數據類型的零值,假設一個類變量的定義爲:
public static int value=123;
那變量 value 在準備階段過後的初始值爲 0 而不是 123,因爲這時候尚未開始執行任何 Java 方法,而把 value 賦值爲 123 的 putstatic 指令是程序被編譯後, 存放於類構造器<clinit>()方法之中,所以把 value 賦值爲 123 的動作將在初始化階段纔會執行。假設上面類變量 value 的定義變爲:public static final int value=123;
編譯時 Javac 將會爲 value 生成 ConstantValue 屬性,在準備階段虛擬機就會根據 ConstantValue 的設置將 value 賦值爲 123。
解析階段
是虛擬機將常量池內的符號引用替換爲直接引用的過程。部分詳細內容見上方解析
類初始化階段
- 是類加載過程的最後一步,前面的類加載過程中,除了在加載階段用戶應用程序可以通過自定義類加載器參與之外,其餘動作完全由虛擬機主導和控制。
- 到了初始化階段,才真正開始執行類中定義的 Java 程序代碼在準備階段,變量已經賦過一次系統要求的初始值,而在初始化階段,則根據程序員通過程 序制定的主觀計劃去初始化類變量和其他資源,或者可以從另外一個角度來表達:初始化階段是執行類構造器<clinit>()方法的過程。
- <clinit>()方 法是由編譯器自動收集類中的所有類變量的賦值動作和靜態語句塊(static{}塊)中的語句合併產生的,編譯器收集的順序是由語句在源文件中出現的順序 所決定的。
- <clinit>()方法對於類或接口來說並不是必需的,如果一個類中沒有靜態語句塊,也沒有對變量的賦值操作,那麼編譯器可以不爲這個類生成<clinit >()方法。
初始化的單例模式(線程安全)
- 虛擬機會保證一個類的<clinit>()方法在多線程環境中被正確地加鎖、同步,如果多個線程同時去初始化一個類,那麼只會有一個線程去執行這個類 的<clinit>()方法,其他線程都需要阻塞等待,直到活動線程執行<clinit>()方法完畢。
- 如果在一個類的<clinit>()方法中有耗時很長的操作,就 可能造成多個進程阻塞。所以類的初始化是線程安全的,項目中可以利用這點。
類加載器
對於任意一個類,都需要由加載它的類加載器和這個類本身一同確立其在 Java 虛擬機中的唯一性,每一個類加載器,都擁有一個獨立的類名稱空間。這句話可以表達得更通俗一些:比較兩個類是否“相等”,只有在這兩個類是由同一個類加載器加載的前提下才有意義,否則,即使這兩個類來源於同一 個 Class 文件,被同一個虛擬機加載,只要加載它們的類加載器不同,那這兩個類就必定不相等。
這裏所指的“相等”,包括代表類的 Class 對象的 equals()方法、isAssignableFrom()方法、isInstance()方法的返回結果,也包括使用 instanceof 關 鍵字做對象所屬關係判定等情況。
加解密案例(deencrpt 代碼)
通過位的二進制異或運算進行加解密(一次就是加密,再運算一次就是解密)
- DemoUser.class 重命名爲 DemoUserSrc.class 同時刪掉 DemoUser.class,再通過 XorEncrpt 加密生成 DemoUser.class,使用編輯工具查看下加密前和加密後
- 寫一個自定義的類加載器,繼承 ClassLoader,同時在加載時進行解密
- 寫一個 DemoRun 類,使用自義定的類加載器加密,再打印類的對象,看它是哪個類加載器加載的,是否能正常顯示。
加解密的項目中運用:可以使用把代碼使用私鑰加密,在解析階段使用公鑰解密。這樣跟用戶做項目時提供對應的公鑰,自己提供私鑰加密後的代碼信 息。在類加載時使用公鑰解密運行。這樣可以可以確保源代碼的保密性。
/*
* 類說明:自定義的類加載器,進行異或解密。URL
*/
public class CustomClassLoader extends ClassLoader {
private final String name;
private String basePath;
private final static String FILE_EXT = ".class";
public CustomClassLoader(String name) {
super();
this.name = name;
}
public void setBasePath(String basePath) {
this.basePath = basePath;
}
private byte[] loadClassData(String name) {
byte[] data = null;
XorEncrpt demoEncryptUtil = new XorEncrpt();
try {
String tempName = name.replaceAll("\\.", "\\\\");
//解密
data = demoEncryptUtil.decrypt(new File(basePath + tempName + FILE_EXT));
} catch (Exception e) {
e.printStackTrace();
}
return data;
}
// @Override
// public Class<?> loadClass(String name) throws ClassNotFoundException {
// if(name.indexOf("java.")<5&&name.indexOf("java.")>-1){
// return super.loadClass(name);
// }
// byte[] data = this.loadClassData(name);
// if (data == null){
// return super.loadClass(name);
// }
// return this.defineClass(name,data,0,data.length);
// }
@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
byte[] data = this.loadClassData(name);
return this.defineClass(name, data, 0, data.length);
}
}
public class DemoRun {
public static void main(String[] args) throws Exception {
//new出自定義類加載器
CustomClassLoader demoCustomClassLoader = new CustomClassLoader("My ClassLoader");
//設置加載類的路徑
demoCustomClassLoader.setBasePath("D:\\work\\ref-jvm\\bin\\");
Class<?> clazz = demoCustomClassLoader.findClass("com.jvm.ch04.deencrpt.DemoUser");
System.out.println(clazz.getClassLoader());
Object o = clazz.newInstance();
System.out.println(o);
}
}
/*
* 類說明:我們要處理的業務類
*/
public class DemoUser {
private int id = 13;
private String name = "King";
@Override
public String toString() {
return "DemoUser [id=" + id + ", name=" + name + "]";
}
}
/*
* 類說明:加密和解密的服務類
*/
public class XorEncrpt{
//位運算:異或操作(一個數經過兩次異或=自己)^
private void xor(InputStream in, OutputStream out) throws Exception{
int ch;
while (-1 != (ch = in.read())){
ch = ch^ 0xff;
out.write(ch);
}
}
//加密方法(流的方式),加密後重新寫入
public void encrypt(File src, File des) throws Exception {
InputStream in = new FileInputStream(src);
OutputStream out = new FileOutputStream(des);
xor(in,out);
in.close();
out.close();
}
//解密方法,返回解密後的二進制數組
public byte[] decrypt(File src) throws Exception {
InputStream in = new FileInputStream(src);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
xor(in,bos);
byte[] data = bos.toByteArray();;
return data;
}
public static void main(String[] args) throws Exception {
File src = new File("D:\\work\\ref-jvm\\bin\\com\\jvm\\ch04\\deencrpt\\DemoUserScr.class");
File dest = new File("D:\\work\\ref-jvm\\bin\\com\\jvm\\ch04\\deencrpt\\DemoUser.class");
XorEncrpt demoEncryptUtil = new XorEncrpt();
demoEncryptUtil.encrypt(src,dest);
System.out.println("加密完成!");
}
}
雙親委派模型
對於任意一個類,都需要由加載它的類加載器和這個類本身一同確立其在 Java 虛擬機中的唯一性。
從 Java 虛擬機的角度來講,只存在兩種不同的類加載器:
- 一種是啓動類加載器(BootstrapClassLoader),這個類加載器使用 C++語言實現,是虛擬機自身的一部分;另一種就是所有其他的類加載器,這些類加 載器都由 Java 語言實現,獨立於虛擬機外部,並且全都繼承自抽象類 java.lang.ClassLoader。
- 啓動類加載器(BootstrapClassLoader):這個類將器負責將存放在<JAVA_HOME>\lib 目錄中的,或者被-Xbootclasspath 參數所指定的路徑中的,並且是 虛擬機識別的(僅按照文件名識別,如 rt.jar,名字不符合的類庫即使放在 lib 目錄中也不會被加載)類庫加載到虛擬機內存中。啓動類加載器無法被 Java 程序直接引用,用戶在編寫自定義類加載器時,如果需要把加載請求委派給引導類加載器,那直接使用 null 代替即可。
- 擴展類加載器(Extension ClassLoader):這個加載器由 sun.misc.Launcher$ExtClassLoader 實現,它負責加載<JAVA_HOME>\lib\ext 目錄中的,或者被 java.ext.dirs 系統變量所指定的路徑中的所有類庫,開發者可以直接使用擴展類加載器。
- 應用程序類加載器(Application ClassLoader):這個類加載器由 sun.misc.Launcher $App-ClassLoader 實現。由於這個類加載器是 ClassLoader 中的 getSystemClassLoader()方法的返回值,所以一般也稱它爲系統類加載器。它負責加載用戶類路徑(ClassPath)上所指定的類庫,開發者可以直接使用這 個類加載器,如果應用程序中沒有自定義過自己的類加載器,一般情況下這個就是程序中默認的類加載器。
我們的應用程序都是由這 3 種類加載器互相配合進行加載的,如果有必要,還可以加入自己定義的類加載器。 雙親委派模型要求除了頂層的啓動類加載器外,其餘的類加載器都應當有自己的父類加載器。這裏類加載器之間的父子關係一般不會以繼承(Inheritance) 的關係來實現,而是都使用組合(Composition)關係來複用父加載器的代碼。
使用雙親委派模型來組織類加載器之間的關係,有一個顯而易見的好處就是 Java 類隨着它的類加載器一起具備了一種帶有優先級的層次關係。例如類 java.lang.Object,它存放在 rt.jar 之中,無論哪一個類加載器要加載這個類,最終都是委派給處於模型最頂端的啓動類加載器進行加載,因此 Object 類在 程序的各種類加載器環境中都是同一個類。相反,如果沒有使用雙親委派模型,由各個類加載器自行去加載的話,如果用戶自己編寫了一個稱爲 java.lang.Object 的類,並放在程序的 ClassPath 中,那系統中將會出現多個不同的 Object 類,Java 類型體系中最基礎的行爲也就無法保證,應用程序也將 會變得一片混亂。
應用程序類加載器
ClassLoader 中的 loadClass 方法中的代碼邏輯就是雙親委派模型:
- 在自定義 ClassLoader 的子類時候,我們常見的會有兩種做法,一種是重寫 loadClass 方法,另一種是重寫 findClass 方法。其實這兩種方法本質上差不多, 畢竟 loadClass 也會調用 findClass,但是從邏輯上講我們最好不要直接修改 loadClass 的內部邏輯。我建議的做法是隻在 findClass 裏重寫自定義類的加載方 法。
- loadClass 這個方法是實現雙親委託模型邏輯的地方,擅自修改這個方法會導致模型被破壞,容易造成問題。因此我們最好是在雙親委託模型框架內進行 小範圍的改動,不破壞原有的穩定結構。同時,也避免了自己重寫 loadClass 方法的過程中必須寫雙親委託的重複代碼,從代碼的複用性來看,不直接修 改這個方法始終是比較好的選擇。
Tomcat 類加載機制
- Tomcat 本身也是一個 java 項目,因此其也需要被 JDK 的類加載機制加載,也就必然存在引導類加載器、擴展類加載器和應用(系統)類加載器。
- CommonClassLoader作爲CatalinaClassLoader和SharedClassLoader的parent,而SharedClassLoader又可能存在多個children類加載器WebAppClassLoader, 一個 WebAppClassLoader 實際上就對應一個 Web 應用,那 Web 應用就有可能存在 Jsp 頁面,這些 Jsp 頁面最終會轉成 class 類被加載,因此也需要一個 Jsp 的類加載器。
- 需要注意的是,在代碼層面 CatalinaClassLoader、 SharedClassLoader、 CommonClassLoader 對應的實體類實際上都是 URLClassLoader 或者 SecureClassLoader, 一般我們只是根據加載內容的不同和加載父子順序的關係,在邏輯上劃分爲這三個類加載器;而 WebAppClassLoader 和 JasperLoader 都是存在對應的類加 載器類的。
當 tomcat 啓動時,會創建幾種類加載器:
- Bootstrap 引導類加載器 加載 JVM 啓動所需的類,以及標準擴展類(位於 jre/lib/ext 下)
- System 系統類加載器 加載 tomcat 啓動的類,比如 bootstrap.jar,通常在 catalina.bat 或者 catalina.sh 中指定。位於 CATALINA_HOME/bin 下。
- Common 通用類加載器 加載 tomcat 使用以及應用通用的一些類,位於 CATALINA_HOME/lib 下,比如 servlet-api.jar
- webapp 應用類加載器每個應用在部署後,都會創建一個唯一的類加載器。該類加載器會加載位於 WEB-INF/lib 下的 jar 文件中的 class 和 WEB-INF/classes 下的 class 文件。