1. 程式人生 > >【JVM故事】瞭解JVM的結構,好在面試時吹牛

【JVM故事】瞭解JVM的結構,好在面試時吹牛


class檔案格式


參考上一篇文章《【JVM故事】一個Java位元組碼檔案的誕生記》,後續還會專門講解class檔案的內部結構。


資料型別


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指令集等。

所以,和語言相關的事情,每一項都是異常的繁瑣複雜,都需要投入大量的人力、財力、時間去研究,最後即使研究成功了,可能沒有生態,沒人使用,自然也無法賺錢。

因此,國人現在還沒有一門屬於自己的真正語言。

>>> 熱門文章集錦 <<<

 

畢業10年,我有話說

【面試】我是如何面試別人List相關知識的,深度有點長文

我是如何在畢業不久只用1年就升為開發組長的

爸爸又給Spring MVC生了個弟弟叫Spring WebFlux

【面試】我是如何在面試別人Spring事務時“套路”對方的

【面試】Spring事務面試考點吐血整理(建議珍藏)

【面試】我是如何在面試別人Redis相關知識時“軟懟”他的

【面試】吃透了這些Redis知識點,面試官一定覺得你很NB(乾貨 | 建議珍藏)

【面試】如果你這樣回答“什麼是執行緒安全”,面試官都會對你刮目相看(建議珍藏)

【面試】迄今為止把同步/非同步/阻塞/非阻塞/BIO/NIO/AIO講的這麼清楚的好文章(快快珍藏)

【面試】一篇文章幫你徹底搞清楚“I/O多路複用”和“非同步I/O”的前世今生(深度好文,建議珍藏)

【面試】如果把執行緒當作一個人來對待,所有問題都瞬間明白了

Java多執行緒通關———基礎知識挑戰

品Spring:帝國的基石

 

作者是工作超過10年的碼農,現在任架構師。喜歡研究技術,崇尚簡單快樂。追求以通俗易懂的語言解說技術,希望所有的讀者都能看懂並記住。下面是公眾號的二維碼,歡迎關注!

&n