關於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物件。
- 常量池查詢類的常量引用,如果沒有先做類載入
- 分配記憶體,視堆記憶體是否是規整(由垃圾回收器是否具有壓縮功能而定)而使用“指標碰撞”或“空閒列表”模式
- 記憶體空間初始化為零值,可能提前線上程建立時分配TLAB時做初始化
- 設定必要資訊,如物件是哪個類的示例、元資訊、GC分代年齡等
- 呼叫
<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)”這部分的分析。
可借鑑的程式設計模式
物件分配的併發控制
物件建立是很頻繁的,線上程共享的堆中會遇到併發的問題。兩種解決辦法:
- 同步鎖定:CAS+失敗重試,確保原子性
- 堆中預先給每個執行緒劃分一小塊記憶體區域——本地執行緒分配緩衝(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中地址穩定,物件被移動時只需要改控制代碼池的地址。相對的,訪問例項需要兩次指標定位。
參考資料
- 周志明.著《深入理解JAVA虛擬機器》
- 深入理解JVM-記憶體模型(jmm)和GC
- jvm的新生代、老年代、永久代關係
- JVM垃圾回收——新生代,老年代,永久代,Minor GC,Full GC