【JVM】一文掌握JVM垃圾回收机制
作為Java程序員,除了業(yè)務邏輯以外,隨著更深入的了解,都無法避免的會接觸到JVM以及垃圾回收相關知識。JVM調(diào)優(yōu)是一個聽起來很可怕,實際上很簡單的事。
感到可怕,是因為垃圾回收相關機制都在JVM的C++層實現(xiàn),我們在Java開發(fā)中看不見摸不著;而實際很簡單,是因為它說到底,也只是JVM替我們實現(xiàn)的垃圾對象回收機制,也是普通的程序代碼,只要理解了垃圾回收器的底層設計思想,掌握JVM調(diào)優(yōu)并非難事!
一、JVM內(nèi)存模型
元數(shù)據(jù)區(qū):JDK8之前是方法區(qū)。存放虛擬機加載的:類型信息,域(Field)信息,方法(Method)信息,常量,靜態(tài)變量,即時編譯器編譯后的代碼緩存
虛擬機棧:虛擬機棧中保存了每一次方法調(diào)用的棧幀信息,棧幀中包含以下信息:
- 局部變量表:保存函數(shù) (即方法) 的局部變量
- 操作數(shù)棧:保存計算過程中的結(jié)果,即臨時變量
- 動態(tài)鏈接:指向方法區(qū)的運行時常量池。字節(jié)碼中的方法調(diào)用指令以常量池中指向方法的符號引用為參數(shù)。
- 方法的返回地址
本地方法棧:和虛擬機棧功能上類似,它管理了native方法的一些執(zhí)行細節(jié),而虛擬機棧管理的是Java方法的執(zhí)行細節(jié)。
程序計數(shù)器:程序計數(shù)器記錄線程執(zhí)行的字節(jié)碼行號,如果當前線程正在運行native方法則為空。每個線程都有自己的計數(shù)器
堆:JVM中產(chǎn)生的實例對象的存儲位置
所謂的垃圾回收,主要就是回收JVM中堆內(nèi)存的區(qū)域
二、垃圾定義
- 引用計數(shù)(ReferenceCount):存在循環(huán)引用的問題,漏掉循環(huán)引用的垃圾
- 根可達算法(RootSearching):判斷對象是否可通過引用尋到JVM的根節(jié)點,不能則是垃圾
三、垃圾回收算法
- 標記清除(mark sweep) - 位置不連續(xù) 產(chǎn)生碎片 效率偏低(兩遍掃描)
- 拷貝算法 (copying) - 沒有碎片,浪費空間
- 標記壓縮(mark compact) - 沒有碎片,效率偏低(兩遍掃描,指針需要調(diào)整)
四、垃圾回收器
通過以上三種算法的排列組合,產(chǎn)生了各種各樣的垃圾回收器
堆內(nèi)存邏輯分區(qū)
常見垃圾回收器
? Serial:單線程STW垃圾回收器,工作在年輕代。采用拷貝算法
? Serial Old:單線程STW垃圾回收器,工作在老年代。采用標記清除加壓縮算法
? Parallel Scavenge:并行垃圾回收,工作在年輕代。采用拷貝算法
? Parallel Old:并行垃圾回收,工作在老年代。采用標記清除加壓縮算法
? ParNew:并行垃圾回收,工作在年輕代。專門配合CMS使用
? CMS(Concurrent Mark-Sweep):并發(fā)標記清除,工作在老年代,采用標記清除算法。
? G1(Garbage First):垃圾優(yōu)先算法,采用拷貝算法
? ZGC(Z Garbage Collector):一種可伸縮的低延遲垃圾回收器,旨在處理TB級別的堆,同時保持低毫秒級別的停頓時間。它通過使用讀屏障和染色指針來實現(xiàn)這一點,并且在垃圾回收過程中幾乎不需要暫停應用線程
? Shenandoah GC:是一種旨在實現(xiàn)低停頓時間的垃圾回收器,它通過并發(fā)的方式來回收內(nèi)存。Shenandoah的目標是減少停頓時間,而不是優(yōu)化吞吐量,適用于需要大內(nèi)存和低延遲的應用
垃圾升級過程
- 新創(chuàng)建對象產(chǎn)生在eden區(qū)
- ygc觸發(fā),把eden區(qū)和s0區(qū)不是垃圾的對象復制到s1區(qū),并對非垃圾對象的頭部的分代年齡加一,然后清除eden區(qū)和s0區(qū)
- ygc觸發(fā),把eden區(qū)和s1區(qū)不是垃圾的對象復制到s0區(qū),并對非垃圾對象的頭部的分代年齡加一,然后清除eden區(qū)和s1區(qū)
- 當對象頭記錄的分代年齡達到15(默認最大分代年齡)時,jvm將把他從年輕代升級到老年代
eden區(qū)和s0、s1的默認比例是8:1:1,可通過參數(shù)-XX:SurvivorRatio配置
對象頭的年齡可通過-XX:MaxTenuringThreshold參數(shù)配置,但由于對象頭中只用4個比特位存儲分代年齡,因此它的區(qū)間是0-15
CMS垃圾回收器
CMS是用于回收老年代的垃圾回收器,它采用的是標記清除算法。CMS的誕生的目的在于提供在多核環(huán)境下的并發(fā)處理中大型堆(MB~GB)垃圾的能力
特點
- 并發(fā)收集:CMS的主要特點是它允許垃圾回收線程與應用程序線程同時運行。這減少了應用程序的停頓時間,特別是在長時間運行的老年代垃圾回收過程中。
- 低停頓時間:由于并發(fā)執(zhí)行,CMS旨在減少垃圾回收引起的停頓時間,這對于延遲敏感的應用程序非常重要。
- 存在內(nèi)存碎片:CMS通常不執(zhí)行堆壓縮,這意味著它不會重新安排存活對象來消除空閑空間之間的碎片。這可以減少停頓時間,但可能導致更多的內(nèi)存碎片。
- CPU資源密集型:CMS需要更多的CPU資源來執(zhí)行并發(fā)的垃圾回收。在多核處理器上,這通常不是問題,但在CPU資源受限的環(huán)境中,可能會影響應用程序的性能。
- 并發(fā)模式失敗:在極端情況下,如果老年代在CMS回收過程中被填滿,會發(fā)生“并發(fā)模式失敗”。這時,JVM會退回到傳統(tǒng)的完全停頓式垃圾回收,以清理老年代。
- 適用于中到大型堆:CMS適合于中到大型的堆,尤其是在有足夠CPU資源和對停頓時間敏感的應用場景中。
- 需要調(diào)優(yōu):為了獲得最佳性能,CMS可能需要通過JVM參數(shù)進行調(diào)優(yōu),如設置堆的大小、老年代的大小、并發(fā)線程數(shù)等。
垃圾回收過程
- 初始標記:找到根上的垃圾,會有非常短暫的STW
- 并發(fā)標記:標記垃圾,這一步可能產(chǎn)生漏標(掃描完不是垃圾之后,突然失去引用變成了垃圾),也可能產(chǎn)生多標(掃描完事垃圾之后,突然重新被引用變成不是垃圾)
- 重新標記:重新標記的目的是糾正上一步所產(chǎn)生的錯誤標記,會有時間不算很長的STW
- 并發(fā)清理:清理前面步驟所標記出來的垃圾
三色標記算法
作用于并發(fā)標記階段
對象標記為黑白灰三個顏色,記錄當前掃描標記的位置。
- 黑色:自己已經(jīng)被標記,自己的子引用也都標記完成
- 白色:沒有遍歷到的節(jié)點
- 灰色:自己已經(jīng)被標記,自己的子引用還沒被全部標記完成
三色標記的bug
由于并發(fā)標記是與用戶線程并行的,所以在并發(fā)標記的過程中對象的引用是可能發(fā)生變化的,所以可能會產(chǎn)生多標和漏標。并且重新標記為了減少STW的時間不會再標記黑色對象,而是掃描灰色對象的直接引用
- 多標:會導致產(chǎn)生浮動垃圾,需要在下一次判斷引用再回收,無大礙
- 漏標:會導致不應該被回收的對象被回收,問題嚴重
如上圖:在并發(fā)標記的過程中,同時產(chǎn)生這兩種情況時就會發(fā)生回收錯誤問題:A和C斷開了引用,A又引用了D。
- 對于對象C:應該回收的對象現(xiàn)在是黑色,留了下來
- 對于對象D:被引用了但還是白色,由于重新標記時不會再掃描黑色對象,這樣會導致對象D被當作垃圾而回收,產(chǎn)生嚴重bug
CMS對于三色標記的錯標處理
CMS的處理方式是Increment Updater(增量更新),即當已經(jīng)被掃描完的黑色對象如果產(chǎn)生了新的引用,則把自己標記為灰色,等待下次掃描重新標記。
但在上述的多標案例中,CMS存在卻依然并發(fā)標記Bug,如下時序圖
當兩個垃圾回收線程m1和m3加上一個業(yè)務線程m2同時標記一個對象時,m3認為應該標灰,但m1認為應該標黑,如果最終m1的標記覆蓋了m3的標記,那么對象的顏色標記錯誤,它下面新增的引用也不會被掃描到
CMS對于這個嚴重的bug的解決方案是,在重新標記階段重新掃描時,必須從頭掃描一遍,這樣就增加了STW的時間
G1垃圾回收器
G1(Garbage-First)垃圾回收器是Java虛擬機(JVM)的一個高級垃圾回收器,旨在為具有大內(nèi)存的多處理器機器提供高吞吐量和低延遲。G1垃圾回收器的主要特點包括:
特點
- 分區(qū)堆結(jié)構(gòu):G1將堆內(nèi)存分割成多個大小相等的區(qū)域(Region),這些區(qū)域可以被劃分為Eden區(qū)、Survivor區(qū)或Old區(qū)。這種分區(qū)方法有助于更有效地管理堆空間。
- 并發(fā)和并行處理:G1結(jié)合了并發(fā)和并行的垃圾回收機制,以優(yōu)化性能和減少停頓時間。
- 可預測的停頓時間:G1的一個關鍵目標是提供可預測的停頓時間,允許用戶指定期望的停頓時間目標(例如,不超過50毫秒),G1將盡量在這個時間范圍內(nèi)完成垃圾回收。
- 增量式清理:G1通過逐步清理堆中的區(qū)域來管理垃圾回收,這有助于控制停頓時間。
- 記憶集(RSet):G1使用記憶集來跟蹤跨區(qū)域引用的對象,這有助于在垃圾回收時快速確定哪些對象是存活的。
- 混合收集:G1可以同時回收Young和Old區(qū)域。在進行混合收集時,G1會根據(jù)需要和停頓時間目標選擇性地回收一部分Old區(qū)域。
- 高效的大對象處理:G1能夠更有效地處理大對象,因為它可以跨多個區(qū)域分配這些對象。
- 自適應調(diào)整:G1會根據(jù)應用程序的行為和指定的停頓時間目標自動調(diào)整堆占用和回收策略。
- 適用于大堆:G1特別適合于大堆(多GB)的應用,因為它能夠更好地管理大內(nèi)存并保持合理的停頓時間。
分區(qū)算法(Region)
G1的物理分區(qū)從分代變成了分區(qū)(Region),邏輯上分代,物理上則取消了分代,把堆整體劃分成了多個(2048)相同大小的小格子(Region)
其中,每個Region的大小可通過-XX:G1HeapRegionSize設定,取值范圍為1-32MB,且必須為2的N次冪,即只能為2,4,8,16,32這五個數(shù)
每一個Region都可以根據(jù)需要充當新生代的Eden區(qū)、S區(qū)(G1取消了S0和S1,只使用一個Survivor區(qū))或者老年代。在一般的垃圾收集中對于堆中的大對象,默認直接會被分配到老年代,但是如果它是一個短期存在的大對象,就會對垃圾收集器造成負面影響。為了解決這個問題,G1劃分了一個Humongous區(qū),它用來專門存放大對象。如果一個H區(qū)裝不下一個大對象,那么G1會尋找連續(xù)的H區(qū)來存儲。為了能找到連續(xù)的H區(qū),有時候不得不啟動Full GC。 G1的大多數(shù)行為都把H區(qū)作為老年代的一部分來看待。當一個對象的大小超過了一個Region容量的一半,即被認為是大對象。
雖然G1仍然保留新生代和老年代的概念,但新生代和老年代不再是固定的了,而是一系列區(qū)域(不需要連續(xù),邏輯連續(xù)即可)的動態(tài)集合。由于G1這種基于Region回收的方式,可以預測停頓時間。G1會根據(jù)每個Region里面垃圾“價值”的大小,在后臺維護一個優(yōu)先級列表,每次根據(jù)用戶設定的允許收集停頓的時間(-XX:MaxGCPauseMillis,默認為200毫秒)優(yōu)先處理價值收益最大的Region。
垃圾回收過程
G1采用的復制(copying)算法進行回收
- 初始標記:僅僅只是標記一下GC Roots能直接關聯(lián)到的對象,并且修改TAMS指針的值,讓下一階段用戶線程并發(fā)運行時,能夠在Region上正確的分配對象。這個階段需要STW,耗時很短,而且是借用MinorGC(上一輪垃圾回收時觸發(fā)GC)時候同步完成的。
- 并發(fā)標記:從GC Roots 開始對堆中的對象進行可達性分析,遞歸掃描整個堆里的對象,這個過程耗時較長,但是是與用戶線程并發(fā)執(zhí)行的。對象掃描完之后還需要重新處理STAB記錄下的在并發(fā)時有引用變動的對象。
- 最終標記:這個階段也需要STW,用于處理并發(fā)階段結(jié)束后仍然遺留下來的最后少量的STAB記錄。
- 篩選回收:負責更新Region的統(tǒng)計數(shù)據(jù),對各個Region的回收價值和成本排序,根據(jù)用戶期望的停頓時間來執(zhí)行回收計劃,然后把決定回收的Region里的存活對象復制到空的Region,然后清空舊Region的空間。由于涉及到對象的移動,所以這個階段也是需要STW的。
從上述可以看出,除了并發(fā)標記,其他階段都是需要STW的,G1收集器不單單是追求低延遲的收集器,也衡量了吞吐量,所以在延遲和吞吐量之間做了一個權衡。
G1對于三色標記的錯標處理
從上述過程可以看出G1的處理方式是SATB(snapshot at the begining),即在并發(fā)標記中,如果出現(xiàn)引用的變更,G1的垃圾回收器會記錄在SATB中,每次線程切回來進行垃圾回收時,先讀取SATB中的記錄。
RememberedSet
簡稱RSet,記錄了其他Region的對象到本Region的引用,使得垃圾回收器不需要掃描整個堆找到誰引用了當前分區(qū)的對象,只需掃描RSet即可
更多技術干貨,歡迎關注我!
總結(jié)
以上是生活随笔為你收集整理的【JVM】一文掌握JVM垃圾回收机制的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Codeforces 918(div4)
- 下一篇: 解密Prompt系列22. LLM Ag