1. 程式人生 > >JVM之JVM的體系結構

JVM之JVM的體系結構

一、JDK的組成

JDK:JDK是Java開發工具包,是Sun Microsystems針對Java開發員的產品。JDK中包含JRE(在JDK的安裝目錄下有一個名為jre的目錄,裡面有兩個資料夾bin和lib,在這裡可以認為bin裡的就是jvm,lib中則是jvm工作所需要的類庫,而jvm和 lib和起來就稱為jre)和一堆Java工具(javac/java/jdb等)和Java基礎的類庫(即Java API 包括rt.jar)。

Java Runtime Environment(JRE):是執行基於Java語言編寫的程式所不可缺少的執行環境。也是通過它,Java的開發者才得以將自己開發的程式釋出到使用者手中,讓使用者使用。JRE中包含了Java virtual machine(JVM),runtime class libraries和Java application launcher,這些是執行Java程式的必要元件。

JVM(java virtual machine):就是我們常說的java虛擬機器,它是整個java實現跨平臺的最核心的部分,所有的java程式會首先被編譯為.class的類檔案,這種類檔案可以在虛擬機器上執行。

二、JVM的位置

JVM就是執行在作業系統之上的一個軟體

 三、JVM體系結構

JVM的組成:

  • 類載入子系統 Class loader
  • 執行時資料區 JVM 記憶體模型
  • 執行引擎

四、類載入子系統


 ======================類載入器=======================

 類載入器(ClassLoader):負責載入class檔案(classs檔案在檔案開頭有特定的檔案標識),將class檔案位元組碼內容載入到記憶體中,並將這些內容轉換成方法區中的執行時資料結構;ClassLoader只負責載入class檔案的載入,至於它是否可以執行,則由Execution Engine決定。

 

1、BootStrapLoader(引導類載入器):類載入器也是java類,他們也需要類載入器載入進入記憶體,顯然必須要有第一個不是java類的類載入器,來完成這個工作,這個正是BootStrap。負責載入存放在D:\Program Files (x86)\Java\jdk1.7.0_79\jre\lib下,或被-Xbootclasspath引數指定的路徑中的,並且能被虛擬機器識別的類庫(如rt.jar,所有的java.*開頭的類均被Bootstrap ClassLoader載入);啟動類載入器是無法被Java程式直接引用的;rt.jar 裡面的類的載入器都是BootStrapLoader。

 2、Extension ClassLoader(擴充套件類載入器):該載入器由sun.misc.Launcher$ExtClassLoader實現,它負責載入D:\Program Files (x86)\Java\jdk1.7.0_79\jre\lib\ext目錄中,或者由java.ext.dirs系統變數指定的路徑中的所有類庫(如javax.*開頭的類),開發者可以直接使用擴充套件類載入器。ext 目錄下所有的類的載入器都是Extension ClassLoader

 

 3、Application ClassLoader(應用程式類載入器):該類載入器由sun.misc.Launcher$AppClassLoader來實現,它負責載入使用者類路徑(ClassPath)所指定的類,開發者可以直接使用該類載入器,如果應用程式中沒有自定義過自己的類載入器,一般情況下這個就是程式中預設的類載入器。

====================JVM類載入機制==============

全盤負責:當前執行緒的類載入器負責載入某個Class時,該Class所依賴的和引用的其他Class也將由該類載入器負責載入,除非顯示使用CLassLoader.loadClass()指定類載入器來載入

父類委託:先讓父類載入器試圖載入該類,只有在父類載入器無法載入該類時才嘗試從自己的類路徑中載入該類。所以我們在開發中儘量不要使用與JDK相同的類(例如自定義一個java.lang.System類),因為父類載入器中已經有一份java.lang.System類了,它會直接將該類給程式使用,而你自定義的類壓根就不會被載入。

雙親委派模型:

  雙親委派模型的工作流程是:如果一個類載入器收到了類載入的請求,它首先不會自己去嘗試載入這個類,而是把請求委託給父載入器去完成,依次向上,因此,所有的類載入請求最終都應該被傳遞到頂層的啟動類載入器中,
