溪源的Java筆記—JVM

溪源的奇思妙想發表於2020-11-05

前言

作為一個Java開發,JVM是我們必須要了解的,我們只有建立在瞭解它的基本運作原理,才可能設計出一個最合理的程式碼方案,在此之前我們已經瞭解了集合中的Map介面,接下來溪源我將帶領大家瞭解一下JVM,希望對大家略有幫助。

集合之Map介面可參考我的部落格:溪源的Java筆記—集合之Map介面

正文

JVM

JVM為了達到給所有硬體提供一致的虛擬平臺的目的,犧牲了一些與硬體相關的特性。

  • Java原始檔可以通過編譯器轉化成位元組碼檔案(.class檔案),這些位元組碼檔案又可以被JVM轉化成機器碼。
  • JVM是執行在作業系統之上的,它與硬體沒有直接互動。Java的執行緒和原生作業系統執行緒有對映關係,Java可以通過對應的作業系統執行緒來獲取計算機資源。

在這裡插入圖片描述

執行緒共享的資料區:

  • 方法區:儲存程式執行時長期存活的物件,比如類的後設資料( 後設資料生成相應的java檔案)、方法、屬性等 (常量在JDK1.8移至JVM堆中)
  • JVM堆:存放物件、陣列、常量等,垃圾收集器就是收集這些物件,然後根據GC演算法回收。

知識點:

  1. 在JDK1.8中廢棄了永久代區域,方法區被放在了元空間,這種設計可以避免永久代OOM(記憶體溢位)導致觸發GC。元空間並不在虛擬機器中,而是使用本地記憶體。因此,預設情況下,元空間的大小僅受本地記憶體限制。預設在20M左右,放在元空間的永久代滿了即,達到MetaspaceSize的閾值,同樣也會觸發FullGC.
  2. 常量在JDK1.8由方法區移至JVM堆中。
  3. 類的後設資料即類的描述資料,虛擬機器通過後設資料可以生成對應的物件。

執行緒隔離的資料區:

  • 本地方法棧Natitve 方法
  • 虛擬機器棧(JVM方法棧):區域性變數區、運算元棧、動態連線(方法呼叫過程的動態連線)、方法返回地址(可以理解為一個類方法的執行區域)。
  • 程式計數暫存器(PC暫存器): 用於記錄正在執行的虛擬機器位元組序列的行指示器。

知識點:

  1. 每一個執行緒都會生成PC暫存器和虛擬機器棧。
  2. 棧的棧由系統自動分配,而堆,需要程式設計師自己申請並指明大小

JVM堆的組成

在這裡插入圖片描述

1/3的新生代:(由 Minor GC進行清理,採用複製演算法)

  • GC開始前,物件只會存在於Eden區和名為“From”Survivor區,Survivor“To”是空的。
  • GC中Eden區中所有存活的物件都會被複制到“To”,而在“From”區中,仍存活的物件會根據他們的年齡值來決定去向。
  • GC結束後Eden區和From區已經被清空,這個“To”“From”互換角色,此時Survivor“To”是空的,而“From”保留上次GC存活物件
    整個流程可以概括 複製-清空-互換。

2/3的老年代:( 由Major GC進行清理,採用標記清除演算法 )

  • 用於存放新生代中經過多次垃圾回收仍然存活的物件
  • 新生代分配不了記憶體的大物件會直接進入老年代

知識點:

  1. 新生代滿了,會放至到空閒的Survivor區,只有所有的Survivor區滿了才會放到老年代。
  2. Survivor區的作用是減少被送到老年代的物件,進而減少Full GC的發生,Survivor的預篩選保證,只有經歷16次Minor GC還能在新生代中存活的物件,才會被送到老年代。

JVM回收機制

JVM確認垃圾回收物件的方式

  • 引用計數法:當引用數為0時,物件死亡
  • 根搜尋演算法:根物件(GC ROOTS)到某物件不可達時,物件死亡。

GC ROOTS的物件包括:

  1. 虛擬機器棧中的引用物件
  2. 本地方法棧的引用物件
  3. 方法區中靜態屬性引用的物件
  4. 方法區中靜態常量池中引用的物件

JVM垃圾回收演算法

  • 標記-清除演算法:效率偏低
  • 複製演算法:效率高,但是佔用2倍記憶體 (預留一塊記憶體 將還存活的物件放到該記憶體)
  • 標記-整理演算法:效率偏低(是對標記-清除演算法的改進,讓存活的物件向一段移動)
  • 分代收集演算法:把Java堆分為新生代和老年代,根據年代將特徵選擇上述演算法。新生代通常採用複製演算法,老年代採用標記-清除演算法或者標記-整理演算法。

