JVM 執行機制及其原理
JVM
JVM是Java Virtual Machine(Java虛擬機器)的縮寫,是一個虛構出來的計算機,是通過在實際的計算機上模擬模擬各種計算機功能來實現的。Java虛擬機器主要由位元組碼指令集、暫存器、棧、垃圾回收堆和儲存方法域等構成。 JVM遮蔽了與具體作業系統平臺相關的資訊,使Java程式只需生成在Java虛擬機器上執行的目的碼(位元組碼),就可以在多種平臺上不加修改地執行。JVM在執行位元組碼時,實際上最終還是把位元組碼解釋成具體平臺上的機器指令執行。
JVM宣告週期
JVM伴隨Java程式的開始而開始,程式的結束而停止。一個Java程式會開啟一個JVM程序,一臺計算機上可以執行多個程式,也就可以執行多個JVM程序。
JVM將執行緒分為兩種:守護執行緒和普通執行緒。守護執行緒是JVM自己使用的執行緒,比如垃圾回收(GC)就是一個守護執行緒。普通執行緒一般是Java程式的執行緒,只要JVM中有普通執行緒在執行,那麼JVM就不會停止。
JVM記憶體模型組成
JVM記憶體模型主要由堆記憶體、方法區、程式計數器、虛擬機器棧和本地方法棧組成,其組成的結構如下圖所示。
其中,堆和方法區是所有執行緒共有的,而虛擬機器棧,本地方法棧和程式計數器則是執行緒私有的。
堆記憶體
堆記憶體是所有執行緒共有的,可以分為兩個部分:年輕代和老年代。下圖中的Perm代表的是永久代,但是注意永久代並不屬於堆記憶體中的一部分,同時jdk1.8之後永久代也將被移除。
堆記憶體是我們在生產環境中進行記憶體效能調優中的一個重要的內容,而記憶體回收的一些機制和演算法也是常見的考點,大家可以訪問下面的連結:Java效能優化之JVM GC
方法區
方法區與Java堆一樣,是各個執行緒共享的區域,它用於儲存已被虛擬機器載入的類資訊,常量,靜態變數,即時編譯(JIT)後的程式碼等資料。
由於程式中所有的執行緒共享一個方法區,所以訪問方法區的資訊必須確保執行緒是安全的。如果有兩個執行緒同時去載入一個類,那麼只能有一個執行緒被允許去載入這個類,另一個必須等待。
在程式執行時,方法區的大小是可以改變的,程式在執行時可以擴充套件。同時,方法區裡面的物件也可以被垃圾回收,但條件非常嚴苛,必須在該類沒有任何引用的情況下才能被GC回收。
程式計數器
在JVM的概念模型裡,位元組碼直譯器工作時就是通過改變這個計數器的值來選取下一條需要執行的位元組碼指令。分支、迴圈、跳轉、異常處理、執行緒恢復等基礎功能都需要依賴這個計數器來完成。
JVM的多執行緒是通過執行緒輪流切換並分配處理器執行時間的方式來實現的,為了各條執行緒之間的切換後計數器能恢復到正確的執行位置,所以每條執行緒都會有一個獨立的程式計數器。
當執行緒正在執行一個Java方法,程式計數器記錄的是正在執行的JVM位元組碼指令的地址;如果正在執行的是一個Natvie(本地方法),那麼這個計數器的值則為空(Underfined)。
程式計數器佔用的記憶體空間很少,也是唯一一個在JVM規範中沒有規定任何OutOfMemoryError(記憶體不足錯誤)的區域。
Java虛擬機器棧
與程式計數器一樣,Java虛擬機器棧也是執行緒私有的,用通俗的話將它就是我們常常聽說到堆疊中的那個“棧記憶體”。虛擬機器棧描述的是Java方法執行的記憶體模型:每個方法在執行的同時都會建立一個棧幀(Stack Frame)用於儲存區域性變量表(區域性變量表需要的記憶體在編譯期間就確定了所以在方法執行期間不會改變大小),運算元棧,動態連結,方法出口等資訊。每一個方法從呼叫至出棧的過程,就對應著棧幀在虛擬機器中從入棧到出棧的過程。
本地方法棧
棧作為一種線性的管道結構,遵循先進後出的原則。主要用於儲存本地方法的區域性變量表,本地方法的運算元棧等資訊。當棧內的資料在超出其作用域後,會被自動釋放掉。
本地方法棧是在程式呼叫或JVM呼叫本地方法介面(Native)時候啟用。
Java類載入機制
什麼是類載入
眾所周知,JVM載入的是.class檔案。其實,類的載入指的是將類的.class檔案中的二進位制資料讀入到記憶體中,將其放在執行時資料區的方法區內,然後在堆區建立一個java.lang.Class物件,用來封裝類在方法區內的資料結構。類的載入的最終產品是位於堆區中的Class物件,Class物件封裝了類在方法區內的資料結構,並且向Java程式設計師提供了訪問方法區內的資料結構的介面。
同時,JVM規範允許類載入器在預料某個類將要被使用時就預先載入它,如果在預先載入的過程中遇到了.class檔案缺失或存在錯誤,類載入器會在程式首次主動使用該類時會生成錯誤報告(LinkageError錯誤),如果這個類一直沒有被程式主動使用,那麼類載入器就不會報告錯誤。
類的載入過程
JVM將類的載入分為3個步驟:
1、裝載(Load)
2、連結(Link)
3、初始化(Initialize)
而連結(Link)又分3個步驟:
1,驗證
2,準備
3,解析
可以使用下面的影象表示。
1,裝載
載入是類載入過程的第一個階段,在載入階段,虛擬機器需要完成以下三件事情:
1、通過一個類的全限定名來獲取其定義的二進位制位元組流。
2、將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構。
3、在Java堆中生成一個代表這個類的java.lang.Class物件,作為對方法區中這些資料的訪問入口。
相對於類載入的其他階段而言,載入階段是獲取類的二進位制位元組流的最佳階段,因為開發人員既可以使用系統提供的類載入器來完成載入,也可以自定義自己的類載入器來完成載入。
載入階段完成後,虛擬機器外部的 二進位制位元組流就按照虛擬機器所需的格式儲存在方法區之中,而且在Java堆中也建立一個java.lang.Class類的物件,這樣便可以通過該物件訪問方法區中的這些資料。
2,連結
連結階段分為三個步驟:驗證、準備和解析。
驗證:確保被載入的類的正確性;
準備:為類的靜態變數分配記憶體,並將其初始化為預設值;
解析:把類中的符號引用轉換為直接引用。
驗證
驗證是連結第一步,這一階段的目的是為了確保Class檔案的位元組流中包含的資訊符合當前虛擬機器的要求,並且不會危害虛擬機器自身的安全。驗證階段大致會完成4個階段的檢驗動作:
檔案格式驗證:驗證位元組流是否符合Class檔案格式的規範;例如:是否以0xCAFEBABE開頭、主次版本號是否在當前虛擬機器的處理範圍之內、常量池中的常量是否有不被支援的型別。
元資料驗證:對位元組碼描述的資訊進行語義分析(注意:對比javac編譯階段的語義分析),以保證其描述的資訊符合Java語言規範的要求;例如:這個類是否有父類,除了java.lang.Object之外。
位元組碼驗證:通過資料流和控制流分析,確定程式語義是合法的、符合邏輯的。
符號引用驗證:確保解析動作能正確執行。
驗證階段是非常重要的,但不是必須的,它對程式執行期沒有影響,如果不需要驗證,那麼可以考慮採用-Xverifynone引數來關閉大部分的類驗證措施,以縮短虛擬機器類載入的時間。
準備
準備階段是正式為類變數分配記憶體並設定類變數初始值的階段,這些記憶體都將在方法區中分配。對於該階段有以下幾點需要注意:
1、這時候進行記憶體分配的僅包括類變數(static),而不包括例項變數,例項變數會在物件例項化時隨著物件一塊分配在Java堆中。
2、這裡所設定的初始值通常情況下是資料型別預設的零值(如0、0L、null、false等),而不是被在Java程式碼中被顯式地賦予的值。
解析
解析階段是虛擬機器將常量池內的符號引用替換為直接引用的過程,解析動作主要針對類或介面、欄位、類方法、介面方法、方法型別、方法控制代碼和呼叫限定符7類符號引用進行。符號引用就是一組符號來描述目標,可以是任何字面量。
直接引用就是直接指向目標的指標、相對偏移量或一個間接定位到目標的控制代碼。
3,初始化
初始化,為類的靜態變數賦予正確的初始值,JVM負責對類進行初始化,主要對類變數進行初始化。在Java中對類變數進行初始值設定有兩種方式:
1,宣告類變數是指定初始值。
2,使用靜態程式碼塊為類變數指定初始值。
類的初始化步驟或JVM初始化的步驟如下:
1)如果這個類還沒有被載入和連結,那先進行載入和連結 ;
2)假如這個類存在直接父類,並且這個類還沒有被初始化(注意:在一個類載入器中,類只能初始化一次),那就初始化直接的父類(不適用於介面);
3 ) 假如類中存在初始化語句(如static變數和static塊),那就依次執行這些初始化語句。
Class載入器
JVM的類載入是通過ClassLoader及其子類來完成的,類的層次關係和載入順序可以由下圖來描述。
Bootstrap ClassLoader
負責載入$JAVA_HOME中 jre/lib/rt.jar 裡所有的class或Xbootclassoath選項指定的jar包。由C++實現,不是ClassLoader子類。
Extension ClassLoader
負責載入java平臺中擴充套件功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar 或 -Djava.ext.dirs指定目錄下的jar包。
App ClassLoader
負責載入classpath中指定的jar包及 Djava.class.path 所指定目錄下的類和jar包。
Custom ClassLoader
通過java.lang.ClassLoader的子類自定義載入class,屬於應用程式根據自身需要自定義的ClassLoader,如tomcat、jboss都會根據j2ee規範自行實現ClassLoader。