只有當父載入器在它的搜尋範圍中沒有找到所需的類時,即無法完成該載入,子載入器才會嘗試自己去載入該類。
雙親委派機制:

  • 1、當AppClassLoader載入一個class時,它首先不會自己去嘗試載入這個類,而是把類載入請求委派給父類載入器ExtClassLoader去完成。
  • 2、當ExtClassLoader載入一個class時,它首先也不會自己去嘗試載入這個類,而是把類載入請求委派給BootStrap ClassLoader去完成。
  • 3、如果BootStrap ClassLoader載入失敗(例如在$JAVA_HOME/jre/lib裡未查詢到該class),會使用ExtClassLoader來嘗試載入;
  • 4、若ExtClassLoader也載入失敗,則會使用AppClassLoader來載入,如果AppClassLoader也載入失敗,則會報出異常ClassNotFoundException。

雙親委派模型意義:

  •   -系統類防止記憶體中出現多份同樣的位元組碼
  •   -保證Java程式安全穩定執行

 

 ==================類的載入過程======================

類的載入過程:JVM將javac編譯好的class位元組碼檔案載入到記憶體中,並對該資料進行驗證、解析和初始化、形成JVM可以直接使用的JAVA類,最終回收(解除安裝)的過程。

位元組碼(.class)檔案來源:

  • – 從本地系統中直接載入
  • – 通過網路下載.class檔案
  • – 從zip,jar等歸檔檔案中載入.class檔案
  • – 從專有資料庫中提取.class檔案
  • – 將Java原始檔動態編譯為.class檔案

1、載入:載入階段其實就是JVM通過一個類的全限定名來獲取其定義的二進位制位元組流,並將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構且在Java堆中生成一個代表這個類的java.lang.Class物件,作為對方法區中這些資料的訪問入口。在該階段我們開發人員可以干預,例如:我們可以指定類載入器來載入該位元組陣列或者自定義類載入器來載入。

2、連結:將java類的二進位制程式碼合併到JVM的執行狀態中的過程

  • a、驗證:驗證是為了確保Class檔案的位元組流中包含的資訊符合當前虛擬機器的要求,並且不會危害虛擬機器自身的安全。
  • b、準備:該階段是在方法區中為類變數(static變數)分配記憶體並設定類變數初始值。例如:public static int flag=1;該階段初始化值為0。
  • c、解析:虛擬機器將常量池中的符號引用替換為直接引用的過程。(直接引用就是直接指向目標的指標、相對偏移量或一個間接定位到目標的控制代碼)

3、初始化:初始化為類的靜態變數賦予正確的初始值,JVM負責對類進行初始化,主要對類變數進行初始化。

  • 初始化階段就是執行類構造器<clinit>()的過程,類構造器<clinit>()是由編譯器自動收集類中的所有類變數的賦值動作和靜態語句塊中的語句合併產生的。
  • 當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先初始化其父類。
  • 虛擬機器會保證一個類的<clinit>()方法在多執行緒環境中被正確加鎖和同步。
  • 當訪問一個java 類的靜態域時,只有正真申明這個域的類才會被初始化。

4、使用:程式使用JVM載入的類

5、解除安裝 

  • 執行了System.exit()方法
  • JVM垃圾回收機制觸發回收
  • 程式正常執行結束
  • 程式在執行過程中遇到了異常或錯誤而異常終止
  • 由於作業系統出現錯誤而導致Java虛擬機器程序終止

五、執行時資料區

1、方法區(Method Area):方法區是各個執行緒共享的記憶體區域;方法區用於儲存已被虛擬機器載入的類的模板資訊、常量、靜態變數等;雖然Java虛擬機器規範把方法區描述為堆的一部分,但是他還有個別名叫做Non-heap(非堆),目的應該是與Java堆區分開來;根據Java虛擬機器規範的規定,當方法區無法滿足記憶體分配需求時,將丟擲OutOfMemoryError 異常;相對而言,垃圾收集在這個區域是比較少出現的,但並非資料進入了方法區就如永久代的名字一樣永久存在了。這區域的記憶體回收目標重要是針對常量池的回收和型別的解除安裝。
方法區只是一個規範:

  • 在HotSpot虛擬機器上開發、部署程式我們把方法區稱為“永久代”(Permanent Generation);
  • 他虛擬機器(如 BEA JRockit、IBM J9 等)來說是不存在永久代的概念的。
  • HotSpot虛擬機器在JKD.8中已經沒有方法區的概念了,他使用元空間代替該區域

