溫故知新-java虛擬機



java虛擬機是什麼?

java虛擬機(java virtual machine,JVM),一種能夠運行java字節碼的虛擬機。
作爲一種編程語言的虛擬機,實際上不只是專用於Java語言,只要生成的編譯文件匹配JVM對加載編譯文件格式要求,任何語言都可以由JVM編譯運行,比如kotlin、scala等。
jvm有很多,除了Hotspot,還有JRockit、J9等
  • 可以理解爲,java運行在虛擬機上,可以屏蔽硬件差異;js運行在瀏覽器中,瀏覽器屏蔽了硬件差異;

jvm的體系結構

JVM由三個主要的子系統構成: 類加載子系統、運行時數據區(內存結構)、執行引擎

image.png

  • 這張圖需要仔細看一下,包含了類加載子系統、運行時數據區(內存結構)、執行引擎三個子系統包含的內容;

第一個類加載子系統

類的生命週期

image.png

  • 過程:加載-> 鏈接-> 初始化-> 使用 ->卸載

加載器分類

  • 啓動類加載器(Bootstrap ClassLoader)
    負責加載JRE的核心類庫,如JRE目標下的rt.jar,charsets.jar等
  • 擴展類加載器(Extension ClassLoader)
    負責加載JRE擴展目錄ext中jar類包
  • 系統類加載器(Application ClassLoader)
    負責加載ClassPath路徑下的類包
  • 用戶自定義加載器(User ClassLoader)
    負責加載用戶自定義路徑下的類包

類加載機制

  • 全盤負責委託機制
當一個ClassLoader加載一個類的時候,除非顯示的使用另一個ClassLoader,該類所依賴和引用的類也由這個
ClassLoader載入
  • 雙親委派機制
指先委託父類加載器尋找目標類,在找不到的情況下載自己的路徑中查找並載入目標類
雙親委派模式的優勢
沙箱安全機制:比如自己寫的String.class類不會被加載,這樣可以防止核心庫被隨意篡改
避免類的重複加載:當父ClassLoader已經加載了該類的時候,就不需要子ClassLoader再加載一次

第二個運行時數據區(內存結構)

image.png
從圖中可以看到,主要分爲方法區、堆、棧、程序計數器、本地方法棧;

  1. 方法區
類的所有字段和方法字節碼,以及一些特殊方法如構造函數,接口代碼在這裏定義。簡單來說,所有定義的方法的
信息都保存在該區域,靜態變量+常量+類信息(構造方法/接口定義)+運行時常量池都存在方法區中;
雖然Java虛擬機規範把方法區描述爲堆的一個邏輯部分,但是它卻有一個別名叫做Non-Heap(非堆),目的應該是爲了和Java的堆區分開;

  1. image.png

虛擬機啓動時自動分配創建,用於存放對象的實例,幾乎所有對象都在堆上分配內存,當對象無法在該空間申請到內存是將拋出OutOfMemoryError異常。同時也是垃圾收集器管理的主要區域。

  • 堆主要分爲三個區域:新生代、老年代、原空間;
    • 新生代(Young Generation)
      • 在這裏會發生類出生、成長、消亡的區域;
      • 新生代又分爲兩個部分:伊甸區(Eden space)和倖存者區(Survivor space),所有的類都是在伊甸區被new出來的。
      • 倖存區又分爲From和To區。
        當Eden區的空間用完是,程序又需要創建對象,JVM的垃圾回收器將Eden區進行垃圾回收(Minor GC),將Eden區中的不再被其它對象應用的對象進行銷燬。然後將Eden區中剩餘的對象移到From Survivor區。若From Survivor區也滿了,再對該區進行垃圾回收,然後移動到To Survivor區。
    • 老年代(Old Generation)
      新生代經過多次GC仍然存貨的對象移動到老年區。若老年代也滿了,這時候將發生Major GC(也可以叫Full GC),進行老年區的內存清理。若老年區執行了Full GC之後發現依然無法進行對象的保存,就會拋出OOM(OutOfMemoryError)異常
    • 元空間(Meta Space)
      在JDK1.8之後,元空間替代了永久代,它是對JVM規範中方法區的實現,區別在於元數據區不在虛擬機當中,而是用的本地內存,永久代在虛擬機當中,永久代邏輯結構上也屬於堆,但是物理上不屬於。
  1. 棧(Stack)
    Java線程執行方法的內存模型,一個線程對應一個棧,每個方法在執行的同時都會創建一個棧幀(用於存儲局部變量表,操作數棧,動態鏈接,方法出口等信息)不存在垃圾回收問題,只要線程一結束該棧就釋放,生命週期和線程一致

  2. 本地方法棧(Native Method Stack)
    和棧作用很相似,區別不過是Java棧爲JVM執行Java方法服務,而本地方法棧爲JVM執行native方法服務。登記native方法,在Execution Engine執行時加載本地方法庫

  3. 程序計數器(Program Counter Register)
    就是一個指針,指向方法區中的方法字節碼(用來存儲指向嚇一跳指令的地址,也即將要執行的指令代碼),由執行引擎讀取下一條指令,是一個非常小的內存空間,幾乎可以忽略不計

