有情懷,有乾貨,微信搜索【 三太子敖丙】關注這個不同的程序員。本文 GitHub https://github.com/JavaFamily 已收錄,有一線大廠面試完整考點、資料以及個人系列文章。git
你們在面試的時候不一樣程度會被問到JVM的垃圾回收,看面試官水平,有些就背個書就行,好比GC的工做原理,有哪些GC算法和回收器,分別優勢和缺點等等,有些面試官估計本身也就背書水平,都沒個追問;有些面試官就能追問,一追問就歇菜,好比低延遲的垃圾回收器有哪些以及其原理,跨代引用及解決方案,三色標記及漏標問題處理,等等。程序員
仍是那句話,雖然都是些理論的問題,可是在實際開發過程當中真的能遇到這些問題來解決實際問題,因此多多瞭解JVM的實現原理總沒有錯,既能抗極限面試,又能在適時的時候幫忙解決實際問題,獲得領導和同事的讚揚,何樂不爲?github
下面進入正題,先來個開胃菜,熱熱身。GC的工做原理就不說了,要準備面試的同窗必須滾瓜爛熟,否則面試官要說出門右轉了...面試
垃圾回收算法的實現設計到大量的程序細節,而且每個平臺的虛擬機操做內存的方式都有不一樣,因此不須要去了解算法的具體實現。算法
將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活着的對象複製到另一塊上面,而後再把已使用過的內存空間一次清理掉。這樣使得每次都是對整個半區進行內存回收,內存分配時也就不用考慮內存碎片等複雜狀況,只要按順序分配內存便可,實現簡單,運行高效。服務器
只是這種算法的代價是將內存縮小爲了原來的一半。可是要注意:內存移動是必須實打實的移動(複製),因此對應的引用(直接指針)須要調整。微信
複製回收算法適合於新生代,由於大部分對象朝生夕死,那麼複製過去的對象比較少,效率天然就高,另一半的一次性清理是很快的。多線程
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)
算法分爲「標記」和「清除」兩個階段:首先掃描全部對象標記出須要回收的對象,在標記完成後掃描回收全部被標記的對象,因此須要掃描兩遍。回收效率略低,若是大部分對象是朝生夕死,那麼回收效率下降,由於須要大量標記對象和回收對象,對比複製回收效率要低。
它的主要問題,標記清除以後會產生大量不連續的內存碎片,空間碎片太多可能會致使之後在程序運行過程當中須要分配較大對象時,沒法找到足夠的連續內存而不得不提早觸發另外一次垃圾回收動做。回收的時候若是須要回收的對象越多,須要作的標記和清除的工做越多,因此標記清除算法適用於老年代。
首先標記出全部須要回收的對象,在標記完成後,後續步驟不是直接對可回收對象進行清理,而是讓全部存活的對象都向一端移動,而後直接清理掉端邊界之外的內存。標記整理算法雖然沒有內存碎片,可是效率偏低。
咱們看到標記整理與標記清除算法的區別主要在於對象的移動。對象移動不僅僅會加劇系統負擔,同時須要全程暫停用戶線程才能進行,同時全部引用對象的地方都須要更新(直接指針須要調整)。因此看到,老年代採用的標記整理算法與標記清除算法,各有優勢,各有缺點。
回收器名稱 | 回收對象和算法 | 回收器類型 |
---|---|---|
Serial | 新生代,複製算法 | 線程(串行) |
Parallel Scavenge | 新生代,複製算法 | 並行的多線程回收器 |
ParNew | 新生代,複製算法 | 並行的多線程回收器 |
Serial Old | 老年代,標記整理算法 | 單線程(串行) |
Parallel Old | 老年代,標記整理算法 | 並行的多線程回收器 |
CMS | 老年代,標記清除算法 | 併發的多線程回收器 |
G1 | 新生代,老年代;標記整理 + 化整爲零 | 併發的多線程回收器 |
目前最經常使用的兩種垃圾回收器,也不用多說,確定是CMS和G1,通常面試官會問下CMS和G1的區別以及各自的特色,不太會深刻問實現原理,畢竟Java面試可問的知識點實在太多了,都一個個深刻問1個小時的面試時間根本不夠。
串行的垃圾回收器就不說了,這裏專門講下併發的垃圾回收器
顧名思義,這是併發的垃圾回收器,這種回收器是一種以獲取最短的回收停頓時間爲目的的垃圾收集器,目前很大一部分Java的互聯網應用或者B/S系統的服務器上,因爲這類應用尤爲在乎相應速度,但願系統停頓時間越短越好,這樣用戶體驗也會更好,CMS就很是符合這類應用的需求。
從名字就能夠看出,這種回收器是基於標記清除的算法實現,它的運做過程相對串行的垃圾回收器相對複雜點,分爲如下4個步驟
初始標記:很短,僅僅只是標記下GC Root能直接關聯的對象,速度極快。
併發標記:和用戶應用同時進行,進行GC Root跟蹤的過程,標記GC Root開始關聯的全部對象,開始遍歷整個可達分析的路徑對象,這個時間比較長,因此併發。
從新標記:短暫,爲了修正併發標記期間因用戶程序繼續運做而致使標記產生變更的那一部分對象的標記記錄,這個階段的停頓時間通常會比初始標 記階段稍長一些,但遠比並發標記的時間短。
併發清除:因爲整個過程當中耗時最長的併發標記和併發清除過程收集器線程均可以與用戶線程一塊兒工做,因此,通常來講,CMS 的內存回收過程是與用戶線程一塊兒執行的。 -XX:+UseConcMarkSweepGC ,表示新生代使用ParNew,老年代的用 CMS。
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。
隨着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會盡最大可能實現它。
運行過程以下:
初始標記:僅僅只是標記一下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。
咱們將對象分爲三種類型:
黑色:根對象,或者該對象與它的子對象都被掃描過。
灰色:對自己被掃描,可是還沒掃描完該對象的子對象。
白色:未被掃描對象,若是掃描完全部對象以後,最終爲白色的爲不可達對象,既垃圾對象。
以上圖爲例,簡單說下三色標記的實現原理,首先如上圖所示,線程1已完成全部標記,全部對象都被標記成黑色;線程2還處於半完成狀態,其中對象B自己已被掃描,可是尚未掃描該對象的子對象。
因爲垃圾回收的線程和正常業務線程都在執行中,並無中斷,若是此時業務代碼以下:
A.c = C
B.c = null
則以下圖
此時線程1因爲已經完成全部掃描,則對象C被遺漏標記爲黑色;而線程2完成掃描,將對象B標記爲黑色,線程2此時也完成全部掃描,問題就來了,對象C被漏標了。
對象C被漏標的直接後果就是被回收。然而它的確還須要被對象A引用,這就是三色標記中的漏標問題。
如何解決併發標記中的漏標問題?
Incremental Update 增量更新算法:
當一個白色對象被一個黑色對象引用,將黑色對象從新標記爲灰色,讓垃圾回收器從新掃描。
STAB(snapshot at the beginning)算法:
開始作一個快照,當B引用C的關係消失的時候要把這個引用推到GC的堆棧中,保證C還能被GC掃描到,最重要的是要把這個引用推到GC的堆棧,是灰色對象指向白色的引用,若是一旦某一個引用消失掉了,就會把它放到棧(GC方法運行時數據也是來自棧中),JVM其實仍是能找到它的,下回直接掃描它就好了,那樣白色就不會漏標。
對應 G1 的垃圾回收過程當中的: 最終標記( Final Marking)對用戶線程作另外一個短暫的暫停,用於處理併發階段結後仍遺留下來的最後那少許的 SATB 記錄(漏標對象)。
SATB 算法是關注引用的刪除。(B對C 的引用)
Incremental Update 算法關注引用的增長。(A->C 的引用)
G1 若是使用 Incremental Update 算法,由於變成灰色的成員還要從新掃,從新再來一遍,效率過低了。 因此 G1 在處理併發標記的過程比 CMS 效率要高,這個主要是解決漏標的算法決定的。
目前各大公司基本都使用CMS或者G1做爲服務的垃圾回收器,可是在使用過程當中若是出現一些奇怪的問題,有時候重現還特別難,考慮下這些垃圾回收器的底層實現原理,沒有百分之一百的完美方案,只有最適合本身業務場景的方案。但願你們在平常工做中多多思考,遇到問題才能迎刃而解。
我是敖丙,你知道的越多,你不知道的越多,感謝各位人才的:點贊、收藏和評論,咱們下期見!
文章持續更新,能夠微信搜一搜「 三太子敖丙 」第一時間閱讀,回覆【 資料】有我準備的一線大廠面試資料和簡歷模板,本文 GitHub https://github.com/JavaFamily 已經收錄,有大廠面試完整考點,歡迎Star。