詳細解讀JVM(一)——JVM結構

寫在前面:

  • 你好,歡迎你的閱讀!
  • 我熱愛技術,熱愛分享,熱愛生活, 我始終相信:技術是開源的,知識是共享的!
  • 博客裏面的內容大部分均爲原創,是自己日常的學習記錄和總結,便於自己在後面的時間裏回顧,當然也是希望可以分享自己的知識。目前的內容幾乎是基礎知識和技術入門,如果你覺得還可以的話不妨關注一下,我們共同進步!
  • 除了分享博客之外,也喜歡看書,寫一點日常雜文和心情分享,如果你感興趣,也可以關注關注!
  • 微信公衆號:傲驕鹿先生
     

一、JVM結構

1、jvm的基本結構

(1)類加載子系統負責從文件系統或者網絡中加載Class信息,加載的類信息存放於一塊稱爲方法區的內存空間。除了類的信息外,方法區中可能還會存放運行時常量池信息,包括字符串字面量和數字常量(這部分常量信息是Class文件中常量池部分的內存映射)。

(2)java堆在虛擬機啓動的時候建立,它是java程序最主要的內存工作區域。幾乎所有的java對象實例都存放在java堆中。堆空間是所有線程共享的,這是一塊與java應用密切相關的內存空間。

(3)java的NIO庫允許java程序使用直接內存。直接內存是在java堆外的、直接向系統申請的內存空間。通常訪問直接內存的速度會優於java堆。因此出於性能的考慮,讀寫頻繁的場合可能會考慮使用直接內存。由於直接內存在java堆外,因此它的大小不會直接受限於Xmx指定的最大堆大小,但是系統內存是有限的,java堆和直接內存的總和依然受限於操作系統能給出的最大內存。

(4)垃圾回收系統是java虛擬機的重要組成部分,垃圾回收器可以對方法區、java堆和直接內存進行回收。其中,java堆是垃圾收集器的工作重點。和C/C++不同,java中所有的對象空間釋放都是隱式的,也就是說,java中沒有類似free()或者delete()這樣的函數釋放指定的內存區域。對於不再使用的垃圾對象,垃圾回收系統會在後臺默默工作,默默查找、標識並釋放垃圾對象,完成包括java堆、方法區和直接內存中的全自動化管理。

(5)每一個java虛擬機線程都有一個私有的java棧,一個線程的java棧在線程創建的時候被創建,java棧中保存着幀信息,java棧中保存着局部變量、方法參數,同時和java方法的調用、返回密切相關。

(6)本地方法棧和java棧非常類似,最大的不同在於java棧用於方法的調用,而本地方法棧則用於本地方法的調用,作爲對java虛擬機的重要擴展,java虛擬機允許java直接調用本地方法(通常使用C編寫)

(7)PC(Program Counter)寄存器也是每一個線程私有的空間,java虛擬機會爲每一個java線程創建PC寄存器。在任意時刻,一個java線程總是在執行一個方法,這個正在被執行的方法稱爲當前方法。如果當前方法不是本地方法,PC寄存器就會指向當前正在被執行的指令。如果當前方法是本地方法,那麼PC寄存器的值就是undefined

(8)執行引擎是java虛擬機的最核心組件之一,它負責執行虛擬機的字節碼,現代虛擬機爲了提高執行效率,會使用即時編譯技術將方法編譯成機器碼後再執行。

2、類加載系統

在JAVA虛擬機中,負責查找並裝載類型的那部分被稱爲類裝載子系統。

JAVA虛擬機有兩種類裝載器:啓動類裝載器和用戶自定義類裝載器。前者是JAVA虛擬機實現的一部分,後者則是Java程序的一部分。由不同的類裝載器裝載的類將被放在虛擬機內部的不同命名空間中。