GC算法和收集器

如何判斷對象可以被回收?

  • 堆中幾乎放着所有的對象實例,對堆垃圾回收前的第一步就是要判斷哪些對象已經死亡(即不能再被任何途徑使用的對象)

  • 引用計數法;

    • 給對象添加一個引用計數器,每當有一個地方引用,計數器就加1。當引用失效,計數器就減1。任何時候計數器爲0的對象就是不可能再被使用的。
    • 實現簡單,效率高,但是目前主流的虛擬機中沒有選擇這個算法來管理內存,最主要的原因是它很難解決對象之前相互循環引用的問題。所謂對象之間的相互引用問題,通過下面代碼所示:除了對象a和b相互引用着對方之外,這兩個對象之間再無任何引用。但是它們因爲互相引用對方,導致它們的引用計數器都不爲0,於是引用計數器法無法通知GC回收器回收它們。
  • 可達性分析算法

    • 算法的基本思想就是通過一系列的稱爲”GC Roots“的對象作爲起點,從這些節點開始向下搜索,節點所走過的路徑稱爲引用鏈,當一個對象到GC Roots沒有任何引用鏈相連的話,則證明此對象時不可用的。
    • GC Roots根節點:類加載器、Thread、虛擬機棧的本地變量表、static成員、常量引用、本地方法棧的變量等等

如何判斷一個常量是廢棄常量?

運行時常量池主要回收的是廢棄的常量。那麼,我們怎麼判斷一個常量時廢棄常量呢?

  • 假如在常量池中存在字符串"abc",如果當前沒有任何String對象引用該字符串常量的話,就說明常量”abc“就是廢棄常量,如果這時發生內存回收的話而且有必要的話,”abc“會被系統清理出常量池。

如何判斷一個類是無用的類?

  • 需要滿足以下三個條件:

    • 該類所有的實例都已經被回收,也就是 Java 堆中不存在該類的任何實例。
    • 加載該類的 ClassLoader 已經被回收。
    • 該類對應的 java.lang.Class 對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。
  • 虛擬機可以對滿足上述3個條件的無用類進行回收,這裏僅僅是”可以“,而並不是和對象一樣不適用了就必然會被回收;

垃圾回收算法

  • 垃圾回收算法有很多,每一種都有不同的優劣、在不通的堆區域,會採用不同的垃圾回收算法;
    image.png
  • 標記-清除算法
    它是最基礎的收集算法,這個算法分爲兩個階段,“標記”和”清除“。首先標記出所有需要回收的對象,在標記完成後統一回收所有被標記的對象。它有兩個不足的地方:
    • 效率問題,標記和清除兩個過程的效率都不高;
    • 空間問題,標記清除後會產生大量不連續的碎片;
      image.png

複製算法

