1. 程式人生 > >關於GC(中):Java垃圾回收相關基礎知識

關於GC(中):Java垃圾回收相關基礎知識

Java記憶體模型


(圖源: 深入理解JVM-記憶體模型(jmm)和GC)

區域名 英文名 訪問許可權 作用 備註
程式計數器 Program Counter Register 執行緒隔離 標記待取的下一條執行的指令 執行Native方法時為空; JVM規範中唯一不會發生OutOfMemoryError的區域
虛擬機器棧 VM Stack 執行緒隔離 每個Java方法執行時建立,用於儲存區域性變量表,操作棧,動態連結,方法出口等資訊 方法執行的記憶體模型
本地方法棧 Native Method Stack 執行緒隔離 Native方法執行時使用 JVM規範沒有強制規定,如Hotspot將VM和Native兩個方法棧合二為一
Java堆 Java Heap 執行緒共享 存放物件例項 更好的回收記憶體 vs 更快的分配記憶體
方法區 Method Area 執行緒共享 儲存已被虛擬機器載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼等資料 JVM規範不強制要求做垃圾收集
執行時常量池 Runtime Constant Pool 執行緒共享 方法區的一部分
直接記憶體 Direct Memory - 堆外記憶體,通過堆的DirectByteBuffer訪問 不是執行時資料區的一部分,但也可能OutOfMemoryError

物件的建立——new的時候發生了什麼

討論僅限於普通Java物件,不包括陣列和Class物件。

  1. 常量池查詢類的常量引用,如果沒有先做類載入
  2. 分配記憶體,視堆記憶體是否是規整(由垃圾回收器是否具有壓縮功能而定)而使用“指標碰撞”或“空閒列表”模式
  3. 記憶體空間初始化為零值,可能提前線上程建立時分配TLAB時做初始化
  4. 設定必要資訊,如物件是哪個類的示例、元資訊、GC分代年齡等
  5. 呼叫<init>方法

垃圾回收器總結

垃圾回收,針對的都是堆。

分代

  • 新生代:適合使用複製演算法, 以下三個區一般佔比為8:1:1
    • Eden 新物件誕生區
    • From Survivor 上一次GC的倖存者(見“GC種類-minor GC”)
    • To Survivor 本次待存放倖存者的區域
  • 老年代:存活時間較久的,大小較大的物件,因此使用標記-整理或標記-清除演算法比較合適
  • 永久代:存放類資訊和元資料等不太可能回收的資訊。Java8中被元空間(Metaspace)代替,不再使用堆,而是實體記憶體。

分代的原因

  • 不同代的物件生命週期不同,可以針對性地使用不同的垃圾回收演算法
  • 不同代可以分開進行回收

回收演算法

名稱 工作原理 優點 缺點
標記-清除 對可回收對物件做一輪標記,標記完成後統一回收被標記的物件 易於理解,記憶體利用率高 效率問題;記憶體碎片;分配大物件但無空間時提前GC
複製 記憶體均分兩塊,只使用其中一塊。回收時將這一塊存活物件全部複製到另一塊 效率高 可用空間減少; 空間不夠時需老年代分配擔保
標記-整理 對可回收對物件做一輪標記,標記完成後將存活物件統一左移,清理掉邊界外記憶體 記憶體利用率高 效率問題

標記-X演算法適用於老年代,複製演算法適用於新生代。

GC種類

  • Minor GC,只回收新生代,將Eden和From Survivor區的存活物件複製到To Survivor
  • Major GC,清理老年代。但因為伴隨著新生代的物件生命週期升級到老年代,一般也可認為伴隨著FullGC。
  • FullGC,整個堆的回收
  • Mixed GC,G1特有,可能會發生多次回收,可以參考關於G1 GC中Mixed GC的分析

垃圾回收器小結

