當前位置:
首頁 > 知識 > 淺析Java虛擬機結構與 機制

淺析Java虛擬機結構與 機制

本文旨在給所有希望了解JVM(Java Virtual Machine)的同學一個概念性的入門,主要介紹了JVM的組成部分以及它們內部工作的機制和原理。當然本文只是一個簡單的入門,不會涉及過多繁雜的參數和配置,感興趣的同學可以做更深入的研究,在研究JVM的過程中會發現,其實JVM本身就是一個計算機體系結構,很多原理和我們平時的硬體、微機原理、操作系統都有十分相似的地方,所以學習JVM本身也是加深自我對計算機結構認識的一個很好的途徑。

另外需要注意的是,雖然平時我們用的大多是Sun(現已被Oracle收購)JDK提供的JVM,但是JVM本身是一個規範,所以可以有多種實現,除了Hotspot外,還有諸如Oracle的JRockit、IBM的J9也都是非常有名的JVM。

一、JVM結構

下圖展示了JVM的主要結構:

可以看出,JVM主要由類載入器子系統、運行時數據區(內存空間)、執行引擎以及與本地方法介面等組成。其中運行時數據區又由方法區、堆、Java棧、PC寄存器、本地方法棧組成。

從上圖中還可以看出,在內存空間中方法區和堆是所有Java線程共享的,而Java棧、本地方法棧、PC寄存器則由每個線程私有,這會引出一些問題,後文會進行具體討論。

眾所周知,Java語言具有跨平台的特性,這也是由JVM來實現的。更準確地說,是Sun利用JVM在不同平台上的實現幫我們把平台相關性的問題給解決了,這就好比是HTML語言可以在不同廠商的瀏覽器上呈現元素(雖然某些瀏覽器在對W3C標準的支持上還有一些問題)。同時,Java語言支持通過JNI(Java Native Interface)來實現本地方法的調用,但是需要注意到,如果你在Java程序用調用了本地方法,那麼你的程序就很可能不再具有跨平台性,即本地方法會破壞平台無關性。

二、類載入器子系統(Class Loader)

類載入器子系統負責載入編譯好的.class位元組碼文件,並裝入內存,使JVM可以實例化或以其它方式使用載入後的類。JVM的類載入子系統支持在運行時的動態載入,動態載入的優點有很多,例如可以節省內存空間、靈活地從網路上載入類,動態載入的另一好處是可以通過命名空間的分隔來實現類的隔離,增強了整個系統的安全性。

1、ClassLoader的分類:

a.啟動類載入器(BootStrap Class Loader):負責載入rt.jar文件中所有的Java類,即Java的核心類都是由該ClassLoader載入。在Sun JDK中,這個類載入器是由C++實現的,並且在Java語言中無法獲得它的引用。

b.擴展類載入器(Extension Class Loader):負責載入一些擴展功能的jar包。

c.系統類載入器(System Class Loader):負責載入啟動參數中指定的Classpath中的jar包及目錄,通常我們自己寫的Java類也是由該ClassLoader載入。在Sun JDK中,系統類載入器的名字叫AppClassLoader。

d.用戶自定義類載入器(User Defined Class Loader):由用戶自定義類的載入規則,可以手動控制載入過程中的步驟。

2、ClassLoader的工作原理

類載入分為裝載、鏈接、初始化三步。

a.裝載

通過類的全限定名和ClassLoader載入類,主要是將指定的.class文件載入至JVM。當類被載入以後,在JVM內部就以「類的全限定名+ClassLoader實例ID」來標明類。

在內存中,ClassLoader實例和類的實例都位於堆中,它們的類信息都位於方法區。

裝載過程採用了一種被稱為「雙親委派模型(Parent Delegation Model)」的方式,當一個ClassLoader要載入類時,它會先請求它的雙親ClassLoader(其實這裡只有兩個ClassLoader,所以稱為父ClassLoader可能更容易理解)載入類,而它的雙親ClassLoader會繼續把載入請求提交再上一級的ClassLoader,直到啟動類載入器。只有其雙親ClassLoader無法載入指定的類時,它才會自己載入類。

雙親委派模型是JVM的第一道安全防線,它保證了類的安全載入,這裡同時依賴了類載入器隔離的原理:不同類載入器載入的類之間是無法直接交互的,即使是同一個類,被不同的ClassLoader載入,它們也無法感知到彼此的存在。這樣即使有惡意的類冒充自己在核心包(例如java.lang)下,由於它無法被啟動類載入器載入,也造成不了危害。

由此也可見,如果用戶自定義了類載入器,那就必須自己保障類載入過程中的安全。

b.鏈接

鏈接的任務是把二進位的類型信息合并到JVM運行時狀態中去。

鏈接分為以下三步:

a.驗證:校驗.class文件的正確性,確保該文件是符合規範定義的,並且適合當前JVM使用。

b.準備:為類分配內存,同時初始化類中的靜態變數賦值為默認值。

c.解析(可選):主要是把類的常量池中的符號引用解析為直接引用,這一步可以在用到相應的引用時再解析。

c.初始化

初始化類中的靜態變數,並執行類中的static代碼、構造函數。