類裝載器子系統涉及Java虛擬機的其他幾個組成部分,以及幾個來自java.lang庫的類。比如,用戶自定義的類裝載器是普通的Java對象,它的類必須派生自java.lang.ClassLoader類。ClassLoader中定義的方法爲程序提供了訪問類裝載器機制的接口。此外,對於每一個被裝載的類型,JAVA虛擬機都會爲它創建一個java.lang.Class類的實例來代表該類型。和所有其他對象一樣,用戶自定義的類裝載器以及Class類的實例都放在內存中的堆區,而裝載的類型信息則都位於方法區。

類裝載器子系統除了要定位和導入二進制class文件外,還必須負責驗證被導入類的正確性,爲類變量分配並初始化內存,以及幫助解析符號引用。這些動作必須嚴格按以下順序進行:

(1)加載(Loading)

通過一個類的全限定名獲取此類的二進制字節流,將這個字節流所代表的靜態存儲結構轉化爲方法區的運行時數據結構,在內存中生成一個代表這個類的java.lang.Class對象作爲方法區這個類的各種數據訪問入口

(2)鏈接(Linking)

  • 驗證(Verify):確保class文件的字節流中包含信息符合當前虛擬機的要求,保證被加載的類的正確性。主要包含四種驗證:文件格式驗證、元數據驗證、字節碼驗證、符號引用驗證

  • 準備(Prepare):爲類變量分配內存並且設置該類變量的默認初始值。不包含用final修飾的類變量,因爲final在編譯的時候就分配了,在準備階段會顯式初始化。不會爲實例變量分配初始化,實例變量會隨着對象一起分配到堆中,類變量會分配在方法區中

  • 解析(Resolve):將常量池中的符號引用轉化爲直接引用的過程。事實上,解析動作往往會在JVM執行完初始化之後再執行。符號引用就是用一組符號來描述所引用的目標,符號引用的字面量形式明確定義在Java虛擬機規範的class文件格式中。直接引用就是直接指向目標的指針、相對偏移量或者一個間接定位到目標的句柄。解析動作主要針對類或接口、字段、類方法、接口方法、方法類型等

(3)初始化(Initialzation)

初始化階段就是執行類構造器方法<clinit>()的過程,該方法是編譯器自動收集類中所有類變量的賦值動作和靜態代碼塊中的語句合併而來。構造器方法中的指令按語句在源文件中出現的順序執行。<clinit>()不同於<init>()。如果該類存在父類,JVM會保證子類的<clinit>()執行前父類的<clinit>()已經執行完成

每個類裝載器都有自己的命名空間,其中維護着由它裝載的類型。所以一個Java程序可以多次裝載具有同一個全限定名的多個類型。這樣一個類型的全限定名就不足以確定在一個Java虛擬機中的唯一性。因此,當多個類裝載器都裝載了同名的類型時,爲了惟一地標識該類型,還要在類型名稱前加上裝載該類型(指出它所位於的命名空間)的類裝載器標識。

3、 java堆

JVM的內存分代策略

Java虛擬及根據對象存貨的週期不同,將堆內存劃分爲幾塊,一般分爲新生代、老年代和永久代(對HotSpot虛擬機而言),這就是jvm的內存分代策略。

Java堆是和應用程序關係最爲密切的內存空間,幾乎所有的對象都存放在堆上。並且java堆是完全自動化管理的,通過垃圾回收機制,垃圾對象會被自動清理,而不需要顯示的釋放。給堆內存分代是爲了提高對象內存分配和垃圾回收的效率。有了內存分代,新創建的對象會在新生代中分配內存,經過多次回收任然活下來的對象存放在老年代中,靜態屬性、類信息等存放在永久代中,新生代中對象的存活時間端,只需要在新生代區域中頻繁驚醒GC,老年代中對象生命週期長,內存回收的頻率相對較低,不需要頻繁回收,永久代中回收效果差,一般不進行垃圾回收。還可以根據不同年代的特點採用合適的垃圾收集算法。這些都是內存分代的好處。

