JVM快速掃盲篇

小懶程式設計日記發表於2021-08-23

JVM虛擬機器基礎

JVM虛擬機器結構

vm的整體結構大致如下:

  1. 類載入器:類載入器用來載入Java類到JVM虛擬機器中,原始碼程式.java檔案在經過編譯器編譯之後就被轉換成位元組程式碼.class檔案,類載入器負責讀取位元組程式碼,並轉換成java.lang.Class類的一個例項。
  2. 執行時資料區
    • 後設資料區:JDK1.8開始的說法,之前稱為方法區Method-Area,儲存已被虛擬機器載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼等資料。
    • 堆區:所有執行緒共享的一塊記憶體區域,虛擬機器啟動時被建立用來存放物件例項。
    • JVM棧:可以參考瞭解棧的資料結構,存放Java方法執行的記憶體模型,在Java開發中,一個功能實現需要多個子程式方法配合,程式執行時跳往子程式前,會將下個指令的地址存到堆疊中,直到子程式執行完後再將地址取出,退回到原來的程式中。
    • 本地方法棧:本地方法棧和虛擬機器棧的功能類似,為JVM呼叫native方法時服務。
    • 程式計數器:相對較小的一塊記憶體空間,作用可以理解是當前執行緒所執行的位元組碼的行號指示器。
  3. 執行引擎:Java虛擬機器最核心的組成部分,輸入的是位元組碼,處理過程是位元組碼解析,輸出執行結果

生命週期

這裡說的JVM生命週期,指JVM執行Java程式時的週期:

  • 啟動初始化:啟動時通過引導類載入器建立初始類完成;
  • 程式執行:從main方法開始,執行Java程式,直到程式執行完結束;
  • 虛擬機器退出:程式正常執行結束,或者發生異常、錯誤等而造成終止,也可以呼叫exit退出方法;

HotSpot虛擬機器

HotSpot是Java體系下使用最多的虛擬機器,它結合了最新的記憶體模型,垃圾收集器和自適應優化器,為使用許多先進技術的Java應用程式提供了最佳效能。

JVM類載入機制

類載入簡介

類的載入機制是指把編譯後的.class類檔案的二進位制資料讀取到記憶體中,併為之建立一個java.lang.Class物件,用來封裝類在後設資料空間的資料結構。

類在JVM中的生命週期為:載入,連線,初始化,使用,解除安裝。不過這裡只重點描述載入,連線,初始化這三個過程

載入過程

基於一張圖看類載入子系統的細節流程:

1.載入階段

過程描述:載入階段需要完成以下三個過程:

  • 通過類的全限定名來獲取其定義的二進位制位元組流;
  • 將位元組流所代表的靜態儲存結構轉化為雲資料空間的執行時資料結構;
  • 在堆Heap中生成一個代表這個類的java.lang.Class物件,作為對後設資料空間中這些資料的訪問入口;

類載入器

  • 引導類載入器:Bootstrap-ClassLoader基於C/C++實現,負責載入Java的核心類庫JAVA_HOME\jre\lib\rt.jar,該載入器不繼承自ClassLoader抽象類,並且只載入包名為java、javax、sun等開頭類,一次保證對核心原始碼的保護。
  • 擴充套件類載入器:Extension-ClassLoader,基於Java語言,由sun.misc.Launcher$ExtClassLoader實現,派生於ClassLoader抽象類,從java.ext.dirs系統變數指定的路徑中的載入類庫,或者JDK安裝目錄jre\lib\ext目錄下載入。
  • 系統類載入器:Application-ClassLoader,基於Java語言,由sun.misc.Launcher$ExtClassLoader實現,它負責載入環境變數ClassPath指定的類庫,如果在應用程式中沒有自定義類載入器,一般情況下作為程式中預設的類載入器。

2.連線階段:

驗證:目的在於確保Class檔案的位元組流中包含的資訊符合當前虛擬機器的要求,保證載入類的正確性,不會危害虛擬機器自身的安全,主要包括四種檢驗動作:

  • 檔案格式驗證:驗證位元組流是否符合Class檔案格式的規範;
  • 後設資料驗證:確保其描述的資訊符合Java語言規範的要求;
  • 位元組碼驗證:確定程式語義是符合邏輯的;
  • 符號引用驗證:確保解析動作能正確執行。

準備:為類的靜態變數分配記憶體,並初始化為預設值,這時候進行記憶體分配的僅包括類變數(static)修飾,不包括(final-static)修飾的,這裡也不會為例項變數分配初始化,例項變數會隨著物件一塊分配到Java堆中。

解析:將常量池中的符號引用轉換為直接引用的過程,直接引用就是直接指向目標的指標、相對偏移量或一個間接定位到目標的控制程式碼。解析主要針對類或介面、欄位、類方法、介面方法、方法型別等,解析的動作實際是會隨著JVM在執行完初始化之後再執行的。

