關於JVM,你必須知道的這些知識點

somehow1002發表於2020-12-20

一、一些必知引數

堆的分配引數

  • -Xmx:堆記憶體的最大大小(max)
  • -Xms:堆記憶體的初始大小(start)
  • -Xmn:新生代大小(new)
  • -XX:NewRatio
    老年代和新生代(eden+2*s)的比值
    例如:4,表示老年代:新生代=4:1,即新生代佔整個堆的1/5
  • -XX:SurvivorRatio(Survivor)
    設定新生代eden區與Survivor區的比值
    例如:8,表示 eden:s0:s1 = 8:1:1,即一個Survivor佔年輕代的1/10

棧的分配引數

棧的分配引數

  • -Xss
    設定棧空間的大小,通常只有幾百K。決定了函式呼叫的深度

舉例

-Xmx800M –Xms800M –Xmn300M -Xss256K

堆的大小 = 新生代大小 + 老年代大小,上例中最大堆記憶體為 800M,初始堆記憶體為 800M,新生代堆記憶體為 300M,棧空間大小為 256K

二、JVM記憶體分配機制

JVM物件分配邏輯順序如下圖

JVM物件分配流程

JVM給物件分配記憶體時,一般都是分配到eden的。但是由於JVM配置的不同,以及一些優化措施,可能會有一些特殊的邏輯,如:棧上分配、TLAB分配、直接進入老年代等。

1. 棧上分配

什麼是棧上分配?為什麼會有棧上分配?

棧上分配是JVM的一種優化技術,基本思想是對於那些執行緒私有的物件,可以將它們打散分配在棧上,而不是分配在堆上。
好處:可以在函式呼叫結束後自行銷燬,而不需要垃圾回收器的介入,從而提高系統的效能。
技術基礎: 逃逸分析標量替換(允許將物件根據屬性打散後分配在棧上)

2. TLAB

什麼是TLAB?為什麼會有TLAB?

TLAB:Thread Local Allocation Buffer(執行緒本地分配快取)是執行緒私有的堆空間
在堆上分配記憶體時,由於堆是執行緒共享的,容易發生衝突(儘管可以使用CAS進行同步,但是仍然可能失敗重試很多次)。所以可以優化使用執行緒自己的TLAB來嘗試分配物件,這樣可以儘可能避免執行緒同步

3. 老年代的物件來源

  • 很大的物件,新生代放不下
    JVM引數:-XX:PretenureSizeThreshold,預設為3145728,即 3M
  • 長期存活的物件
    JVM通過物件年齡判斷哪些物件需要晉升到老年代。
    JVM引數:-XX:MaxTenuringThreshold,預設為 15
  • 符合動態物件年齡判定規則的物件
    JVM並非 “固執” 的要求物件年齡必須達到 MaxTenuringThreshold 才能晉升到老年代,如果:
    Survivor空間相同年齡所有物件的大小總和 >Survivor空間的一半
    則:
    物件年齡 >= 該年齡的物件 就可以 直接晉升到老年代

    這樣做,相當於將Survivor空間一半的空間釋放出來了,減少了這些物件在Survivor空間間的來回挪騰

三、垃圾回收

1. 垃圾識別演算法

  • 引用計數法
  • 可達性分析演算法
    • GC Roots物件種類:
      • 虛擬機器棧中本地變數表引用的物件
      • 方法區中,類靜態變數/常量引用的物件
      • 本地方法棧中JNI(Native方法)引用的物件

如何記憶GC Roots節點種類?
其實可以回憶一下Java執行時資料區。

  1. 程式計數器顯然無法作為根節點
  2. 堆是我們要進行垃圾回收的區域
    因此就只剩下虛擬機器棧、本地方法棧和方法區這三個地方了。
    Java執行時資料區
    圖為Java執行時資料區

引用的型別有哪些?什麼時候需要回收?

Java將引用分為四種型別,分別如下(強度遞減):

  • 強引用 (Strong Reference)
    • 最常用的引用型別,預設宣告的物件為強引用,如:Object obj = new Object();
    • 使用上述的垃圾識別演算法(可達性分析演算法)進行是否是垃圾、是否要被回收
  • 軟引用 (Soft Reference)
    • 有用但非必需的物件,常用作快取
    • 當在記憶體不足時,就會回收(拋OutOfMemoryError異常之前)
  • 弱引用(Weak Reference)
    • 非必需的物件
    • 無論記憶體是否足夠,GC執行緒都會回收
  • 虛引用(Phantom Reference)
    • 任何時候都能被回收,主要用來接收GC的通知

