溪源的Java筆記—JVM

前言

作爲一個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方法通過反射拿到實現類的實例。

在這裏插入圖片描述