根據java回收機制的不同,java堆有可能擁有不同的結構。最爲常見的一種構成是將整個java堆分爲新生代和老年代。其中新生代存放新生對象或者年齡不大的對象,老年代則存放老年對象。新生代有可能分爲eden區、s0區、s1區,s0區和s1區也被稱爲from和to區,他們是兩塊大小相同、可以互換角色的內存空間。

如下圖:顯示了一個堆空間的一般結構:

在絕大多數情況下,對象首先分配在eden區,在一次新生代回收之後,如果對象還存活,則進入s0或者s1,每經過一次新生代回收,對象如果存活,它的年齡就會加1。當對象的年齡達到一定條件後,就會被認爲是老年對象,從而進入老年代。其具體的垃圾回收算法在後面會介紹。

4、直接內存

直接內存不是Java虛擬機規範中定義的內存區域,但是這部分內存也被頻繁地使用,而且也可能導致OutOfMemoryError異常出現。在JDK1.4中新加入了NIO(New Input/Output)類,引入了一種基於通道(Channel)與緩衝區(Buffer)的I/O方式,它可以使用Native函數庫直接分配堆外內存,然後通過一個存儲在Java堆裏面的DirectByteBuffer對象作爲這塊內存的引用 進行操作。這樣能在一些場景中顯著提高性能,因爲避免了在Java堆和Native堆中來回複製數據。

JAVA的NIO允許java程序使用直接內存,直接內存是在JAVA堆外的,直接向系統申請的內存空間。通常訪問直接內存的速度要優於JAVA堆,直接內存適用於頻繁讀寫的場景,直接內存在JAVA堆外,因此它的大小不會直接受限於Xmx指定的最大堆大小的限制,但是JAVA堆和直接內存依然受限於系統的最大內存。

5、垃圾回收系統

垃圾回收機制是由垃圾收集器Garbage Collection GC來實現的,GC是後臺的守護進程。它的特別之處是它是一個低優先級進程,但是可以根據內存的使用情況動態的調整他的優先級。因此,它是在內存中低到一定限度時纔會自動運行,從而實現對內存的回收。這就是垃圾回收的時間不確定的原因。

6、java棧

java棧是一塊線程私有的內存空間。如果說,java堆和程序數據密切相關,那麼java棧就是和線程執行密切相關。線程執行的基本行爲是函數調用,每次函數調用的數據都是通過java棧傳遞的。

java棧與數據結構上的棧有着類似的含義,它是一塊先進後出的數據結構,只支持出棧和進棧兩種操作,在java棧中保存的主要內容爲棧幀。每一次函數調用,都會有一個對應的棧幀被壓入java棧,每一個函數調用結束,都會有一個棧幀被彈出java棧。如下圖:棧幀和函數調用。函數1對應棧幀1,函數2對應棧幀2,依次類推。函數1中調用函數2,函數2中調用函數3,函數3調用函數4.當函數1被調用時,棧幀1入棧,當函數2調用時,棧幀2入棧,當函數3被調用時,棧幀3入棧,當函數4被調用時,棧幀4入棧。當前正在執行的函數所對應的幀就是當前幀(位於棧頂),它保存着當前函數的局部變量、中間計算結果等數據。

當函數返回時,棧幀從java棧中被彈出,java方法區有兩種返回函數的方式,一種是正常的函數返回,使用return指令,另一種是拋出異常。不管使用哪種方式,都會導致棧幀被彈出。

在一個棧幀中,至少包含局部變量表、操作數棧和幀數據區幾個部分。

 

7、方法區

和JAVA堆一樣,方法區是一塊所有線程共享的內存區域。方法區與Java堆一樣,是各個線程共享的內存區域,它用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。雖然Java虛擬機規範把方法區描述爲堆的一個邏輯部分,但是它卻有一個別名叫做Non-Heap(非堆),目的是與Java堆區分開來。

對於習慣在HotSpot虛擬機上開發和部署程序的開發者來說,很多人願意把方法區稱爲“永久代”(Permanent Generation),本質上兩者並不等價,僅僅是因爲HotSpot虛擬機的設計團隊選擇把GC分代收集擴展至方法區,或者說使用永久代來實現方法區。對於其他虛擬機(BEA JRockit、IBM J9等)來說是不存在永久代的概念的。即使是HotSpot虛擬機本身,也有放棄永久代並“搬家”至Native Memory來實現方法區的規劃。