垃圾分代回收型別

  • Minor GC
    發生在新生代的GC,比較頻繁,回收速度也較快
  • Major GC/Full GC
    發生在老年代的GC,一般同時會伴隨著至少一次的新生代的GC(Minor GC)。回收速度較慢

    分配擔保機制是什麼?
    當發生在Minor GC之前,JVM會進行檢查記憶體空間是否足夠,預設使用老年代的空間作為記憶體分配的擔保。
    但是如果:
    老年代連續可用空間 < 新生代物件總大小老年代連續可用空間 < 歷次晉升的連續大小
    則會先進行Full GC,清理釋放老年代空間。
    分配擔保機制的意義在於,保證Minor GC後有記憶體可以分配

2. 垃圾回收演算法

  • 標記-清除演算法
  • 複製回收演算法
  • 標記-整理演算法

3. 垃圾收集器

常見垃圾收集器

常見垃圾收集器
有連線代表可以配合使用

常見垃圾收集器工作流程

常見垃圾收集器工作流程

速記

Java各種垃圾收集器比較

  • 新生代

    1. Serial:
      • 使用複製回收演算法
      • STW時,單執行緒進行回收(STW:Stop The World)
    2. ParNew:
      • 使用複製回收演算法
      • STW時,多執行緒進行回收
    3. Parallel Scavenge(全域性):
      • 使用複製回收演算法
      • 關注吞吐量,即 CPU的使用效率,而不是單純的STW時間長短,適合於後臺任務型程式
      • 吞吐量 = 執行使用者程式碼時間 / (執行使用者程式碼時間 + 垃圾收集時間)
  • 老年代

    1. CMS:
      • 使用標記-清除演算法

      • 重點:減少回收停頓時間

      • 收集過程:
        ① 初始標記(CMS initial mark)
        ② 併發標記(CMS concurrenr mark)
        ③ 重新標記(CMS remark)
        ④ 併發清除(CMS concurrent sweep)

        其中初始標記、重新標記這兩個步驟任然需要停頓其他使用者執行緒。初始標記僅僅只是標記出GC ROOTS能直接關聯到的物件,速度很快,併發標記階段是進行GC ROOTS 根搜尋演算法階段,會判定物件是否存活。而重新標記階段則是為了修正併發標記期間,因使用者程式繼續執行而導致標記產生變動的那一部分物件的標記記錄,這個階段的停頓時間會被初始標記階段稍長,但比並發標記階段要短。

        由於整個過程中耗時最長的併發標記和併發清除過程中,收集器執行緒都可以與使用者執行緒一起工作,所以整體來說,CMS收集器的記憶體回收過程是與使用者執行緒一起併發執行的。

    2. Serial Old:
      • 使用標記-整理演算法
      • 是CMS備用方案,在Concurrent Mode Failure時使用(CMS垃圾回收時,但老年代空間不足)
    3. Parallel Old:
      • 使用標記-整理演算法
      • 與Parallel Scavenge配合使用,關注吞吐量
  • G1收集器

    • 將整個Java堆劃分為多個大小相等的獨立區域(Region),新生代和老年代不再是物理隔離的了
    • 從整體來看是基於“標記-整理”演算法,但從區域性來看是基於“複製“演算法
    • 收集過程:
      ① 初始標記(Initial Marking)
      ② 併發標記(Concurrent Marking)
      ③ 最終標記(Final Marking)
      ④ 篩選回收(Live Data Counting and Evacuation)

4. GC日誌怎麼看

Minor GC日誌
Minor GC log

Full GC日誌
Full GC log

5. 監控

JDK自帶的監控工具

  • jmap -heap pid 堆使用情況
  • jstat -gcutil pid 1000
  • jstack 執行緒dump
  • jvisualvm
  • jconsole

官方說明:
https://docs.oracle.com/javase/8/docs/technotes/tools/windows/toc.html

相關文章