目錄
- 前言
- 5. 類檔案結構
- 5.1 無關性概述
- 5.2 Class 類檔案結構
- 5.3 class 檔案的資料項
- 5.4 位元組碼指令
- 5.5 位元組碼用途分類
- 6. 類加載機制
- 6.1 必須要對類進行初始化的五種時機(對類的主動參考)
- 6.2 類加載程序(生命周期)
- 6.3 類加載器
- 6.3 雙親委派模式
- 6.4 破壞雙親委派模式
- 7. 虛擬機位元組碼執行引擎
- 7.1 確定被呼叫的方法
- 最后
前言
參考資料:
《深入理解 Java 虛擬機 - JVM 高級特性與最佳實踐》
第1部分主題為自動記憶體管理,以此延伸出 Java 記憶體區域與記憶體溢位、垃圾收集器與記憶體分配策略、引數配置與性能調優等相關內容;
第2部分主題為虛擬機執行子系統,以此延伸出 class 類檔案結構、虛擬機類加載機制、虛擬機位元組碼執行引擎等相關內容;
第3部分主題為程式編譯與代碼優化,以此延伸出程式前后端編譯優化、前端易用性優化、后端性能優化等相關內容;
第4部分主題為高效并發,以此延伸出 Java 記憶體模型、執行緒與協程、執行緒安全與鎖優化等相關內容;
本系列學習筆記可看做《深入理解 Java 虛擬機 - JVM 高級特性與最佳實踐》書籍的縮減版與總結版,想要了解細節請見紙質版書籍;
5. 類檔案結構
5.1 無關性概述
- 實作語言無關性的基礎是虛擬機和位元組碼存盤格式;
- Java 虛擬機不和包括 Java 在內的任何語言系結,它只與 class 檔案這種特定的二進制檔案格式所關聯;
- Java 虛擬機不關心 class 的來源是何種語言,比如 Groovy、Scala 等語言都能產出符合規范的class檔案;
- Java 虛擬機規范要求在 class 檔案中使用許多強制性的語法和結構化約束;
5.2 Class 類檔案結構
- class 檔案是一組以
8位bit(1位元組)為基礎單位的二進制流,各個資料專案嚴格按照順序緊湊的排列在 class 檔案之中,中間沒有任何分隔符,當遇到需要占用 1 位元組以上空間的資料項時,則會按照高位在前的方式分割成若干個 1 位元組進行存盤; - 包含兩種資料型別:
- 無符號數:基本的資料型別,以 u1、u2、u4、u8 來分別代表 1 個位元組、2 個位元組、4 個位元組和 8 個位元組的無符號數,無符號數可以用來描述數字、索引參考、數量值或者字串值;
- 表:由多個無符號數或者其他表作為資料項構成的復合資料型別,表用于描述有層次關系的復合結構的資料,整個 class 檔案本質上就是一張表;
- class 檔案的資料項如下表:

5.3 class 檔案的資料項
- u4 魔數(Magic Number):唯一的作用是確定這個檔案是否為一個能被虛擬機接受的 class 檔案,固定為 0xCAFEBABE;
- u2+u2 版本:虛擬機也必須拒絕執行超過其版本號的 class 檔案;
- u2+ 常量池:常量池容量計數器用來記錄常量個數,常量池中主要存放兩大類常量:
- 字面量:近于 Java 語言層面的常量概念,如文本字串、final 修飾的常量值等;
- 符號參考:編譯原理方面的概念,包括了:類和介面的全限定名、欄位的名稱和描述符、方法的名稱和描述符,常量池中的每一項常量都是一個表,可以用 javap 分析 class 檔案;
- u2 訪問標記:用于標識一些類或者介面層次的訪問資訊;
- 4*u2 類與介面索引集合:由這 4 項資料確定類的繼承關系;
- u2+ 欄位表集合:用于描述介面或者類中宣告的變數,包括類級變數和實體級變數,不包括在方法內部宣告的區域變數;(public、static、final、volatile、transient 等)
- u2+ 方法表集合:類似上面欄位表,方法里的 Java 代碼,經過編譯器編譯成位元組碼指令后,存放在方法屬性表集合中一個名為"Code"的屬性里,方法呼叫指令以常量池中指向方法的符號參考作為引數;
- u2+ 屬性表集合:不是單獨的一部分,而是由 class 檔案、欄位表、方法表等攜帶,以描述某些場景專有的資訊;
5.4 位元組碼指令
- 由一個位元組長度的、代表著某種特定操作含義的數字(稱為操作碼,Opcode)以及跟隨其后的0至多個所需引數(稱為運算元,Operands)構成;
- 由于 Java 虛擬機采用面向運算元堆疊的架構,而不是暫存器,所以多大數的指令都不包含運算元,只有一個操作碼(追求小數量、高傳輸效率),對運算元堆疊進行出堆疊、入堆疊操作;
- 指令集的操作碼總數不超過 256 條(操作碼只有1位元組),因此 Java 虛擬機的指令集對于特定的操作只提供了有限的型別相關指令去支持(例如有 int 型別的 iload,沒有 byte 型別的同類指令);
- 對于沒有定義的資料型別的相關指令,大多數會在編譯期或運行期轉換成 int 型別作為運算型別;
5.5 位元組碼用途分類
- 加載和存盤指令:用于將資料在堆疊幀中的區域變數表和運算元堆疊之間來回傳輸,比如 iload、istore、bipush等;
- 運算指令:用于對兩個運算元堆疊上的值進行某種特定運算,并把結果重新存入到運算元堆疊頂,比如加法指令:iadd,減法指令:isub 等等;
- 型別轉換指令:將兩種不同的數值型別進行相互轉換,這些轉換操作一般用于實作用戶代碼中的顯示型別轉換操作,或者處理前面提到的指令集中資料型別相關指令無法與資料型別一一對應的問題(byte、short等擴展為int);
- 物件創建與訪問指令:要注意 Java 虛擬機對類實體和陣列的創建與操作使用了不同的位元組碼指令,創建類實體:new,創建陣列:nwarray、anewarray 等;
- 運算元堆疊管理指令:類似于操作普通資料結構中的堆疊,Java虛擬機提供了一些用于直接操作運算元堆疊的指令,比如pop、dup、swap等;
- 控制轉移指令:可以讓 Java 虛擬機有條件或無條件的修改程式計數器的值,包括條件分支(比如ifeq)、復合條件分支(比如tableswitch)、無條件分支(比如goto)等等;
- 方法呼叫和回傳指令:方法呼叫指令包括,像 invokevirtual 指令:用于呼叫物件的實體方法,invokespecial指令:呼叫一些需要特殊處理的方法,包括實體初始化方法、私有方法和父類方法;方法呼叫指令與資料型別無關,但方法回傳指令是根據回傳值型別區分的,包括ireturn(回傳boolean、byte、char、short、int),lreturn、freturn、dreturn和areturn,另外還有一條return指令供宣告為void的方法、實體初始化方法以及類和介面類初始化方法使用;
- 例外處理指令:Java 程式中顯示拋出例外的操作(throw)都是用 athrow 指令來實作,除此之外,Java 虛擬機規范還規定了許多運行時例外會在其他 Java 虛擬機指令檢測到例外狀況時自動拋出,比如在整數運算中,當除數為 0 時,虛擬機會在 idiv 或 ldiv 指令中拋出 ArithmeticException 例外,現在在 Java 虛擬機中處理例外是采用例外表完成的,以前則使用的是 jsr 和 ret 指令實作;
- 同步指令:synchronized 陳述句塊對應的指令就是 monitorenter 和 monitorexit,編譯器必須確保無論方法通過何種方式完成,方法中呼叫過的每條 monitorenter 指令都必須執行其對應的 monitorexit 指令,所以為了保證在方法例外完成時,monitorenter 和 monitorexit 指令依然可以正確配對執行,編譯器會自動產生一個例外處理器,這個例外處理器宣告可以處理所有的例外;
6. 類加載機制
6.1 必須要對類進行初始化的五種時機(對類的主動參考)
- 遇到
new、getstatic、putstatic或invokestatic這 4 條位元組碼指令時沒初始化觸發初始化;(即:new 關鍵字實體化物件、讀取一個類的 finel 靜態欄位、呼叫一個類的靜態方法); - 使用
java.lang.reflect包的方法對類進行反射呼叫; - 發現某類的父類還沒有進行初始化,先觸發其父類的初始化;
- 當虛擬機啟動時,用戶需指定一個要加載的主類(包含 main() 方法的那個類),虛擬機會先初始化這個主類;
- 當使用 JDK 1.7 的動態語言支持時,如果一個
java.lang.invoke.MethodHandle實體最后的決議結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且這個方法句柄所對應的類沒有進行過初始化,則需先觸發其初始化;
6.2 類加載程序(生命周期)
- 程式主動使用某個類時,如果該類還未被加載到記憶體中,則 JVM 會通過加載、連接、初始化 3 個步驟來對該類進行初始化;
- 在程式運行期間完成;
- 1. 加載:將類的 class 檔案讀入到記憶體,通過一個類的全限定名來獲取定義次類的二進制流,將這個位元組流所代表的靜態存盤結構轉換成方法區中的運行時資料結構,在堆中生成一個代表這個類的 java.lang.Class 物件,作為方法區類資料的訪問入口(反射介面),這個程序需要類加載器參與;
- 陣列類的特殊性:陣列類本身不通過類加載器創建,它是由 Java 虛擬機直接創建的:
- 如果陣列的組件型別是參考型別,那就遞回采用類加載加載;
- 如果陣列的組件型別不是參考型別,Java 虛擬機會把陣列標記為引導類加載器關聯;
- 陣列類的可見性與他的組件型別的可見性一致,如果組件型別不是參考型別,那陣列類的可見性將默認為 public;
- 陣列類的特殊性:陣列類本身不通過類加載器創建,它是由 Java 虛擬機直接創建的:
- 連接:負責把類的二進制資料合并到 JRE 中(將 Java 類的二進制代碼合并到 JVM 的運行狀態之中);
- 2. 驗證:確保加載的類資訊符合 JVM 規范,沒有安全方面的問題,驗證是否符合 Class 檔案格式規范,并且是否能被當前的虛擬機加載處理;
- (驗證即其之前都是操作位元組流的,之后操作基于方法區的存盤結構);
- 驗證程序包括檔案格式驗證、元資料驗證、位元組碼驗證(最復雜)、符號參考驗證
- 3. 準備:為類變數(static 變數)分配記憶體并設定類變數初始值的階段,這些記憶體都將在方法區中進行分配;(static 修飾的變數賦默認值,final 和 static 修飾的變數直接賦值(編譯時生成 ConstantValue 屬性));
- 4. 決議:(這里是靜態決議)虛擬機常量池的符號參考替換為直接參考程序;
- 符號參考:以一組符號來描述所參考的目標,符號可以使任何形式的字面量,與虛擬機的記憶體布局無關,參考的目標并不一定加載到記憶體中;
- 直接參考:可以使直接指向目標的指標、相對偏移量或是一個能間接定位到目標的句柄(與記憶體布局有關),與虛擬機布局相關;
- (決議及其之前都是虛擬機主導,之后是 Java 代碼主導);
- 2. 驗證:確保加載的類資訊符合 JVM 規范,沒有安全方面的問題,驗證是否符合 Class 檔案格式規范,并且是否能被當前的虛擬機加載處理;
- 5. 初始化:執行類構造器
<clinit>()方法的程序,為類的變數賦予正確的初始值,類構造器<clinit>()方法是由編譯器自動收藏類中的所有類變數的賦值動作和靜態陳述句塊(static塊)中的陳述句合并產生,代碼從上往下執行,如果發現父類還沒有進行過初始化,則需要先觸發其父類的初始化,虛擬機保證一個類的<clinit>()方法在多執行緒環境中被正確加鎖和同步; - 6. 使用;
- 7. 卸載;
6.3 類加載器
- 概述:
- 由 JVM 提供,是所有程式運行的基礎;
- 開發者可以通過繼承 ClassLoader 基類來創建自己的類加載器;
- 類加載器的任務就是根據一個類的全限定名來讀取此類的二進制位元組流到 JVM 中,然后轉換為一個與目標類對應的 java.lang.Class 物件實體;
- 最終產物就是位于堆中的 Class 物件,該物件封裝了類在方法區中的資料結構,并且向用戶提供了訪問方法區資料結構的介面,即 Java 反射的介面;
- 幾種類加載器:
- 啟動類加載器(Bootstrap Class Loader):用來加載 Java 的核心類,是用原生代碼來實作的,并不繼承自 java.lang.ClassLoader,加載
lib下或被-Xbootclasspath路徑下的類,C++ 實作,不允許直接通過參考啟動類加載器進行操作, - 擴展類加載器(Extensions Class Loader):Sun 公司(已被 Oracle 收購)實作的 sun.misc.Launcher$ExtClassLoader 類,由 Java 語言實作的,是 Launcher 的靜態內部類,它負責加載
<JAVA_HOME>/lib/ext目錄下或者由系統變數-Djava.ext.dir指定位路徑中的類別庫,開發者可以直接使用標準擴展類加載器; - 系統類加載器(System Class Loader)、應用程式類加載器(Application Class Loade):負責在 JVM 啟動時加載來自 Java 命令的
-classpath選項、java.class.path系統屬性,或者CLASSPATH將變數所指定的 JAR 包和類路徑,程式可以通過 ClassLoader 的靜態方法 getSystemClassLoader() 來獲取系統類加載器,如果沒有特別指定,則用戶自定義的類加載器都以此類加載器作為父加載器,由 Java 語言實作,父類加載器為 ExtClassLoader;
- 啟動類加載器(Bootstrap Class Loader):用來加載 Java 的核心類,是用原生代碼來實作的,并不繼承自 java.lang.ClassLoader,加載
- 類加載器間的關系:
- 啟動類加載器:C++ 實作,沒有父類;
- 拓展類加載器(ExtClassLoader):Java 實作,父類加載器為 Null;
- 系統類加載器(AppClassLoader):Java 實作,父類加載器為 ExtClassLoader;
- 自定義類加載器,父類加載器為 AppClassLoader;
- 類加載器的執行步驟:
- 1. 判斷緩沖區中是否有此 Class,如果有直接進入第 8 步,否則進入第 2 步;
- 2. 判斷父類加載器是否存在,存在則進入第 3 步,否則說明 Parent / 本身是啟動類加載器,則跳到第 4 步;
- 3. 請求使用父類加載器去載入目標類,如果載入成功則跳至第 8 步,否則接著執行第 5 步;
- 4. 請求使用啟動類加載器去載入目標類,如果載入成功則跳至第 8 步,否則跳至第 7 步;
- 5. 當前類加載器嘗試尋找 Class 檔案,如果找到則執行第 6 步,如果找不到則執行第 7 步;
- 6. 從檔案中載入 Class,成功后跳至第 8 步;
- 7. 拋出 ClassNotFountException 例外;
- 8. 回傳對應的 java.lang.Class 物件;