Java虛擬機規範對這個區域的限制非常寬鬆,除了和Java堆一樣不需要連續的內存和可以選擇固定大小或者可擴展外,還可以選擇不實現垃圾回收。相對而言,垃圾收集行爲在這個區域是比較少出現的,但並非數據進入了方法區就如永久代的名字一樣“永久”存在了。這個區域的內存回收目標主要是針對常量池的回收和對類型的卸載,這部分回收的成績比較難以令人滿意,尤其是類型的卸載,條件相當苛刻,但是這部分區域的回收確實是有必要的。

根據Java虛擬機規範的規定,當方法區無法滿足內存分配需求時,將拋出OutOfMemoryError異常。

8、本地方法棧

本地方法棧與虛擬機棧所發揮的作用是非常相似的,其區別不過是虛擬機棧爲虛擬機執行Java方法(也就是字節碼)服務,而本地方法棧則是爲虛擬機使用到的Native方法服務。虛擬機規範中對本地方法棧中的方法使用的語言、使用方式與數據結構並沒有強制規定,因此具體的虛擬機可以自由實現它。甚至有的虛擬機(譬如 Sun HotSpot虛擬機)直接就把本地方法棧和虛擬機棧合二爲一。與虛擬機棧一樣,本地方法棧也會拋出StackOverFlowError和OutOfMemoryError異常。

9、 PC(Program Counter)寄存器

每個線程啓動的時候,都會創建一個PC(Program Counter,程序計數器)寄存器。PC寄存器裏保存有當前正在執行的JVM指令的地址。 每一個線程都有它自己的PC寄存器,也是該線程啓動時創建的。保存下一條將要執行的指令地址的寄存器是 :PC寄存器。PC寄存器的內容總是指向下一條將被執行指令的地址,這裏的地址可以是一個本地指針,也可以是在方法區中相對應於該方法起始指令的偏移量。

它是一塊較小的內存空間,它的作用可以看做是當前線程所執行的字節碼的行號指示器。在虛擬機的概念模型裏(僅是概念模型,各種虛擬機可能會通過一些更高效的方式去實現),字節碼解釋器工作時就是通過改變這個計數器的值來選取下一條需要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等基礎功能都需要依賴這個計數器來完成。

由於Java虛擬機的多線程是通過線程輪流切換並分配處理器執行時間的方式來實現的,在任何一個確定的時刻,一個處理器(對於多核處理器來說是一個內核)只會執行一條線程中的指令。因此,爲了線程切換後能恢復到正確的執行位置,每條線程都需要有一個獨立的程序計數器,各條線程之間的計數器互不影響,獨立存儲,我們稱這類內存區域爲“線程私有”的內存。

如果線程正在執行的是一個Java方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址;如果正在執行的是Native方法,這個計數器值則爲空(Undefined)。此內存區域是唯一一個在Java虛擬機規範中沒有規定任何OutOfMemoryError情況的區域。

10、執行引擎

執行引擎是Java虛擬機最核心的組成部分之一,“虛擬機”是一個相對於“物理機”的概念,這兩種機器都具有執行代碼的能力。其區別是物理機的執行引擎是直接建立在處理器、硬件、指令集和操作系統層面上的,而虛擬機的執行引擎則是由自己實現的,因此可以自行制定指令集和執行引擎的結構體系,並且能夠執行那些不被硬件直接支持的指令集格式。

在Java虛擬機規範中制定了虛擬機字節碼執行引擎的概念模型,這個概念模型成爲各種虛擬機執行引擎的統一外觀(Facade)。從外觀來看,所有的Java虛擬機執行引擎都是一致的:輸入的是字節碼文件,處理過程是字節碼解析的過程,輸出的是執行結果

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章