JVM規範嚴格定義了何時需要對類進行初始化:

a、通過new關鍵字、反射、clone、反序列化機制實例化對象時。

b、調用類的靜態方法時。

c、使用類的靜態欄位或對其賦值時。

d、通過反射調用類的方法時。

e、初始化該類的子類時(初始化子類前其父類必須已經被初始化)。

f、JVM啟動時被標記為啟動類的類(簡單理解為具有main方法的類)。

三、Java棧(Java Stack)

Java棧由棧幀組成,一個幀對應一個方法調用。調用方法時壓入棧幀,方法返回時彈出棧幀並拋棄。Java棧的主要任務是存儲方法參數、局部變數、中間運算結果,並且提供部分其它模塊工作需要的數據。前面已經提到Java棧是線程私有的,這就保證了線程安全性,使得程序員無需考慮棧同步訪問的問題,只有線程本身可以訪問它自己的局部變數區。

它分為三部分:局部變數區、操作數棧、幀數據區。

1、局部變數區

局部變數區是以字長為單位的數組,在這裡,byte、short、char類型會被轉換成int類型存儲,除了long和double類型佔兩個字長以外,其餘類型都只佔用一個字長。特別地,boolean類型在編譯時會被轉換成int或byte類型,boolean數組會被當做byte類型數組來處理。局部變數區也會包含對象的引用,包括類引用、介面引用以及數組引用。

局部變數區包含了方法參數和局部變數,此外,實例方法隱含第一個局部變數this,它指向調用該方法的對象引用。對於對象,局部變數區中永遠只有指向堆的引用。

2、操作數棧

操作數棧也是以字長為單位的數組,但是正如其名,它只能進行入棧出棧的基本操作。在進行計算時,操作數被彈出棧,計算完畢後再入棧。

3、幀數據區

幀數據區的任務主要有:

a.記錄指向類的常量池的指針,以便於解析。

b.幫助方法的正常返回,包括恢復調用該方法的棧幀,設置PC寄存器指向調用方法對應的下一條指令,把返回值壓入調用棧幀的操作數棧中。

c.記錄異常表,發生異常時將控制權交由對應異常的catch子句,如果沒有找到對應的catch子句,會恢復調用方法的棧幀並重新拋出異常。

局部變數區和操作數棧的大小依照具體方法在編譯時就已經確定。調用方法時會從方法區中找到對應類的類型信息,從中得到具體方法的局部變數區和操作數棧的大小,依此分配棧幀內存,壓入Java棧。

四、本地方法棧(Native Method Stack)

本地方法棧類似於Java棧,主要存儲了本地方法調用的狀態。在Sun JDK中,本地方法棧和Java棧是同一個。

五、方法區(Method Area)

類型信息和類的靜態變數都存儲在方法區中。方法區中對於每個類存儲了以下數據:

a.類及其父類的全限定名(java.lang.Object沒有父類)

b.類的類型(Class or Interface)

c.訪問修飾符(public, abstract, final)

d.實現的介面的全限定名的列表

e.常量池

f.欄位信息

g.方法信息

h.靜態變數

i.ClassLoader引用

j.Class引用

可見類的所有信息都存儲在方法區中。由於方法區是所有線程共享的,所以必須保證線程安全,舉例來說,如果兩個類同時要載入一個尚未被載入的類,那麼一個類會請求它的ClassLoader去載入需要的類,另一個類只能等待而不會重複載入。

此外為了加快調用方法的速度,通常還會為每個非抽象類創建私有的方法表,方法表是一個數組,存放了實例可能被調用的實例方法的直接引用。方法表對於多態有非常重要的意義,具體可以參照《淺談多態機制的意義及實現》一文中「多態的實現」一節。

在Sun JDK中,方法區對應了持久代(Permanent Generation),默認最小值為16MB,最大值為64MB。

六、堆(Heap)

堆用於存儲對象實例以及數組值。堆中有指向類數據的指針,該指針指向了方法區中對應的類型信息。堆中還可能存放了指向方法表的指針。堆是所有線程共享的,所以在進行實例化對象等操作時,需要解決同步問題。此外,堆中的實例數據中還包含了對象鎖,並且針對不同的垃圾收集策略,可能存放了引用計數或清掃標記等數據。

在堆的管理上,Sun JDK從1.2版本開始引入了分代管理的方式。主要分為新生代、舊生代。分代方式大大改善了垃圾收集的效率。

1、新生代(New Generation)

大多數情況下新對象都被分配在新生代中,新生代由Eden Space和兩塊相同大小的Survivor Space組成,後兩者主要用於Minor GC時的對象複製(Minor GC的過程在此不詳細討論)。

JVM在Eden Space中會開闢一小塊獨立的TLAB(Thread Local Allocation Buffer)區域用於更高效的內存分配,我們知道在堆上分配內存需要鎖定整個堆,而在TLAB上則不需要,JVM在分配對象時會盡量在TLAB上分配,以提高效率。

2、舊生代(Old Generation/Tenuring Generation)

在新生代中存活時間較久的對象將會被轉入舊生代,舊生代進行垃圾收集的頻率沒有新生代高。

七、執行引擎