6.3 雙親委派模式
- 作業原理:如果一個類加載器收到了類加載請求,它并不會自己先去加載,而是把這個請求委托給父類的加載器去執行,如果父類加載器還存在其父類加載器,則進一步向上委托,依次遞回,請求最終將到達頂層的啟動類加載器,如果父類加載器可以完成類加載任務,就成功回傳,倘若父類加載器無法完成此加載任務,子加載器才會嘗試自己去加載;
- 優勢:Java 類隨著它的類加載器一起具備了一種帶有優先級的層次關系,通過這種層級關可以避免類的重復加載,即:當父親已經加載了該類時,就沒有必要子 ClassLoader 再加載一次,安全因素,Java 核心 API 中定義型別不會被隨意替換(父類已經加載過,從父類中查找回傳);
6.4 破壞雙親委派模式
- 到目前為止,雙親委派模型主要出現過3次較大規模的“被破壞的”情況:
- 第一次:主要是歷史問題,雙親委派模型在 JDK1.2 之后才被引入,在這之前用戶都是通過重寫 loadClass() 方法實作自定義加載器,為了向前兼容,JDK1.2 之后的 java.Lang.ClassLoader 添加了一個新的 protected 方法 findClass(),以此保證雙親委派模型;
- 第二次:由模型本身的缺陷導致的,缺陷在于:當某個類的介面使用父類加載器,而其實作類使用子類加載器時,父類加載器無法委托子類加載器作業,Java 服務介面 SPI 由 Java 核心庫提供,靠啟動類加載器來加載的,而 SPI 的實作類需要由應用程式類加載器來加載,在加載 SPI 的實作類時,啟動類加載器無法找到應用程式類加載器,因為依照雙親委派模型,BootstrapClassloader 無法委派 AppClassLoader 來加載類,JDK 設定執行緒背景關系類加載器(Thread Context ClassLoader),當父類加載器需要使用子類加載器(子類加載器未創建)時,會從父執行緒中繼承一個執行緒背景關系類加載器,以此請求子類加載器去完成類加載的動作,這種行為實際上已經打破了雙親委派模型的層次結構來逆向使用類加載器,已經違背了雙親委派模型的一般性原則;
- 第三次:由開發者對程式動態性的追求而導致,動態性指:代碼熱替換、模塊熱部署等,OSGi(面向Java的動態模塊化系統)實作模塊化熱部署的關鍵就是它自定義的類加載器機制的實作,當需要更換一個 Bundlle(程式模塊)時,就把 Bundle 連同類加載器一起換掉以實作代碼的熱替換,在替換時需要在平級間呼叫類加載器,在原則上破壞了雙親委派模型;
7. 虛擬機位元組碼執行引擎
“堆疊幀”的概念在《JVM | 第1部分:自動記憶體管理與性能調優》提到,這里不再贅述;
7.1 確定被呼叫的方法
- 決議:所有方法呼叫的目標方法在 Class 檔案里都是一個常量池中的符號參考,有兩種決議:
- 靜態決議:其中的一部分符號參考在
類加載的決議階段會被轉化為直接參考(即:靜態方法、final 修飾的方法、私有方法、父類方法、<init>方法,統稱非虛方法); - 動態鏈接:其他的符號參考會在
運行期被決議為直接參考; - Java 虛擬機提供了 5 條方法呼叫位元組碼指令:invokestatic(靜態方法)、invokespecial(實體構造器 <init> 方法、私有方法和父類方法)、invokevirtual(虛方法)、invokeinterface(介面方法)、invokedynamic(動態決議);
- 靜態決議:其中的一部分符號參考在
- 分派:用來確定虛方法的目標方法,體現 Java 面向物件的繼承、封裝和多型 3 大特性,有如下 4 種:
- 靜態分派:典型應用是處理
方法多載,多載的方法在經過編譯期編譯后得到相同的方法呼叫位元組碼指令和指令引數,虛擬機在處理多載時是通過引數的靜態型別,方法引數的允許發送型別轉變,但方法接收者本身靜態型別不變;- 如果物件 A 繼承 B,那么對于陳述句:
B b = new A();其中 B 稱為 b 變數的靜態型別(Static Type,編譯器可知),A 稱為 b 變數的實際型別(Actual Type,運行期可知); - 選擇靜態分派目標的程序(多載的本質),例如:嘗試呼叫方法
say('a'):- 'a' 首先是一個 char 型別:對應
say(char arg); - 其次還可以代表數字 97(參照 ASCII 碼):對應
say(int arg); - 而轉化為 97 之后,還可以轉型為 long 型別的 97L:對應
say(long arg); - 另外還能被自動裝箱包裝為 Character:對應
say(Character arg); - 裝箱類 Character 還實作了 Serializable 介面(若直接或間接實作了多個介面,優先級都是一樣的,如果出現能適配多個介面的多個多載方法,會提示型別模糊,拒絕編譯):對應
say(Serializable); - 而且 Character 還繼承自 Object(如果有多個父類,那將在繼承關系中從下往上開始搜索,越接近上層的優先級越低),對應
say(Object arg); - 最侄訓能匹配到變長型別:對應
say(char... arg);
- 'a' 首先是一個 char 型別:對應
- 如果物件 A 繼承 B,那么對于陳述句:
- 動態分派:典型應用是
方法重寫,Java 虛擬機在運行期會依據invokevirtual指令的多型查找程序,通過實際型別來分派方法執行版本的,程序如下:- 1. 找到運算元堆疊頂的第一個元素所指向的物件的實際型別,記做 M;
- 2. 如果在型別 M 中找到與常量中的描述符和簡單名稱都相符的方法,則進行訪問權限校驗,若通過則回傳這個方法的直接參考,查找程序結束;否則則回傳 IllegalAccessError 例外;
- 3. 否則,按照繼承關系從下往上依次對 M 的各個父類進行第 2 步的搜索和驗證程序;
- 4. 如果始終沒有找到合適的方法,則拋出 AbstractMethodError 例外;
- 單分派和多分派:方法的
接收者和方法的引數統稱為方法的宗量, 根據分派基于多少種宗量,可以將分派劃分為單分派和多分派兩種;
- 靜態分派:典型應用是處理
- 決議和分派不強調二選一的關系,強調的是在不同層次上的解決方案,例如:靜態方法會在類加載的
決議階段就進行直接參考的轉化,而靜態方法也是可以擁有多載版本的,選擇多載版本的程序也是通過靜態分派完成的; - Java 語言的 靜態多分派、動態單分派 示例:
- 方法多載:編譯期看靜態分派,運行期看動態分派;
public class Main {
static class A {
}
static class B extends A {
}
static class C extends B {
}
public void say(A a) {
System.out.println("A");
}
public void say(B b) {
System.out.println("B");
}
public void say(C c) {
System.out.println("C");
}
public static void main(String[] args) throws Exception {
Main main = new Main();
Main superMain = new Super();
B os = new C();
main.say(os);
superMain.say((A) os);
//輸出 B S-A
}
}
class Super extends Main {
public void say(A a) {
System.out.println("S-A");
}
public void say(B b) {
System.out.println("S-B");
}
public void say(C c) {
System.out.println("S-C");
}
}
- 編譯期看靜態分派 - 多分派:
- main 和 superMain 的靜態型別都是 Main,方法引數的靜態型別一個是 B,一個是 A,所以此次選擇產生的兩條 invokevitrual 指令的引數分別為常量池中指向 Main.say(B) 和 Main.say(A) 的方法的符號參考,這里根據兩個宗量(方法接受者和引數)進行選擇;
- 運行期看動態分派 - 單分派:
- 這階段 Java 虛擬機此時不用關心引數的靜態型別、實際型別,只有方法接收者的實際型別會影響到方法版本的選擇,Main.say(B) 和 Main.say(A) 方法的實際型別分別是 Main.say(B) 和 Super.say(A),也就是只有一個宗量作為選擇依據;
最后

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/449048.html
標籤:Java
下一篇:python之面向物件的程式開發