3.初始化階段

執行類構造器clinit()方法的過程,該方法不需要自定義,是javac編譯器自動收集類中的所有類變數的賦值動作和靜態程式碼塊中的語句合併而來,Jvm要保證clinit()方法在多執行緒訪問下的安全性。

機制策略

1.雙親委派模式

類載入器收到了類載入的請求時,不會自己先去嘗試載入這個類,而是把請求委託給父載入器去執行;

如果父載入器還存在父類載入器,則依次向上委託,因此類載入請求最終都應該被傳遞到頂層的啟動類載入器中;

如果父類載入器可以完成類載入請求,就直接成功返回,只有當父載入器在無法完成該載入,子載入器才會嘗試自己去載入該類;

2.沙箱安全機制

假設自定義一個類名為String且所在包為java.lang,在使用引導類載入器載入時會先載入JDK中的String類,因為這個類本來是屬於jdk的,後面再次出現String類就會報錯,以此保證原始碼不被惡意篡改,這就是沙箱安全機制

JVM執行時資料區

  1. 記憶體結構
    記憶體是計算機的重要部件之一,它是外存與CPU進行溝通的橋樑,計算機中所有程式的執行都在記憶體中進行,記憶體效能的強弱影響計算機整體發揮的水平。JVM的記憶體結構規定Java程式在執行時記憶體的申請、劃分、使用、回收的管理策略,通說來說JVM的記憶體管理指執行時資料區這一大塊的管理。

  2. 執行緒執行
    JVM中一個應用是可以有多個執行緒並行執行,執行緒被一對一對映為服務所在作業系統執行緒,排程在可用的CPU上執行,啟動時會建立一個作業系統執行緒;當該執行緒終止時,這個作業系統執行緒也會被回收。

在虛擬機器啟動執行時,會建立多個執行緒,資料區中有的模組是執行緒共享的,有的是執行緒私有的:

執行緒共享:後設資料區、堆Heap;

執行緒私有:虛擬機器棧、本地方法棧、程式計數器;

單個CPU在特定時刻只能執行一個執行緒,所以多執行緒通過幾塊空間的使用,然後不斷的爭搶CPU的執行時間段。

後設資料空間

基本描述:方法元空間(方法區)在JVM啟動的時候被建立,是被各個執行緒共享的記憶體空間,用於存放類和方法的後設資料以及常量池,比如Class和Method。在實際的開發中,經常因為載入的類太多,進而導致記憶體溢位問題,這樣可以對元空間的大小進行擴充套件。

與堆的關係:

元空間存放載入的類資訊,當類被例項化時,堆中儲存例項化的物件資訊,並且通過物件型別資料的指標找到類。

堆空間

基本描述:JVM啟動時建立堆區,是記憶體管理的核心區,通常情況下也是最大的記憶體空間,是被所有執行緒共享的,幾乎所有的物件例項都要在堆中分配記憶體,所以這裡也是垃圾回收的重點空間。

堆疊關係

棧是JVM執行時的單位,堆是儲存單位,當棧中方法結束,相關物件失去所有引用後,不會馬上被移除堆空間,要等到垃圾收集器執行的時候。

虛擬機器棧

虛擬機器棧(Java棧)在每個執行緒建立時都會生成一個虛擬機器棧,棧的內部是一個個棧幀單元,對應Java方法的呼叫,其生命週期和執行緒週期保持一致。用來儲存方法的區域性遍歷,部分執行結果,方法的呼叫和返回。

棧幀是方法執行的資料集,維持執行過程中的各種資料資訊,執行的方法依次入棧,棧頂存放當前要執行的方法,執行結束後出棧,對於棧沒有垃圾回收問題。

程式計數器

基本描述:JVM中程式計數暫存器用來儲存下一條將要執行指令的地址,執行引擎獲取到指令後進行執行,是執行緒私有的。它可以看作是當前執行緒所執行的位元組碼的行號指示器。

前後關係:執行緒在獲取CPU的時間段內執行程式碼,但是執行緒隨時可能沒有執行完就被掛起,等到執行緒A再次獲取CPU執行時,CPU 得知道執行到執行緒A的哪一個指令,程式計數器會儲存該動作。

本地方法棧

本地方法棧與虛擬機器棧所起到的作用是類似的,虛擬機器棧為虛擬機器執行Java方法,本地方法棧管理虛擬機器使用到的 本地方法,在虛擬機器規範中對本地方法棧中方法使用的語言、使用方式與資料結構並沒有強制規定,因此具體的虛擬機器可以自由實現它。HotSpot虛擬機器直接就把本地方法棧和虛擬機器棧合二為一。

執行引擎和垃圾回收

執行引擎