爲了解決效率問題,複製算法出現了。它可以把內存分爲大小相同的兩塊,每次只使用其中的一塊。當這一塊的內存,使用完後,就將還存活的對象複製到另一塊區,然後再把使用的空間一次清理掉。這樣就使每次的內存回收都是對內存區間的一半進行回收
image.png

標記-整理算法

根據老年代的特點提出的一種標記算法,標記過程和“標記-清除”算法一樣,但是後續步驟不是直接對可回收對象進行回收,而是讓所有存活的對象向一段移動,然後直接清理掉邊界以外的內存
image.png

分代收集算法

現在的商用虛擬機的垃圾收集器基本都採用"分代收集"算法,這種算法就是根據對象存活週期的不同將內存分爲幾塊。一般將java堆分爲新生代和老年代,這樣我們就可以根據各個年代的特點選擇合適的垃圾收集算法。

  • eg:在新生代中,每次收集都有大量對象死去,所以可以選擇複製算法,只要付出少量對象的複製成本就可以完成每次垃圾收集。而老年代的對象存活機率時比較高的,而且沒有額外的空間對它進行分配擔保,就必須選擇“標記-清除”或者“標記-整理”算法進行垃圾收集

垃圾收集器

Serial收集器

Serial(串行)收集器收集器是最基本、歷史最悠久的垃圾收集器了。一個單線程收集器,它的 “單線程” 的意義不僅僅意味着它只會使用一條垃圾收集線程去完成垃圾收集工作,更重要的是它在進行垃圾收集工作的時候必須暫停其他所有的工作線程( “Stop The World” ),直到它收集結束。

ParNew收集器

  • ParNew收集器其實就是Serial收集器的多線程版本,除了使用多線程進行垃圾收集外,其餘行爲(控制參數、收集算法、回收策略等等)和Serial收集器完全一樣。
  • 新生代採用複製算法,老年代採用標記-整理算法。

Parallel Scavenge收集器

  • Parallel Scavenge 收集器類似於ParNew 收集器。
  • Parallel Scavenge收集器關注點是吞吐量(高效率的利用CPU)。
    • CMS等垃圾收集器的關注點更多的是用戶線程的停頓時間(提高用戶體驗)。所謂吞吐量就是CPU中用於運行用戶代碼的時間與CPU總消耗時間的比值。 Parallel Scavenge收集器提供了很多參數供用戶找到最合適的停頓時間或最大吞吐量,如果對於收集器運作不太瞭解的話,手工優化存在的話可以選擇把內存管理優化交給虛擬機去完成也是一個不錯的選擇。
  • 新生代採用複製算法,老年代採用標記-整理算法。

Serial Old收集器

Serial收集器的老年代版本,它同樣是一個單線程收集器。它主要有兩大用途:一種用途是在JDK1.5以及以前的版本中與Parallel Scavenge收集器搭配使用,另一種用途是作爲CMS收集器的後備方案

Parallel Old收集器

Parallel Scavenge收集器的老年代版本。使用多線程和“標記-整理”算法。在注重吞吐量以及CPU資源的場合,都可以優先考慮 Parallel Scavenge收集器和Parallel Old收集器。