垃圾回收器名稱 特性 目前工作分代 回收演算法 可否與Serial配合 可否與ParNew配合 可否與ParallelScavenge配合 可否與SerialOld配合 可否與ParallelOld配合 可否與CMS配合 可否與G1配合
Serial 單執行緒 新生代 複製 - - - Y N Y N/A
ParNew 多執行緒 新生代 複製 - - - N N Y N/A
ParallelScavenge 多執行緒, 更關注吞吐量可調節 新生代 複製 - - - N N Y N/A
SerialOld 單執行緒 老年代 標記-整理 - - - Y Y N N/A
ParallelOld 多執行緒 老年代 標記-整理 N N Y - - - N/A
CMS 多執行緒,併發收集,低停頓。但無法處理浮動垃圾,標記-清除會產生記憶體碎片較多 老年代 標記-清除 Y Y N Y - - N/A
G1 並行併發收集,追求可預測但回收時間,整體記憶體模型有所變化 新生代/老年代 整體是標記-整理,區域性(兩Region)複製 N N N N N N -

在本系列的上一篇文章關於GC(上):Apache的POI元件導致線上頻繁FullGC問題排查及處理全過程中,減少FullGC的方式是使用G1代替CMS,計劃在下一篇文章中對比CMS和G1的區別。

理解GC日誌

只舉比較簡單的例子,具體各項的格式視情況分析,不同回收器也會有差異。

2019-11-22T10:28:32.177+0800: 60188.392: [GC (Allocation Failure) 2019-11-22T10:28:32.178+0800: 60188.392: [ParNew: 1750382K->2520K(1922432K), 0.0312604 secs] 1945718K->198045K(4019584K), 0.0315892 secs] [Times: user=0.09 sys=0.01, real=0.03 secs]

開始時間-(方括號[)-發生區域(ParNew,命名和GC回收器有關)-回收前大小-回收後大小-(方括號])-GC前堆已使用容量-GC後堆已使用容量大小-回收時間-使用時間詳情(使用者態時間-核心時間-牆上時鐘時間)

注意這裡沒有包括“2019-11-22T10:28:32.177+0800: 60188.392: [GC (Allocation Failure)”這部分的分析。

可借鑑的程式設計模式

物件分配的併發控制

物件建立是很頻繁的,線上程共享的堆中會遇到併發的問題。兩種解決辦法:

  1. 同步鎖定:CAS+失敗重試,確保原子性
  2. 堆中預先給每個執行緒劃分一小塊記憶體區域——本地執行緒分配緩衝(TLAB),TLAB使用完並分配新的TLAB時才做同步鎖定。可看作1的優化。

CAS: Conmpare And Swap,用於實現多執行緒同步的原子指令。 將記憶體位置的內容與給定值進行比較,只有在相同的情況下,將該記憶體位置的內容修改為新的給定值。關於CAS可以參考:
Java中的CAS實現原理
CAS系列(3):CAS無鎖自旋和同步鎖執行緒切換使用場景對比

物件訪問的定位方式

前提條件:通過棧上本地變量表的reference訪問堆中的物件及它在方法區的物件型別資料(類資訊)
主流的兩種方式,這兩種方式各有優點,可以看出方式2是方式1的優化,但並不是全面超越方式1,無法完全取代。
這裡可以看到要權衡垃圾回收和訪問速度兩方面。

方式1: 直接指標訪問例項資料


圖源:深入理解JVM-記憶體模型(jmm)和GC
reference直接存放物件例項地址,只需要一次訪問即可,執行效率較高。

方式2: 使用控制代碼池


圖源:深入理解JVM-記憶體模型(jmm)和GC
reference中地址穩定,物件被移動時只需要改控制代碼池的地址。相對的,訪問例項需要兩次指標定位。

參考資料

  1. 周志明.著《深入理解JAVA虛擬機器》
  2. 深入理解JVM-記憶體模型(jmm)和GC
  3. jvm的新生代、老年代、永久代關係
  4. JVM垃圾回收——新生代,老年代,永久代,Minor GC,Full GC