2、PC暫存器(程式計數器):每個執行緒都有一個程式計數器,是執行緒私有的;就是一個指標,指向方法區中的方法位元組碼(用來儲存指向下一條指令的地址,既將要執行的指令程式碼),由執行引擎讀取下一條指令,是一個非常小的記憶體空間,幾乎可以忽略不記;它是當前執行緒所執行的位元組碼的行號指示器,位元組碼直譯器通過改變這個計數器的值來選取下一條需要執行的位元組碼指令。如果執行的是一個Native方法,那這個計數器是空的;用以完成分支、迴圈、跳轉、異常處理、執行緒恢復等基礎功能。不會發生記憶體溢位OOM錯誤
本地方法棧(Native Stack):與虛擬機器棧基本類似,區別在於虛擬機器棧為虛擬機器執行的java方法服務,而本地方法棧則是為Native方法服務。(棧的空間大小遠遠小於堆)

3、虛擬機器棧(Vm Stack)
  棧也叫棧記憶體,主管 Java 程式的執行,是線上程建立時建立,它的生命期是跟隨執行緒的生命期,執行緒結束棧記憶體也就仔放,對於棧來說不存在垃圾回收問題,只要執行緒結束該棧就釋放,生命週期和執行緒一致,是執行緒私有的。8種基木型別的變數+物件的引用變數+例項方法都是在函式的棧記憶體中分配。

棧的執行原理:棧中的資料都是以棧幀(Stack Frame)的格式存在,棧幀是一個記憶體區塊,是一個數據集,是一個有關方法( Method )和執行期資料的資料集,當一個方法A被呼叫時就產生了一個棧幀 Fl ,並被壓入到棧中, A方法又呼叫了B方法,於是產生棧幀 F2 也被壓入棧,B方法又呼叫了C方法,於是產生棧幀 F3 也被壓入棧,執行完畢後,先彈出 F3 棧幀,再彈出 F2 棧幀,再彈出 Fl 棧幀 以此類推, 遵循“先進後出” / “後進先出”原則。每個方法執行的同時都會建立一個棧幀,用於儲存區域性變量表、運算元、動態連結、方法出口等資訊,每一個方法從呼叫直至執行完畢的過,就對應著一個棧幀在虛擬機器中入棧到出棧的過程。棧的大小和具體JVM的實現有關,通常在 256K~1024K 之間, 1M 左右。

JVM棧的特點:

  • 區域性變量表所需的記憶體空間在編譯期間完成記憶體分配。當進入一個方法時,這個方法需要在幀中分配多大的記憶體空間是完全確定的,在方法執行期間不會改變區域性變量表的大小。
  • 在Java虛擬機器規範中,對這個區域規定了兩種異常狀態:如果執行緒請求的棧的深度大於虛擬機器允許的深度,將丟擲StackOverFlowError異常(棧溢位);如果虛擬機器棧可以動態擴充套件(現在大部分Java虛擬機器都可以動態擴充套件,只不過Java虛擬機器規範中也允許固定長度的java虛擬機器棧),如果擴充套件時無法申請到足夠的記憶體空間,就會丟擲OutOfMemoryError異常(沒有足夠的記憶體)。

4、本地方法棧(Native Method Stacks):與虛擬機器棧所發揮的作用是非常相似的,他們之間的區別不過是虛擬機器棧為虛擬機器執行Java方法(也就是位元組碼)服務,而本地方法棧則為虛擬機器使用到的本地Native方法服務‘;在虛擬機器規範中對本地方法棧中的使用方法、語言、資料結構並沒有強制規定,因此具體的虛擬機器可以自由實現它。甚至有的虛擬機器(例如Sun HotSpot虛擬機器)直接就把本地方法棧和虛擬機器棧合二為一。本地方法棧也會丟擲StackOverFlowError和OutOfmMemoryError異常。


5、Java堆(Java Heap):是Java虛擬機器管理記憶體中的最大一塊;Java堆是所有執行緒共享的一塊記憶體管理區域。此記憶體區域唯一目的就是存放物件的例項,幾乎所有物件例項都在堆中分配記憶體。這一點在Java虛擬機器規範中的描述是:所有物件例項以及陣列都要在堆上分配,但是隨著JIT編譯器的發展與逃逸技術逐漸成熟,棧上分配、標量替換優化技術將會導致一些微妙的變化發生,所有的物件都分配在堆上也不是變的那麼“絕對”了。詳解請學習我的:JVM之堆的體系結構