位元組跳動面試官:Java多程序從頭講到尾
垃圾回收演算法
垃圾回收演算法的實現設計到大量的程式細節,並且每一個平臺的虛擬機器操作記憶體的方式都有不同,所以不需要去了解演算法的具體實現。
複製演算法
將可用記憶體按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的記憶體用完了,就將還存活著的物件複製到另外一塊上面,然後再把已使用過的記憶體空間一次清理掉。這樣使得每次都是對整個半區進行記憶體回收,記憶體分配時也就不用考慮記憶體碎片等複雜情況,只要按順序分配記憶體即可,實現簡單,執行高效。
只是這種演算法的代價是將記憶體縮小為了原來的一半。但是要注意:記憶體移動是必須實打實的移動(複製),所以對應的引用(直接指標)需要調整。
複製回收演算法適合於新生代,因為大部分物件朝生夕死,那麼複製過去的物件比較少,效率自然就高,另外一半的一次性清理是很快的。
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個小時的面試時間根本不夠。
序列的垃圾回收器就不說了,這裡專門講下併發的垃圾回收器
CMS(Concurrent Mark Sweep)回收器
顧名思義,這是併發的垃圾回收器,這種回收器是一種以獲取最短的回收停頓時間為目的的垃圾收集器,目前很大一部分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。
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會盡最大可能實現它。
執行過程如下:
初始標記:僅僅只是標記一下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。
我們將物件分為三種類型:
黑色:根物件,或者該物件與它的子物件都被掃描過。
灰色:對本身被掃描,但是還沒掃描完該物件的子物件。
白色:未被掃描物件,如果掃描完所有物件之後,最終為白色的為不可達物件,既垃圾物件。
最後
面試題文件來啦,內容很多,485頁!
由於筆記的內容太多,沒辦法全部展示出來,下面只擷取部分內容展示。有想獲取完整版筆記的朋友,點贊後點擊這裡免費領取哦
1111道Java工程師必問面試題
MyBatis 27題 + ZooKeeper 25題 + Dubbo 30題:
Elasticsearch 24 題 +Memcached +?Redis 40題:
Spring 26 題+ 微服務 27題+ Linux 45題:
Java面試題合集: