天天看點

深入淺出!如何用一段代碼證明JVM加載類是懶加載模式?看看這篇文章吧!前言垃圾回收算法複制算法标記清除标記整理垃圾回收器CMS(Concurrent Mark Sweep)回收器G1(Garbage First)并發标記三色标記算法總結

前言

我的一個朋友,開發四年了,沒跳過槽,四年時間也不過是從最開始的10K漲到了15K,經常和我吐槽工資低。去年8月份左右開始了他“騎驢找馬”的行動,從各種地方找學習資料、刷面試題。值得慶幸的是,他出去找工作時疫情還不嚴重,異常順利的面進了螞蟻,薪資更是翻了幾倍。現在讓我好生羨慕,于是找他要了他刷了至少七遍以上的面試題,特地分享給大家學習:

這裡就不過過多贅述了,直接進入正文!

垃圾回收算法

垃圾回收算法的實作設計到大量的程式細節,并且每一個平台的虛拟機操作記憶體的方式都有不同,是以不需要去了解算法的具體實作。

複制算法

将可用記憶體按容量劃分為大小相等的兩塊,每次隻使用其中的一塊。當這一塊的記憶體用完了,就将還存活着的對象複制到另外一塊上面,然後再把已使用過的記憶體空間一次清理掉。這樣使得每次都是對整個半區進行記憶體回收,記憶體配置設定時也就不用考慮記憶體碎片等複雜情況,隻要按順序配置設定記憶體即可,實作簡單,運作高效。

深入淺出!如何用一段代碼證明JVM加載類是懶加載模式?看看這篇文章吧!前言垃圾回收算法複制算法标記清除标記整理垃圾回收器CMS(Concurrent Mark Sweep)回收器G1(Garbage First)并發标記三色标記算法總結

隻是這種算法的代價是将記憶體縮小為了原來的一半。但是要注意:記憶體移動是必須實打實的移動(複制),是以對應的引用(直接指針)需要調整。

複制回收算法适合于新生代,因為大部分對象朝生夕死,那麼複制過去的對象比較少,效率自然就高,另外一半的一次性清理是很快的。

Appel式回收

一種更加優化的複制回收分代政策:具體做法是配置設定一塊較大的 Eden 區和兩塊較小的 Survivor 空間(一般稱作做From區和To區,也可以叫做S0和S1)

基于經驗統計,新生代中的對象98%是“朝生夕死”的,是以并不需要按照 1:1 的比例來劃分記憶體空間,而是将記憶體分為一塊較大的 Eden 空間和兩塊較小的 Survivor 空間,每次使用 Eden和其中一塊Survivor[1]。當回收時,将 Eden 和 Survivor 中還存活着的對象一次性地複制到另外一塊 Survivor 空間上, 最後清理掉 Eden 和剛才用過的 Survivor 空間。

HotSpot 虛拟機預設 Eden 和 Survivor 的大小比例是 8:1,也就是每次新生代中可用記憶體空間為整個新生代容量的 90%(80%+10%),隻有10%的記憶體會被 “浪費”。當然,98%的對象可回收隻是一般場景下的資料,我們沒有辦法保證每次回收都隻有不多于10%的對象存活,當 Survivor 空間不夠用時,需要依賴其他記憶體(這裡指老年代)進行配置設定擔保(Handle Promotion)

深入淺出!如何用一段代碼證明JVM加載類是懶加載模式?看看這篇文章吧!前言垃圾回收算法複制算法标記清除标記整理垃圾回收器CMS(Concurrent Mark Sweep)回收器G1(Garbage First)并發标記三色标記算法總結

标記清除

算法分為“标記”和“清除”兩個階段:首先掃描所有對象标記出需要回收的對象,在标記完成後掃描回收所有被标記的對象,是以需要掃描兩遍。回收效率略低,如果大部分對象是朝生夕死,那麼回收效率降低,因為需要大量标記對象和回收對象,對比複制回收效率要低。

它的主要問題,标記清除之後會産生大量不連續的記憶體碎片,空間碎片太多可能會導緻以後在程式運作過程中需要配置設定較大對象時,無法找到足夠的連續記憶體而不得不提前觸發另一次垃圾回收動作。回收的時候如果需要回收的對象越多,需要做的标記和清除的工作越多,是以标記清除算法适用于老年代。

深入淺出!如何用一段代碼證明JVM加載類是懶加載模式?看看這篇文章吧!前言垃圾回收算法複制算法标記清除标記整理垃圾回收器CMS(Concurrent Mark Sweep)回收器G1(Garbage First)并發标記三色标記算法總結

标記整理

首先标記出所有需要回收的對象,在标記完成後,後續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然後直接清理掉端邊界以外的記憶體。标記整理算法雖然沒有記憶體碎片,但是效率偏低。

我們看到标記整理與标記清除算法的差別主要在于對象的移動。對象移動不單單會加重系統負擔,同時需要全程暫停使用者線程才能進行,同時所有引用對象的地方都需要更新(直接指針需要調整)。是以看到,老年代采用的标記整理算法與标記清除算法,各有優點,各有缺點。

深入淺出!如何用一段代碼證明JVM加載類是懶加載模式?看看這篇文章吧!前言垃圾回收算法複制算法标記清除标記整理垃圾回收器CMS(Concurrent Mark Sweep)回收器G1(Garbage First)并發标記三色标記算法總結

垃圾回收器

回收器名稱回收對象和算法回收器類型Serial新生代,複制算法線程(串行)Parallel Scavenge新生代,複制算法并行的多線程回收器ParNew新生代,複制算法并行的多線程回收器Serial Old老年代,标記整理算法單線程(串行)Parallel Old老年代,标記整理算法并行的多線程回收器CMS老年代,标記清除算法并發的多線程回收器G1新生代,老年代;标記整理 + 化整為零并發的多線程回收器

深入淺出!如何用一段代碼證明JVM加載類是懶加載模式?看看這篇文章吧!前言垃圾回收算法複制算法标記清除标記整理垃圾回收器CMS(Concurrent Mark Sweep)回收器G1(Garbage First)并發标記三色标記算法總結

目前最常用的兩種垃圾回收器,也不用多說,肯定是CMS和G1,一般面試官會問下CMS和G1的差別以及各自的特點,不太會深入問實作原理,畢竟Java面試可問的知識點實在太多了,都一個個深入問1個小時的面試時間根本不夠。

串行的垃圾回收器就不說了,這裡專門講下并發的垃圾回收器

CMS(Concurrent Mark Sweep)回收器

顧名思義,這是并發的垃圾回收器,這種回收器是一種以擷取最短的回收停頓時間為目的的垃圾收集器,目前很大一部分Java的網際網路應用或者B/S系統的伺服器上,由于這類應用尤其在意相應速度,希望系統停頓時間越短越好,這樣使用者體驗也會更好,CMS就非常符合這類應用的需求。

從名字就可以看出,這種回收器是基于标記清除的算法實作,它的運作過程相對串行的垃圾回收器相對複雜點,分為以下4個步驟

初始标記:很短,僅僅隻是标記下GC Root能直接關聯的對象,速度極快。

并發标記:和使用者應用同時進行,進行GC Root跟蹤的過程,标記GC Root開始關聯的所有對象,開始周遊整個可達分析的路徑對象,這個時間比較長,是以并發。

重新标記:短暫,為了修正并發标記期間因使用者程式繼續運作而導緻标記産生變動的那一部分對象的标記記錄,這個階段的停頓時間一般會比初始标 記階段稍長一些,但遠比并發标記的時間短。

并發清除:由于整個過程中耗時最長的并發标記和并發清除過程收集器線程都可以與使用者線程一起工作,是以,一般來說,CMS 的記憶體回收過程是與使用者線程一起執行的。-XX:+UseConcMarkSweepGC ,表示新生代使用ParNew,老年代的用 CMS。

深入淺出!如何用一段代碼證明JVM加載類是懶加載模式?看看這篇文章吧!前言垃圾回收算法複制算法标記清除标記整理垃圾回收器CMS(Concurrent Mark Sweep)回收器G1(Garbage First)并發标記三色标記算法總結

CPU 敏感:CMS 對處理器資源敏感,畢竟采用了并發的收集、當處理核心數不足 4 個時,CMS 對使用者的影響較大。

浮動垃圾:由于 CMS 并發清理階段使用者線程還在運作着,伴随程式運作自然就還會有新的垃圾不斷産生,這一部分垃圾出現在标記過程之後,CMS無法在當次收集中處理掉它們,隻好留待下一次GC時再清理掉。這一部分垃圾就稱為“浮動垃圾”。由于浮動垃圾的存在,是以需要預留出一部分記憶體,意味着 CMS 收集不能像其它收集器那樣等待老年代快滿的時候再回收。在1.6的版本中老年代空間使用率門檻值(92%)如果預留的記憶體不夠存放浮動垃圾,就會出現 Concurrent Mode Failure,這時虛拟機将臨時啟用 Serial Old 來替代 CMS。