CMS收集器

  • CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間爲目標的收集器。它而非常符合在注重用戶體驗的應用上使用。

  • CMS(Concurrent Mark Sweep)收集器是HotSpot虛擬機第一款真正意義上的併發收集器,它第一次實現了讓垃圾收集線程與用戶線程(基本上)同時工作。

  • 從名字中的Mark Sweep這兩個詞可以看出,CMS收集器是一種 “標記-清除”算法實現的,它的運作過程相比於前面幾種垃圾收集器來說更加複雜一些。整個過程分爲四個步驟:

    • 初始標記(CMS initial mark): 暫停所有的其他線程,並記錄下直接與root相連的對象,速度很快;
    • 併發標記(CMS concurrent mark): 同時開啓GC和用戶線程,用一個閉包結構去記錄可達對象。但在這個階段結束,這個閉包結構並不能保證包含當前所有的可達對象。因爲用戶線程可能會不斷的更新引用域,所以GC線程無法保證可達性分析的實時性。所以這個算法裏會跟蹤記錄這些發生引用更新的地方。
    • 重新標記(CMS remark): 重新標記階段就是爲了修正併發標記期間因爲用戶程序繼續運行而導致標記產生變動的那一部分對象的標記記錄,這個階段的停頓時間一般會比初始標記階段的時間稍長,遠遠比並發標記階段時間短
    • 併發清除(CMS concurrent sweep): 開啓用戶線程,同時GC線程開始對爲標記的區域做清掃
  • CMS主要優點:併發收集、低停頓。但是它有下面三個明顯的缺點:

    • 對CPU資源敏感;
    • 無法處理浮動垃圾;
    • 它使用的回收算法-“標記-清除”算法會導致收集結束時會有大量空間碎片產生。

G1收集器

G1 (Garbage-First)是一款面向服務器的垃圾收集器,主要針對配備多顆處理器及大容量內存的機器. 以極高概率滿足,GC停頓時間要求的同時,還具備高吞吐量性能特徵;
被視爲JDK1.7中HotSpot虛擬機的一個重要進化特徵。它具備一下特點:

  • 並行與併發:G1能充分利用CPU、多核環境下的硬件優勢,使用多個CPU(CPU或者CPU核心)來縮短StopThe-World停頓時間。部分其他收集器原本需要停頓Java線程執行的GC動作,G1收集器仍然可以通過併發的方式讓java程序繼續執行

  • 分代收集:雖然G1可以不需要其他收集器配合就能獨立管理整個GC堆,但是還是保留了分代的概念。 空間整合:與CMS的“標記–清理”算法不同,G1從整體來看是基於“標記整理”算法實現的收集器;從局部上來看是基於“複製”算法實現的

  • 可預測的停頓:這是G1相對於CMS的另一個大優勢,降低停頓時間是G1 和 CMS 共同的關注點,但G1 除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度爲M毫秒的時間片段內

  • G1收集器的運作大致分爲以下幾個步驟:

    • 初始標記
    • 併發標記
    • 最終標記
    • 篩選回收
  • G1收集器在後臺維護了一個優先列表,每次根據允許的收集時間,優先選擇回收價值最大的Region(這也就是它的名字Garbage-First的由來)。這種使用Region劃分內存空間以及有優先級的區域回收方式,保證了GF收集器在有限時間內可以儘可能高的收集效率(把內存化整爲零)

Z Garbage Collector

即ZGC,是一個可伸縮的、低延遲的垃圾收集器,主要爲了滿足如下目標進行設計:
停頓時間不會超過10ms
停頓時間不會隨着堆的增大而增大(不管多大的堆都能保持在10ms以下)
可支持幾百M,甚至幾T的堆大小(最大支持4T)
停頓時間在10ms以下,10ms其實是一個很保守的數據,在SPECjbb 2015基準測試,128G的大堆下最大停頓時間才1.68ms,遠低於10ms;

The Z Garbage Collector, also known as ZGC, is a scalable low latency garbage collector designed to meet the following goals:

Pause times do not exceed 10ms
Pause times do not increase with the heap or live-set size
Handle heaps ranging from a few hundred megabytes to multi terabytes in size

怎麼選擇垃圾收集器?

  1. 優先調整堆的大小讓服務器自己來選擇
  2. 如果內存小於100m,使用串行收集器
  3. 如果是單核,並且沒有停頓時間的要求,串行或JVM自己選擇
  4. 如果允許停頓時間超過1秒,選擇並行或者JVM自己選
  5. 如果響應時間最重要,並且不能超過1秒,使用併發收集器
  • 官方推薦G1,性能高,如果使用JDK11及以上,也可以考慮一下ZGC

調優

