【JVM故事】了解JVM的結構,好在面試時吹牛
- 2019 年 12 月 5 日
- 筆記
本文公眾號來源:編程新說
作者:編程新說李新傑
本文已收錄至我的GitHub
數據類型
jvm包括兩種數據類型,基本類型和引用類型。 基本類型包括,數值類型,boolean類型,和returnAddress類型。 數值類型包括,整型,浮點型,和char類型。 boolean類型同樣只有true和false。 returnAddress類型是一個指針,指向jvm指令的操作碼,在Java中沒有與之對應的類型。 boolean類型的操作會被轉化為int類型的操作進行,boolean數組會當成byte數組去操作。1表示true,0表示false。 引用類型包括三種,類類型,數組類型,和介面類型。 它們的值是動態創建的類實例,數組,或實現介面的類實例。 數組有component類型和element類型,component類型就是數組去掉最外層維度後剩下的類型,可能還是一個數組類型(對於多維數組)。 element類型就是數組裡面存儲的最小數據的類型,它必須是一個基本類型,類類型,或介面類型。 對於一維數組的話,component類型和element類型是相同的。 引用類型還有一個特殊值,就是null,表示沒有引用任何對象。 運行時公有數據區 堆 jvm有一個堆,在所有jvm執行緒間共享,堆是一個運行時數據區域,所有為類實例和數組分配的記憶體都來自於它。 堆在jvm啟動時創建,堆中對象不用顯式釋放,gc會幫我們釋放並回收記憶體。 方法區 jvm有一個方法區,在所有jvm執行緒間共享,它存儲每一個類的結構。 像運行時常量池,欄位和方法數據,方法和構造函數的程式碼,還有特殊的方法用於類和實例的初始化,以及介面的初始化。 方法區在jvm啟動時創建,雖然方法區在邏輯上是堆的一部分。 但簡單實現時可以選擇不進行gc和壓縮,本規範沒有強制要求方法區的位置,也沒有要求管理已編譯程式碼的策略。 運行時常量池 運行時常量池就是類或介面的位元組碼文件里的常量池的運行時表示形式,它包含幾種常量。 如在編譯時就已經知道的數字字面量值,和必須在運行時解析的方法和欄位的引用,運行時常量池的功能類似於傳統語言的符號表,不過它包含的數據會更加寬泛。 運行時常量池分配在jvm的方法區,類或介面的運行時常量池在類或介面被jvm創建時才會構建。 運行時私有數據區 pc暫存器 jvm支援一次運行多個執行緒,每個執行緒都有自己的pc暫存器,任何時候一個執行緒只能運行一個方法的程式碼。 如果方法不是native的,pc暫存器包含當前正在被執行的jvm指令地址,如果方法是native的,pc暫存器的值是未定義的。 jvm棧 每一個jvm執行緒都有一個私有的jvm棧,隨著執行緒的創建而創建,棧中存儲的是幀。 jvm棧和傳統語言如C的棧相似,保存局部變數和部分計算結果,參與方法的調用和返回。jvm棧主要用於幀的出棧和入棧,除此之外沒有其它操作, 幀可能是在堆上分配的,所以jvm棧使用的記憶體不必是連續的。 native方法棧 native方法不是用Java語言寫的,為了支援它需要使用傳統棧,如C語言棧。不過jvm不能載入native方法,所以也不需要提供native方法需要的棧。 幀 每次當一個方法被調用時一個新的幀會被創建。當方法調用完成時,與之對應的幀會被銷毀,無論是正常完成還是拋異常結束。 所以幀是方法調用的具體體現形式,或稱方法調用是以幀的形式進行的。幀用來存儲數據和部分計算結果,和執行動態鏈接,方法返回值,分發異常。 幀分配在創建幀的執行緒的jvm棧上,每一個幀都有自己的本地變數數組,自己的操作數據棧,和一個對當前方法所在類的運行時常量池的引用。 本地變數數組和操作數棧的大小在編譯時就確定了,它們隨著和幀關聯的方法編譯後的程式碼一起被提供,因此幀這種數據結構的大小隻依賴於jvm的實現,這些結構所需的記憶體可以在方法調用時同時被分配。 在一個執行緒執行的任何時刻,都只會有一個幀是處於激活的。這個幀被稱為當前幀,與之對應的方法被稱為當前方法,方法所在的類被稱為當前類,此時用到的本地變數數組和操作數棧也都是當前幀的。 一個幀將不在繼續是當前幀,如果它的方法調用了另一個方法,或者它的方法結束了。 當一個方法被調用,一個新的幀被創建,當執行控制由原來的方法傳遞到新的方法時,這個新的幀變為當前幀。 當方法返回時,當前幀把方法執行的結果傳回到上一幀,當上一幀被激活的同時當前幀會被丟棄。 本地變數數組 每一幀都包含一個變數數組,就是都熟知的本地變數存儲的地方。這個本地變數數組的長度在編譯時確定,隨著編譯後的方法程式碼一起提供。 通常一個本地變數(的位置)能夠存儲一個類型的值,但是long和double類型卻需要兩個本地變數(的位置)才能存一個值。 本地變數按索引定址,第一個本地變數的索引是0。long和double需要消耗兩個連續的索引,但卻是按照較小的這個索引定址的。不能按照較大的那個索引去讀數據,但是可以寫入,當然這樣將使本地變數內容錯亂。 在方法被調用時,jvm使用本地變數來接收傳遞進來的參數值。在類(靜態)方法調用時,所有參數被傳入從索引0開始的連貫的本地變數數組裡。 在實例(非靜態)方法調用時,索引0處總是傳入正在其上執行方法調用的那個對象的引用,(就是Java中的this了),所有參數被傳入從1開始的連貫的本地變數數組裡。 操作數棧 每個幀包含一個後進先出的棧,用於存儲正在執行的jvm指令的操作數,就是都熟知的操作數棧,這個棧的最大深度在編譯時就已確定,隨著編譯後的方法程式碼一起提供。 當幀被創建時,操作數棧是空的,jvm提供一些指令用於載入常量值,本地變數值,欄位值到操作數棧上,另一些jvm指令採用操作數棧上的操作數進行操作,並把結果放回到操作數棧上。 操作數棧也用於準備將要傳遞給方法調用的參數和接收方法調用返回的結果。 long和double類型的值佔用兩個單位的棧深度,其它類型的值佔用一個單位的棧深度。 動態鏈接 每一個幀都包含了對當前方法所屬類型的運行時常量池的引用。目的是為了支援方法程式碼的動態鏈接。class文件中描述一個方法引用被調用的方法和被訪問的變數的程式碼,是採用符號引用的形式實現的。 符號引用的形式可以粗略的認為是字元串的形式,就是用字元串標明需要調用哪個類的哪個方法或訪問哪個欄位或變數。就像符號引用這個名字一樣,這些僅僅是符號,是拿不到具體值的,所以必須要進行轉換。 動態鏈接就是把這些符號方法引用轉換為具體的方法引用,在必要時載入類來解析尚未明確的符號,把符號變數的訪問轉換為這些變數運行時所在存儲結構的適合的偏移量(索引)。這樣的方式又稱為後期綁定。 方法調用 一個方法調用正常完成(即沒有拋異常)時,會根據所返回的值的類型執行一個適合的return指令,當前幀會去恢復調用者的狀態,包括它的本地變數和操作數棧,使調用者的程式計數器適合的遞增來跳過剛剛的那個方法調用指令。 返回值會被放到調用者幀的操作數棧上,然後繼續執行調用者方法的幀。 一個方法在調用時拋出了異常,且這個異常沒有在這個方法內被捕獲處理,將會導致這個方法調用的突然結束,這種情況下永遠不會向方法的調用者返回一個值。 特殊方法 站在jvm的級別,每一個用Java寫的構造函數都以一個實例初始化方法出現,且都是特殊的名字,就是<init>,這個名字是編譯器提供的。 實例初始化方法只能在jvm內部使用invokespecial這個指令調用,且只能在尚未初始化的類實例上調用。 一個類或介面最多可以有一個類或介面初始化方法,通過調用這個方法被初始化。類或介面的初始化方法也有特殊的名字,就是<clinit>,該方法沒有參數,且返回值是void。 方法名稱也是由編譯器提供的,從Java7開始,在位元組碼中這個方法必須被標記為靜態的才行。 這個初始化方法是被jvm隱式調用的,它們絕對不會直接被用任何jvm指令調用,僅作為類初始化進程的一部分被間接的調用。 Java類庫 jvm必須為Java類庫的實現提供足夠的支援。一些類庫中的類如果沒有jvm協助是無法實現的。 反射,就是在運行時獲取某個類的類型相關資訊,如它的欄位資訊,方法資訊,構造函數資訊,父類資訊,實現的介面資訊。 這些資訊都必須是把一個類載入完之後才可以知道的,只有jvm才可以載入類。如java.lang.reflect這個包下的類和Class這個類。 在Java中載入一個類或介面用類載入器,即ClassLoader,背後還是委託給jvm來實現的。 鏈接和初始化一個類或介面。 安全,如java.security包下的類,還有其它類像SecurityManager。 多執行緒,如執行緒這個類Thread。 弱引用,像java.lang.ref包下的類。 公有設計,私有實現 以上內容只是jvm的一個「相對寬泛」的規範,它並不是實現方案,也不是實現細節。 實現者可以根據自身的需要來實現jvm,如運行在後端伺服器上的jvm和運行在移動設備上的jvm肯定側重點有所不同。 從事Java的人都知道,事實上jvm是有較多的實現版本。 由於jvm是處在Java語言和作業系統之間的,所以它要向上提供對Java的支援,向下與作業系統良好交互。 寫在最後 高級語言(Java,C#)中的很多操作如文件操作,網路操作,記憶體操作,執行緒操作,I/O操作等,都不是高級語言自身能夠實現的。 也不是它們的虛擬機(JVM,CLR)能夠實現的,實際最終是由作業系統實現的,因為這些都是系統資源,只有作業系統才有許可權訪問。 如果你用Java或C#程式碼創建了一個文件,千萬不要以為是Java或C#創建了這個文件,它們只是層層向下調用了作業系統的API,然後到文件系統API,最後可能到磁碟驅動程式。 由此可以看出,要想設計一門語言,不單單是關鍵字、語法、編譯器,類庫,虛擬機這些,還要深度了解作業系統,甚至是硬體,如CPU架構和CPU指令集等。 所以,和語言相關的事情,每一項都是異常的繁瑣複雜,都需要投入大量的人力、財力、時間去研究,最後即使研究成功了,可能沒有生態,沒人使用,自然也無法賺錢。