會産生空間碎片:标記 - 清除算法會導緻産生不連續的空間碎片總體來說,CMS是JVM 推出了第一款并發垃圾收集器,是以還是非常有代表性。但是最大的問題是 CMS 采用了标記清除算法,是以會有記憶體碎片,當碎片較多時,給大對象的配置設定帶來很大的麻煩,為了解決這個問題,CMS 提供一個 參數:-XX:+UseCMSCompactAtFullCollection,一般是開啟的,如果配置設定不了大對象,就進行記憶體碎片的整理過程。這個地方一般會使用 Serial Old ,因為 Serial Old 是一個單線程,是以如果記憶體空間很大、且對象較多時,CMS 發生這樣情況會很卡。

總結:CMS 問題比較多,是以JDK沒有一個版本預設垃圾回收器是CMS,隻能手動指定。但是它畢竟是第一個并發垃圾回收器,對于了解并發垃圾回收具有一定意義,是以我們必須了解。為什麼 CMS 采用标記-清除,在實作并發的垃圾回收時,如果采用标記整理算法,那麼還涉及到對象的移動(對象的移動必定涉及到引用的變化,這個需要暫停業務線程來處理棧資訊,這樣使得并發收集的暫停時間更長),是以使用簡單的标記-清除算法才可以降低 CMS的STW的時間。

該垃圾回收器适合回收堆空間幾個 G至20G。

G1(Garbage First)

随着JVM記憶體的增大,STW的時間成為JVM 急迫解決的問題,但是如果按照傳統的分代模型,總跳不出STW時間不可預測這點。

為了實作STW的時間可預測,首先要有一個思想上的改變。

G1将堆記憶體“化整為零”,将堆記憶體劃分成多個大小相等獨立區域(Region),每一個Region 都可以根據需要,扮演新生代的Eden空間、Survivor空間,或者老年代空間。

回收器能夠對扮演不同角色的 Region 采用不同的政策去處理,這樣無論是新建立的對象還是已經存活了一段時間、熬過多次收集的舊對象都能擷取很好的收集效果。

Region:Region可能是Eden,也有可能是Survivor,也有可能是Old,另外 Region 中還有一類特殊的Humongous區域,專門用來存儲大對象。G1認為隻要大小超過了一個Region容量一半的對象即可判定為大對象。每個Region的大小可以通過參數-XX:G1HeapRegionSize 設定,取值範圍為 1MB至32MB,且應為2的N次幂。而對于那些超過了整個 Region 容量的超級大對象,将會被存放在 N 個連續的 Humongous Region 之中,G1 的進行回收大多數情況下都把 Humongous Region 作為老年代的一部分來進行看待。

開啟參數 -XX:+UseG1GC分區大小 -XX:+G1HeapRegionSize一般建議逐漸增大該值,随着 size 增加,垃圾的存活時間更長,GC 間隔更長,但每次 GC 的時間也會更長。

最大GC暫停時間 -XX:MaxGCPauseMillis設定最大GC暫停時間的目标(機關毫秒),這是個軟目标,JVM會盡最大可能實作它。

深入淺出!如何用一段代碼證明JVM加載類是懶加載模式?看看這篇文章吧!前言垃圾回收算法複制算法标記清除标記整理垃圾回收器CMS(Concurrent Mark Sweep)回收器G1(Garbage First)并發标記三色标記算法總結

運作過程如下:

初始标記:僅僅隻是标記一下GC Roots能直接關聯到的對象,并且修改 TAMS 指針的值,讓下一階段使用者線程并發運作時,能正确地在可用的 Region 中配置設定新對象。這個階段需要停頓線程,但耗時很短,而且是借用進行Minor GC的時候同步完成的,是以G1收集器在這個階段實際并沒有額外的停頓。要達到GC與使用者線程并發運作,必須要解決回收過程中新對象的配置設定,是以G1為每一個Region 區域設計了兩個名為TAMS(Top at Mark Start)的指針,從 Region 區域劃出一部分空間用于記錄并發回收過程中的新對象。這樣的對象認為它們是存活的,不納入垃圾回收範圍。

并發标記:從GC Root開始對堆中對象進行可達性分析,遞歸掃描整個堆裡的對象圖,找出要回收的對象,這階段耗時較長,但可與使用者程式并發執行。當對象圖掃描完成以後,并發時有引用變動的對象,這些對象會漏标,漏标的對象會被一個叫做SATB(snapshot at the beginning)算法來解決。

最終标記:對使用者線程做另一個短暫的暫停,用于處理并發階段結後仍遺留下來的最後那少量的 SATB 記錄(漏标對象)。

篩選回收:負責更新Region的統計資料,對各個Region的回收價值和成本進行排序,根據使用者所期望的停頓時間來制定回收計劃,可以自由選擇任意多個Region構成回收集,然後把決定回收的那一部分 Region 的存活對象複制到空的Region中,再清理掉整個舊 Region 的全部空間。這裡的操作涉及存活對象的移動,是必須暫停使用者線程,由多條收集器線程并行完成的。

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

分代收集:與其他收集器一樣,分代概念在 G1 中依然得以保留。雖然 G1 可以不需要其他收集器配合就能獨立管理整個 GC 堆,但它能夠采用不同的方式 去處理新建立的對象和已經存活了一段時間、熬過多次 GC 的舊對象以擷取更好的收集效果。

空間整合:與 CMS 的“标記—清理”算法不同,G1 從整體來看是基于“标記—整理”算法實作的收集器,從局部(兩個 Region 之間)上來看是基于“複 制”算法實作的,但無論如何,這兩種算法都意味着 G1 運作期間不會産生記憶體空間碎片,收集後能提供規整的可用記憶體。這種特性有利于程式長時間運 行,配置設定大對象時不會因為無法找到連續記憶體空間而提前觸發下一次 GC。

追求停頓時間:-XX:MaxGCPauseMillis 指定目标的最大停頓時間,G1 嘗試調整新生代和老年代的比例,堆大小,晉升年齡來達到這個目标時間。

并發标記

三色标記算法

說到并發标記,就不能不提下并發标記中的三色标記算法,它是一種描述追蹤式回收器的有效的辦法,利用它可以推演回收器的正确性。

在三色标記法之前有一個算法叫 Mark-And-Sweep(标記清除)。這個算法會設定一個标志位來記錄對象是否被使用。最開始所有的标記位都是0,如果發現對象是可達的就會置為1,一步步下去就會呈現一個類似樹狀的結果。等标記的步驟完成後,會将未被标記的對象統一清理,再次把所有的标記位 設定成0友善下次清理。

這個算法最大的問題是 GC 執行期間需要把整個程式完全暫停,不能異步進行 GC 操作。因為在不同階段标記清掃法的标志位0和1有不同的含義,那麼新增的對象無論标記為什麼都有可能意外删除這個對象。對實時性要求高的系統來說,這種需要長時間挂起的标記清掃法是不可接受的。是以就需要一個算法來解決 GC 運作時程式長時間挂起的問題,那就三色标記法。三色标記最大的好處是可以異步執行,進而可以以中斷時間極少的代價或者完全沒有中斷來進行整個GC。

我們将對象分為三種類型:

黑色:根對象,或者該對象與它的子對象都被掃描過。

灰色:對本身被掃描,但是還沒掃描完該對象的子對象。

白色:未被掃描對象,如果掃描完所有對象之後,最終為白色的為不可達對象,既垃圾對象。

深入淺出!如何用一段代碼證明JVM加載類是懶加載模式?看看這篇文章吧!前言垃圾回收算法複制算法标記清除标記整理垃圾回收器CMS(Concurrent Mark Sweep)回收器G1(Garbage First)并發标記三色标記算法總結

總結

就寫到這了,也算是給這段時間的面試做一個總結,查漏補缺,祝自己好運吧,也希望正在求職或者打算跳槽的 程式員看到這個文章能有一點點幫助或收獲,我就心滿意足了。多思考,多問為什麼。希望小夥伴們早點收到滿意的offer! 越努力越幸運!

金九銀十已經過了,就目前國内的面試模式來講,在面試前積極的準備面試,複習整個 Java 知識體系将變得非常重要,可以很負責任的說一句,複習準備的是否充分,将直接影響你入職的成功率。但很多小夥伴卻苦于沒有合适的資料來回顧整個 Java 知識體系,或者有的小夥伴可能都不知道該從哪裡開始複習。我偶然得到一份整理的資料,不論是從整個 Java 知識體系,還是從面試的角度來看,都是一份含技術量很高的資料。

感興趣的朋友可以點選這裡獲得免費領取!

深入淺出!如何用一段代碼證明JVM加載類是懶加載模式?看看這篇文章吧!前言垃圾回收算法複制算法标記清除标記整理垃圾回收器CMS(Concurrent Mark Sweep)回收器G1(Garbage First)并發标記三色标記算法總結

識體系,還是從面試的角度來看,都是一份含技術量很高的資料。**

感興趣的朋友可以點選這裡獲得免費領取!

[外鍊圖檔轉存中…(img-upkIp2bL-1621328143251)]