JVM調優主要就是調整下面兩個指標

  • 停頓時間:垃圾收集器做垃圾回收中斷應用執行的時間。-XX:MaxGCPauseMillis
  • 吞吐量:垃圾收集的時間和總時間的佔比:1/(1+n),吞吐量爲1-1/(1+n)。-XX:GCTimeRatio=n

GC常用參數

堆棧設置

-Xss:每個線程的棧大小
-Xms:初始堆大小,默認物理內存的1/64
-Xmx:最大堆大小,默認物理內存的1/4
-Xmn:新生代大小
-XX:NewSize:設置新生代初始大小-XX:NewRatio:默認2表示新生代佔年老代的1/2,佔整個堆內存的1/3。
-XX:SurvivorRatio:默認8表示一個survivor區佔用1/8的Eden內存,即1/10的新生代內存。
-XX:MetaspaceSize:設置元空間大小
-XX:MaxMetaspaceSize:設置元空間最大允許大小,默認不受限制,JVM Metaspace會進行動態擴展

垃圾回收統計信息

-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:filename

收集器設置

-XX:+UseSerialGC:設置串行收集器
-XX:+UseParallelGC:設置並行收集器
-XX:+UseParallelOldGC:老年代使用並行回收收集器
-XX:+UseParNewGC:在新生代使用並行收集器
-XX:+UseParalledlOldGC:設置並行老年代收集器
-XX:+UseConcMarkSweepGC:設置CMS併發收集器
-XX:+UseG1GC:設置G1收集器
-XX:ParallelGCThreads:設置用於垃圾回收的線程數

並行收集器設置

-XX:ParallelGCThreads:設置並行收集器收集時使用的CPU數。並行收集線程數。
-XX:MaxGCPauseMillis:設置並行收集最大暫停時間
-XX:GCTimeRatio:設置垃圾回收時間佔程序運行時間的百分比。公式爲1/(1+n)

CMS收集器設置

-XX:+UseConcMarkSweepGC:設置CMS併發收集器
-XX:+CMSIncrementalMode:設置爲增量模式。適用於單CPU情況。
-XX:ParallelGCThreads:設置併發收集器新生代收集方式爲並行收集時,使用的CPU數。並行收集線程數。
-XX:CMSFullGCsBeforeCompaction:設定進行多少次CMS垃圾回收後,進行一次內存壓縮
-XX:+CMSClassUnloadingEnabled:允許對類元數據進行回收
-XX:UseCMSInitiatingOccupancyOnly:表示只在到達閥值的時候,才進行CMS回收
-XX:+CMSIncrementalMode:設置爲增量模式。適用於單CPU情況
-XX:ParallelCMSThreads:設定CMS的線程數量
-XX:CMSInitiatingOccupancyFraction:設置CMS收集器在老年代空間被使用多少後觸發
-XX:+UseCMSCompactAtFullCollection:設置CMS收集器在完成垃圾收集後是否要進行一次內存碎片的整理

G1收集器設置

-XX:+UseG1GC:使用G1收集器
-XX:ParallelGCThreads:指定GC工作的線程數量
-XX:G1HeapRegionSize:指定分區大小(1MB~32MB,且必須是2的冪),默認將整堆劃分爲2048個分區
-XX:GCTimeRatio:吞吐量大小,0-100的整數(默認9),值爲n則系統將花費不超過1/(1+n)的時間用於垃圾收集
-XX:MaxGCPauseMillis:目標暫停時間(默認200ms)
-XX:G1NewSizePercent:新生代內存初始空間(默認整堆5%)
-XX:G1MaxNewSizePercent:新生代內存最大空間
-XX:TargetSurvivorRatio:Survivor填充容量(默認50%)
-XX:MaxTenuringThreshold:最大任期閾值(默認15)
-XX:InitiatingHeapOccupancyPercen:老年代佔用空間超過整堆比IHOP閾值(默認45%),超過則執行混合收集
-XX:G1HeapWastePercent:堆廢物百分比(默認5%)
-XX:G1MixedGCCountTarget:參數混合週期的最大總次數(默認8


你的鼓勵也是我創作的動力

打賞地址

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章