常見的GC方式

  • Minor GC:是清理新生代
  • MajorGC:是指清理老年代
  • Full GC:清理新生代和老年代GC,通常來時Full GCMinor GC至少慢10倍。

觸發Full GC的情況 :

  • 老年代滿了
  • 永久代滿了
  • System.gc()
  • 採用CMS收集器發生"Concurrent Mode Failure”異常時

知識點:

  1. 立即回收還是延遲迴收是取決於JVM的,所以即使有GC機制還是可能存在無用但可達的物件沒有即時被回收而導致記憶體洩漏。

垃圾收集器

垃圾收集器,又稱為垃圾回收器,是垃圾回收演算法(標記-清除演算法、複製演算法、標記-整理演算法)的具體實現,不同版本的JVM所提供的垃圾收集器可能會有很在差別,本文主要介紹HotSpot虛擬機器中的垃圾收集器。

選擇垃圾回收器考慮的因素:

  • 應用程式的場景
  • 硬體的制約
  • 吞吐量的需求

選擇垃圾回收器的標準:

  • 發生gc的停頓時間
  • 產生空間碎片的大小,會間接影響併發量

序列、並行和併發的區別:

  • 序列: 只會使用一個CPU或一條收集執行緒去完成垃圾收集工作 ,並且在進行垃圾收集時,必須暫停其他所有的工作執行緒。
  • 並行:指多條垃圾收集執行緒並行工作,但此時使用者執行緒仍然處於等待狀態;
  • 併發:指使用者執行緒與垃圾收集執行緒同時執行(但不一定是並行的,可能會交替執行);使用者程式在繼續執行,而垃圾收集程式執行緒執行於另一個CPU上;
    在這裡插入圖片描述

常見的收集器(7種)

  • 新生代收集器SerialParNewParallel Scavenge(複製演算法)
  • 老年代收集器Serial OldParallel OldCMS; (1、2 採用標記整理演算法 3採用標記清除演算法)
  • 整堆收集器G1;(標記整理,分割槽)

ParNew收集器
ParNew的特點:

  • 用於新生代收集器
  • 採用複製演算法
  • 並行,採用多執行緒收集,垃圾手機時會造成“Stop The World”

應用場景:在多核的情況下和CMS搭配使用,以滿足使用者互動頻繁實現低延遲的場景(最常見就是遊戲)

Parallel Scavenge收集器
Parallel Scavenge的特點:

  • 用於新生代收集器
  • 採用複製演算法
  • 並行,採用多執行緒收集,垃圾收集時會造成 “Stop The World”
  • Parallel Scavenge 沒有采用傳統的GC程式碼框架,它相對於ParNew的特點在於: JVM會根據當前系統執行情況收集效能監控資訊,動態調整這些引數,以提供最合適的停頓時間或最大的吞吐量,這種調節方式稱為GC自適應的調節策略

應用的場景:在多核的情況下和Parallel Old搭配使用,以滿足高併發的場景(預設的搭配,最常就是web應用)

Parallel Old收集器
Parallel Old的特點:

  • 用於老年代收集器
  • 採用”標記—整理"演算法
  • 並行,採用多執行緒收集,垃圾收集時會造成 “Stop The World”
    應用的場景:在多核的情況下和Parallel Scavenge搭配使用,以滿足高併發的場景(預設的搭配,最常就是web應用)

CMS收集器
CMS的特點:

  • 基於"標記-清除”演算法,不進行壓縮,以產生記憶體碎片,換取更短回收停頓時間
  • 併發收集、低停頓
  • 需要更多記憶體
    應用的場景:在多核的情況下和Parallel Scavenge搭配使用,以滿足使用者互動頻繁實現低延遲的場景(最常見就是遊戲)

CMS運作的過程:

  1. 初始標記:僅標記GC Roots能直接關聯到的物件,速度很快,但會造成 “Stop The World”
  2. 併發標記:應用程式執行的同時,對初始標記的物件中存活的物件進行標記,並不能保證可以標記出所有的存活物件;
  3. 重新標記:為了修正併發標記期間因使用者程式繼續運作而導致標記變動的那一部分物件的標記記錄,會造成 “Stop The World”,停頓時間比初始標記稍長,但遠比並發標記短;
  4. 併發清除:應用程式執行的同時,回收所有的垃圾物件