應用程式經過編譯,轉換為位元組碼檔案,位元組碼載入到記憶體空間並不能直接在作業系統上執行,執行引擎作為Java虛擬機器核心的組成部分,作用就是將位元組碼指令解釋/編譯為對應系統平臺上的本地機器指令。

直譯器:虛擬機器啟動時會根據預定義對位元組碼採用逐行解釋的方式執行,將每條位元組碼檔案中的內容解釋為對應系統平臺的本地機器指令執行;

JIT編譯器:虛擬機器將原始碼編譯成本地機器平臺相關的機器語言,並且尋找熱點高頻執行的程式碼將其放入元空間中,即元空間中存放的JIT快取程式碼;

垃圾回收:對於沒有任何引用的物件標記為垃圾,會被回收釋放記憶體空間。

垃圾物件標記

1. 引用計數法

每個物件儲存一個整型引用計數器,用來記錄物件被引用的次數,當該物件被一個物件引用時,計數器加1,當失去一個引用時,計數器減1;引用計數演算法就是通過判斷物件的引用數量來決定物件是否可以被當做垃圾物件回收掉。

雖然引用計數法效率高,但是當兩個物件互相引用時會導致這兩個物件一直不會被回收,這是一個致命的缺陷。所以JVM並沒有採用該標記演算法。

2. 垃圾物件標記

可達性分析演算法是基於物件到根物件的引用鏈是否可達來判斷物件是否可以被回收;

執行程式把所有的引用關係鏈看作一張圖,通過GC-Roots根物件物件集合作為起始點,從每個根節點向下不斷搜尋被根物件集合所連線的物件是否可達,搜尋路徑稱為引用鏈(Reference-Chain),如果物件到GC-Roots沒有任何引用鏈存在,則說明此物件是不可用的,虛擬機器棧中引用的物件如下:

  • 元空間中類靜態屬性引用的物件;
  • 元空間中常量引用的物件;
  • 本地方法棧中Native方法引用的物件;

相對於引用計數法演算法,可達性分析演算法則避免了迴圈引用導致的問題,同樣具備執行高效的特點,也是JVM採用的標記演算法。

垃圾回收機制

1.標記清除演算法

標記-清除演算法分為標記和清除兩個階段:

  • 標記階段:從根物件集合進行掃描,對存活的物件物件標記;
  • 清除階段:再次掃描發現未被標記的物件並進行回收

該演算法效率不高,進行垃圾回收需要暫停應用程式,同時會產生大量記憶體碎片,後續程式執行過程中分配記憶體佔用較大的物件時,會有連續記憶體不夠情況,容易觸發再一次垃圾收集動作。

2.標記整理演算法

標記整理演算法的標記過程類似標記清除演算法

  • 第一階段:標記出垃圾物件;
  • 第二階段:讓所有存活的物件都向記憶體區一端移動;
  • 第三階段:直接清理掉邊界端以外的記憶體,類似於磁碟整理的過程;

該垃圾回收演算法效率不高,物件移動過程需要暫停應用程式,適用於物件存活率高的場景(老年代)。

3.複製演算法

複製演算法將記憶體按容量劃分為大小相等的兩塊,每次只使用其中的一塊,當使用的這塊的記憶體用完,就將還存活著的物件複製到另外一塊空閒記憶體上,然後使用過的記憶體空間一次清理。

該演算法實現簡單,執行效率高,但是記憶體空間嚴重浪費,適用於物件存活率低的場景,比如新生代。

4.分代收集演算法

當前市場上幾乎所有的虛擬機器都採用該回收演算法,分代收集演算法根據年輕代和老年代的各自特點採用不同的演算法機制,不同記憶體區域中物件生命週期也不同,因此對堆記憶體不同區域採用不同的回收策略可以提高垃圾回收執行效率。通常情況新生代物件存活率低,回收頻繁,就採用複製演算法;老年代存物件生命週期長,活率高,就用標記清除演算法或者標記整理演算法。

Java堆記憶體一般可以分為新生代、老年代和永久代三個模組,如下圖所示:

新生代:通常情況下,新建立的物件例項首先都是放在新生代空間中,所以追求快速的回收掉垃圾物件,一般情況下,新生代記憶體按照8:1:1的比例分為一個eden區和兩個survivor(survivor0,survivor1)區,物件例項大部分在Eden區中生成;

  垃圾回收時先把eden區存活物件複製到S0區,然後清空eden區,當S0區也滿時,再將eden區和S0區存活物件複製到S1區,然後清空eden和S0區,之後交換S0區和S1區的角色,當S1區無法存放eden區和S0區的存活物件時,就將存活物件直接存移到老年代區,當老年代區也滿了,觸發一次FullGC,即新生代、老年代都進行回收。

老年代:老年代區存放一些生命週期較長的物件,物件例項在新生代中經歷了多次垃圾回收仍然存活的物件,會被移動到老年代區中。

相關文章