執行引擎是JVM執行Java位元組碼的核心,執行方式主要分為解釋執行、編譯執行、自適應優化執行、硬體晶元執行方式。

JVM的指令集是基於棧而非寄存器的,這樣做的好處在於可以使指令儘可能緊湊,便於快速地在網路上傳輸(別忘了Java最初就是為網路設計的),同時也很容易適應通用寄存器較少的平台,並且有利於代碼優化,由於Java棧和PC寄存器是線程私有的,線程之間無法互相干涉彼此的棧。每個線程擁有獨立的JVM執行引擎實例。

JVM指令由單位元組操作碼和若干操作數組成。對於需要操作數的指令,通常是先把操作數壓入操作數棧,即使是對局部變數賦值,也會先入棧再賦值。注意這裡是「通常」情況,之後會講到由於優化導致的例外。

1、解釋執行

和一些動態語言類似,JVM可以解釋執行位元組碼。Sun JDK採用了token-threading的方式,感興趣的同學可以深入了解一下。

解釋執行中有幾種優化方式:

a.棧頂緩存

將位於操作數棧頂的值直接緩存在寄存器上,對於大部分只需要一個操作數的指令而言,就無需再入棧,可以直接在寄存器上進行計算,結果壓入操作數站。這樣便減少了寄存器和內存的交換開銷。

b.部分棧幀共享

被調用方法可將調用方法棧幀中的操作數棧作為自己的局部變數區,這樣在獲取方法參數時減少了複製參數的開銷。

c.執行機器指令

在一些特殊情況下,JVM會執行機器指令以提高速度。

2、編譯執行

為了提升執行速度,Sun JDK提供了將位元組碼編譯為機器指令的支持,主要利用了JIT(Just-In-Time)編譯器在運行時進行編譯,它會在第一次執行時編譯位元組碼為機器碼並緩存,之後就可以重複利用。Oracle JRockit採用的是完全的編譯執行。

3、自適應優化執行

自適應優化執行的思想是程序中10%~20%的代碼佔據了80%~90%的執行時間,所以通過將那少部分代碼編譯為優化過的機器碼就可以大大提升執行效率。自適應優化的典型代表是Sun的Hotspot VM,正如其名,JVM會監測代碼的執行情況,當判斷特定方法是瓶頸或熱點時,將會啟動一個後台線程,把該方法的位元組碼編譯為極度優化的、靜態鏈接的C++代碼。當方法不再是熱區時,則會取消編譯過的代碼,重新進行解釋執行。

自適應優化不僅通過利用小部分的編譯時間獲得大部分的效率提升,而且由於在執行過程中時刻監測,對內聯代碼等優化也起到了很大的作用。由於面向對象的多態性,一個方法可能對應了很多種不同實現,自適應優化就可以通過監測只內聯那些用到的代碼,大大減少了內聯函數的大小。

Sun JDK在編譯上採用了兩種模式:Client和Server模式。前者較為輕量級,佔用內存較少。後者的優化程序更高,佔用內存更多。

在Server模式中會進行對象的逃逸分析,即方法中的對象是否會在方法外使用,如果被其它方法使用了,則該對象是逃逸的。對於非逃逸對象,JVM會在棧上直接分配對象(所以對象不一定是在堆上分配的),線程獲取對象會更加快速,同時當方法返回時,由於棧幀被拋棄,也有利於對象的垃圾收集。Server模式還會通過分析去除一些不必要的同步,感興趣的同學可以研究一下Sun JDK 6引入的Biased Locking機制。

此外,執行引擎也必須保證線程安全性,因而JMM(Java Memory Model)也是由執行引擎確保的。


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 java學習吧 的精彩文章:

Java常見問題匯總(一)
Java新手問題匯總
java 基礎 語法

TAG:java學習吧 |

您可能感興趣

研究揭示亞細胞核結構nuclear speckle在mRNA出核中的功能與機制
淺析Myobrace的結構與功能作用的關係
Serrano-Santos:適配體結構變化控制釋放納米器件的表徵
Nature:從結構上揭示DNA解鏈機制
Redmi K20 Pro內部結構揭秘,手機用料究竟穩不穩?
書單推薦包裝設計包裝的結構藝術II Structural Packaging Art
python數據結構和演算法「總結」
協作機器人界OnRobot合併,阿里巴巴AI實驗室發布新服務機器人,FIBERBOTS用機器人群構建結構
機能結構造型!全新 React Runner Mid 官圖釋出!
網友測試OPPO Find X的機械結構,結果真相了
Attbotics效法蟻窩結構打造高效率倉儲系統
法國Stelia公司公開展示3D印表機身面板結構驗證件
Nat Commun:新型金屬-有機物框架模擬DNA的結構
多份專利公布 一窺Magic Leap One內部結構和工作原理
結構 Vionnet 廓形與插片
Oracle資料庫儲存結構
Nature 子刊:加強共價有機骨架網路結構結合力的新技術
多份專利公布,一窺Magic Leap One內部結構和工作原理
「像造iPhone一樣造木結構建築」!聽聽Katerra首席工程師怎麼說
Redis 數據結構與內存管理策略(上)