CMS的缺陷:

  • CPU資源非常敏感:當CPU核數低於4時,效能會比較差
  • 在併發清除時無法處理應用程式新產生的垃圾物件(即浮動垃圾),所以需要此時需要預留一定的記憶體空間,當預留的空間也無法填滿時會出現"Concurrent Mode Failure”失敗,JVM會臨時啟用Serail Old收集器,而導致另一次Full GC的產生;
  • 由於採用"標記-清除”演算法,會產生大量的記憶體碎片。

G1收集器
G1的特點:

  • 並行與併發:既可以並行來縮短"Stop The World”停頓時間,也可以併發讓垃圾收集與使用者程式同時進行,減少停頓時間;
  • 分代收集:將整個堆劃分為多個大小相等的獨立區域 (Region),能夠採用不同方式處理不同時期的物件;
  • 結合多種垃圾收集演算法,空間整合,不產生碎片: 從整體看,是基於標記-整理演算法;從區域性(兩個Region間)看,是基於複製演算法;
  • 可預測的停頓:低停頓的同時實現高吞吐量

應用場景:具有比較大的記憶體空間、物件相對比較大的場景。

G1的運作流程:

  1. 初始標記:僅標記GC Roots能直接關聯到的物件,並且修改TAMSNext Top at Mark Start),讓下一階段併發執行時,使用者程式能在正確可用的Region中建立新物件,速度很快,但會造成 “Stop The World”
  2. 併發標記:應用程式執行的同時,對初始標記的物件中存活的物件進行標記,同時物件的變化記錄線上程的Remembered Set Log,並不能保證可以標記出所有的存活物件;
  3. 最終標記 :為了修正併發標記期間因使用者程式繼續運作而導致標記變動的那一部分物件的標記記錄,這裡把Remembered Set Log合併到Remembered Set中; 會造成 “Stop The World”,停頓時間比初始標記稍長,但遠比並發標記短;
  4. 篩選回收:首先排序各個Region的回收價值和成本,然後根據使用者期望的GC停頓時間來制定回收計劃;,最後按計劃回收一些價值高的Region中垃圾物件;採用複製演算法和並行的方式,降低停頓時間、並增加併發量。

Java四種引用型別

  • 強引用A a=new A() 只要引用a存在,垃圾回收器不會回收。
  • 軟引用SoftReference類似於快取的方式,不影響垃圾回收,可以提升速度,節省記憶體。若物件被回收,此時可以重新new,主要是用來快取伺服器中間計算結果以及不需要實時儲存的使用者行為。通常放在用在對快取比較敏感的應用中。
  • 弱引用WeakReference用於監控物件是否被垃圾回收器回收。
  • 虛引用PhantomReference,每次垃圾回收的時候都會被回收。主要用於判斷物件是否已經從記憶體中刪除。

類載入機制

類載入器的任務就是.class檔案載入到到JVM轉換成 java.lang.class
在這裡插入圖片描述

常見的類載入器

  • 根類載入器:用來載入Java的核心類;
  • 擴充套件類載入器:用來載入jre的擴充套件目錄;
  • 系統載入器:它負責在JVM啟動時載入來自Java命令的-classpath選項、java.class.path系統屬性,或者CLASSPATH換將變數所指定的JAR包和類路徑。

雙親委託模型
雙親委託模型,確保了載入的唯一性,當類收到載入請求時,它首先不會嘗試載入這個類,而是把請求委託給父類載入器執行,每個類都是如此(如果還有父類繼續上交),只有父類載入完或者父類不存在,子類才會進行載入。

類載入過程
裝載:獲取類的二進位制位元組流,將其靜態儲存結構轉化為方法區的執行時資料結構;
連結,可以細分為:

  • 校驗:獲取類的二進位制位元組流,將其靜態儲存結構轉化為方法區的執行時資料結構;
  • 準備:在方法區中對類的static變數分配記憶體並設定類變數資料型別預設的初始值,不包括例項變數,例項變數將會在物件例項化的時候隨著物件一起分配在Java堆中;
  • 解析:將常量池內的符號引用替換為直接引用的過程;

初始化:為類的靜態變數賦予正確的初始值,使得Java程式碼中被顯式地賦予的值。

當我們要對基礎類進行修改時,打破雙親委託模型的方式:

  • 自定義類載入器:繼承ClassLoader類重寫loadClass方法。
  • SPI機制:JDK內建的一種服務提供發現機制:通過載入ClassPathMETA_INF/services,自動載入檔案裡所定義的類,通過ServiceLoader.load/Service.providers方法通過反射拿到實現類的例項。

在這裡插入圖片描述

相關文章