1 垃圾回收的理論依據(jù)
當(dāng)前大部分的垃圾收集器都遵循著 “分代收集” (Generational Collection) 的理論進(jìn)行設(shè)計(jì)的, 建立在 2 個(gè)分代假設(shè)之上
- 弱分代假說 (Weak Generational Hypothesis): 絕大多數(shù)對(duì)象都是朝生夕滅的
- 強(qiáng)分代假說 (Strong Generational Hypothesis): 熬過越多次垃圾收集過程的對(duì)象就越難以消亡
根據(jù)這 2 個(gè)假說, 收集器將 Java 堆劃分出不同的區(qū)域, 然后將回收對(duì)象依據(jù)其年齡 (年齡即對(duì)象熬過垃圾收集過程的次數(shù)) 分配到不同的區(qū)域之中存儲(chǔ)。
現(xiàn)在主流的 Java 虛擬機(jī)實(shí)現(xiàn)通常將 Java 堆分為 2 個(gè)區(qū)域:
- 新生代 (Young Generation)
- 老年代 (Old Generation)
分代收集存在的一個(gè)問題: 新生代的對(duì)象有可能被老年代所引用, 為了確保完整的存活對(duì)象, 除了在固定的 GC Roots 之外, 還需要額外遍歷整個(gè)老年代中所有
對(duì)象來(lái)確保可達(dá)性分析結(jié)果的正確性 (同樣, 老年代也可能被新生代所引用)?;谶@個(gè)問題, 有了第三條假設(shè)
- 跨代引用假說 (Intergenerational Reference Hypothesis): 跨代引用相對(duì)于同代引用來(lái)說僅占極少數(shù)
依據(jù)這條假說, 我們就不應(yīng)再為了少量的跨代引用去掃描整個(gè)老年代, 也不必浪費(fèi)空間專門記錄每一個(gè)對(duì)象是否存在及存在哪些跨代引用, 只需在新生代上建立
一個(gè)全局的數(shù)據(jù)結(jié)構(gòu) (該結(jié)構(gòu)被稱為 “記憶集”, Remembered Set), 這個(gè)結(jié)構(gòu)把老年代劃分成若干小塊, 標(biāo)識(shí)出老年代的哪一塊內(nèi)存會(huì)存在跨代引用。此后
當(dāng)發(fā)生 Minor GC 時(shí), 只有包含了跨代引用的小塊內(nèi)存里的對(duì)象才會(huì)被加入到 GC Roots 進(jìn)行掃描。雖然這種方法需要在對(duì)象改變引用關(guān)系時(shí) (如將自己或者某
個(gè)屬性賦值維護(hù)), 記錄數(shù)據(jù)的正確性, 這會(huì)增加一些運(yùn)行時(shí)的開銷, 但比起收集時(shí)掃描整個(gè)老年代來(lái)說仍然是劃算的。
1.1 記憶集 (Remembered Set)
記憶集是一種 “抽象” 的數(shù)據(jù)結(jié)構(gòu), 只定義了記憶集的行為意圖, 并沒有定義其行為的具體實(shí)現(xiàn)。
而現(xiàn)在常用的的具體實(shí)現(xiàn)為 “卡表”(Card Table), 2 者的關(guān)系類似于 Map 和 HashMap 的關(guān)系。
卡表最簡(jiǎn)單的形式可以只是一個(gè)字節(jié)數(shù)組。
CARD_TABLE [this address >> 9] = 0;
字節(jié)數(shù)組 CARD_TABLE 的每一個(gè)元素都對(duì)應(yīng)著其標(biāo)識(shí)的內(nèi)存區(qū)域中一塊特定大小的內(nèi)存塊, 這個(gè)內(nèi)存塊被稱作 “卡頁(yè)”(Card Page)。
一般來(lái)說, 卡頁(yè)大小都是以 2 的 N 次冪的字節(jié)數(shù), HotSpot 默認(rèn)為 512 字節(jié)。
如果卡表標(biāo)識(shí)內(nèi)存區(qū)域的起始地址是 0x0000 的話, 數(shù)組 CARD_TABLE 的第 0, 1, 2 號(hào)元素分別對(duì)應(yīng)了地址范圍為
0x0000~0x01FF, 0x0200~0x03FF, 0x0400~0x05FF 的卡頁(yè)內(nèi)存塊。
一個(gè)卡頁(yè)的內(nèi)存中通常包含不止一個(gè)對(duì)象, 只要卡頁(yè)內(nèi)有一個(gè)或更多對(duì)象的字段存在著跨代指針, 那就將對(duì)應(yīng)卡表的數(shù)組元素的值標(biāo)識(shí)為 1, 稱為這個(gè)元素變
臟 (Dirty), 沒有則標(biāo)識(shí)為 0。
在垃圾收集發(fā)生時(shí), 只要篩選出卡表中變臟的元素, 就能輕易得出哪些卡頁(yè)內(nèi)存塊中包含跨代指針, 把它們加入 GC Roots 中一并掃描。
注意這里是把這個(gè)區(qū)域內(nèi)的所有對(duì)象都加入的。
1.1.1 記憶集的維護(hù)
在 HotSpot 虛擬機(jī)里是通過寫屏障 (Write Barrier) 技術(shù)維護(hù)卡表狀態(tài)的 (這里的寫屏障不是解決并發(fā)的讀寫屏障), 看作在虛擬機(jī)層面對(duì) “引用類型字
段賦值” 這個(gè)動(dòng)作的 AOP 切面, 在引用對(duì)象賦值時(shí)會(huì)產(chǎn)生一個(gè)環(huán)形 (Around) 通知, 供程序執(zhí)行額外的動(dòng)作, 也就是說賦值的前后都在寫屏障的覆蓋范疇內(nèi)。
在賦值前的部分的寫屏障叫作寫前屏障 (Pre-Write Barrier), 在賦值后的則叫作寫后屏障 (Post-Write Barrier)。
HotSpot 虛擬機(jī)只用到了寫后屏障。
void oop_field_store(oop* field, oop new_value) {
// 引用字段賦值操作
*field = new_value;
// 寫后屏障, 在這里完成卡表狀態(tài)更新
post_write_barrier(field, new_value);
}
通過寫屏障后, 虛擬機(jī)就可以為所有賦值操作生成相應(yīng)的指令。
但是每個(gè)引用類型的賦值都會(huì)觸發(fā)更新卡表操作, 無(wú)論更新的是不是老年代對(duì)新生代對(duì)象的引用, 都會(huì)產(chǎn)生額外的開銷。
不過這個(gè)開銷與 Minor GC 時(shí)掃描整個(gè)老年代的代價(jià)相比還是低得多的。
除了寫屏障的開銷外, 卡表在高并發(fā)場(chǎng)景下還面臨著 “偽共享” (False Sharing) 問題。
什么是偽共享可以看這里。
為了避免偽共享問題, 一種簡(jiǎn)單的解決方案是不采用無(wú)條件的寫屏障, 而是先檢查卡表標(biāo)記, 只有當(dāng)該卡表元素未被標(biāo)記過時(shí)才將其標(biāo)記為變臟, 即將卡表更新的
邏輯變?yōu)橐韵麓a所示:
if (CARD_TABLE [this address >> 9] != 0)
CARD_TABLE [this address >> 9] = 0;
在 JDK 7 之后, HotSpot 虛擬機(jī)增加了一個(gè)新的參數(shù) -XX: +UseCondCardMark, 用來(lái)決定是否開啟卡表更新的條件判斷。
開啟會(huì)增加一次額外判斷的開銷, 但能夠避免偽共享問題, 兩者各有性能損耗, 是否打開要根據(jù)應(yīng)用實(shí)際運(yùn)行情況來(lái)進(jìn)行測(cè)試權(quán)衡。
2 常用的垃圾回收算法
2.1 標(biāo)記-清除算法
先標(biāo)記所有需要回收的對(duì)象, 標(biāo)記完成后, 統(tǒng)一回收所有標(biāo)記的對(duì)象 (也可以反過來(lái), 標(biāo)記存活的對(duì)象, 回收未標(biāo)記的對(duì)象)。
標(biāo)記的依據(jù)通過可達(dá)性分析法。
存在 2 個(gè)問題:
- 執(zhí)行效率不穩(wěn)定, 標(biāo)記和清除的過程會(huì)隨著 Java 堆中的對(duì)象增多而變長(zhǎng)
- 內(nèi)存空間碎片化, 回收完成后, 會(huì)產(chǎn)生大量不連續(xù)的內(nèi)存碎片, 空間碎片太多的話, 可能會(huì)導(dǎo)致后續(xù)大對(duì)象的分配找不到足夠的連續(xù)內(nèi)存
2.2 標(biāo)記-復(fù)制算法
為了解決標(biāo)記-清除算法 面對(duì)大量可回收對(duì)象時(shí)執(zhí)行效率低的問題。
將可用內(nèi)存按容量劃分為大小相等的兩塊, 每次只使用其中的一塊。
當(dāng)這一塊的內(nèi)存用完了, 就將還存活著的對(duì)象復(fù)制到另外一塊上面, 然后再把已使用過的內(nèi)存空間一次清理掉。
如果內(nèi)存中多數(shù)對(duì)象都是存活的, 這種算法將會(huì)產(chǎn)生大量的內(nèi)存間復(fù)制的開銷, 但對(duì)于多數(shù)對(duì)象都是可回收的情況, 算法需要復(fù)制的就是占少數(shù)的存活對(duì)象, 而
且每次都是針對(duì)整個(gè)半?yún)^(qū)進(jìn)行內(nèi)存回收, 分配內(nèi)存時(shí)也就不用考慮有空間碎片的復(fù)雜情況, 只要移動(dòng)堆頂指針, 按順序分配即可。
這樣實(shí)現(xiàn)簡(jiǎn)單, 運(yùn)行高效。
最大的缺點(diǎn): 是將可用內(nèi)存縮小為了原來(lái)的一半, 空間浪費(fèi)未免太多了。
針對(duì)空間浪費(fèi)大的問題, 有一種更優(yōu)化的半?yún)^(qū)復(fù)制分代策略 – Appel 式回收 (HotSpot 采用的就是這種策略)。
Appel 式回收
- 將新生代劃分為一塊較大的 Eden 區(qū)域 + 兩塊較小的 Survivor 空間
- 每次分配內(nèi)存只使用 Eden 和其中一塊 Survivor
- 發(fā)生垃圾回收時(shí), 將 Eden 和 Survivor 中存活的對(duì)象一次性復(fù)制到另外一塊 Survivor 上, 然后把 Eden 和 已使用過的那塊 Survivor 空間清理掉
HotSpot 默認(rèn) Eden 和 Survivor 的比例是 8:1:1, 也就是每次新生代中可使用的內(nèi)存占總量的 90%。
當(dāng)然, 可能一次垃圾回收時(shí), 10 % 的 Survivor 的區(qū)域無(wú)法存放存活的對(duì)象了, Appel 式回收會(huì)通過分配擔(dān)保 (Handle Promotion), 將這些對(duì)象直接
放入老年代。
當(dāng)一個(gè)對(duì)象進(jìn)入到 Survivor 時(shí), 他的年齡將會(huì) + 1, 后續(xù)在 2 個(gè) Survivor 區(qū)來(lái)回拷貝時(shí), 每拷貝一次, 年齡就 + 1, 當(dāng)年齡達(dá)到了 15 (HotSpot
默認(rèn)的配置), 這個(gè)對(duì)象就會(huì)被移入到老年代。
2.3 標(biāo)記-整理算法
標(biāo)記-復(fù)制算法在對(duì)象存活率較高時(shí)就要進(jìn)行較多的復(fù)制操作, 效率將會(huì)降低。
同時(shí)一定會(huì)有空間的浪費(fèi), 所以老年代一般都不會(huì)選用這種算法。
針對(duì)老年代的特點(diǎn), 有一種針對(duì)性的標(biāo)記-整理算法, 同樣的先通過標(biāo)記, 確定對(duì)象是否可回收, 然后讓所有存活的對(duì)象都向內(nèi)存空間的一端移動(dòng), 然后清
理掉邊界以外的內(nèi)存。這種移動(dòng)式的算法是一項(xiàng)優(yōu)缺點(diǎn)并存的風(fēng)險(xiǎn)決策。
如果移動(dòng)存活對(duì)象, 尤其是在老年代這種每次回收都有大量對(duì)象存活區(qū)域, 移動(dòng)存活對(duì)象并更新所有引用這些對(duì)象的地方將會(huì)是一種極為負(fù)重的操作, 而且這種
對(duì)象移動(dòng)操作必須全程暫停用戶應(yīng)用程序才能進(jìn)行。
如果不移動(dòng)對(duì)象, 則會(huì)有空間碎片, 這個(gè)問題就只能依賴更為復(fù)雜的內(nèi)存分配器和內(nèi)存訪問器來(lái)解決。
基于以上兩點(diǎn), 是否移動(dòng)對(duì)象都存在弊端, 移動(dòng)則內(nèi)存回收時(shí)會(huì)更復(fù)雜, 不移動(dòng)則內(nèi)存分配時(shí)會(huì)更復(fù)雜。
但是從垃圾收集的停頓時(shí)間來(lái)看, 不移動(dòng)對(duì)象停頓時(shí)間會(huì)更短, 甚至可以不需要停頓,
但是從整個(gè)程序的性能來(lái)看, 移動(dòng)對(duì)象會(huì)更劃算 (因?yàn)閮?nèi)存分配和訪問相比垃圾收集頻率要高得多, 這部分的耗時(shí)增加, 最終可能導(dǎo)致性能的下降)。
HotSpot 里面的 Parallel Scavenge 收集器是基于標(biāo)記-整理算法的, 而 CMS 收集器是基于標(biāo)記-清除 + 標(biāo)記-清除算法 2 種算法共同協(xié)作。
CMS 的實(shí)現(xiàn): 平時(shí)多數(shù)時(shí)間都采用標(biāo)記-清除算法, 暫時(shí)容忍內(nèi)存碎片的存在, 直到內(nèi)存空間的碎片化程度已經(jīng)大到影響對(duì)象分配時(shí), 再采用標(biāo)記-整理算法收集
一次, 以獲得規(guī)整的內(nèi)存空間。
3 經(jīng)典垃圾收集器
如圖, 展示了 7 款 HotSpot 常用的收集器, 收集器所處的區(qū)域, 表示了他屬于哪個(gè)分代的收集器。
連線表示 2 個(gè)收集器可以搭配使用 (注: JDK8 將 Serial + CMS 和 ParNew + Serial Old 聲明為廢棄, 并在 JDK9 中完全取消了這 2 個(gè)組合的支持)。
3.1 Serial 串行收集器-復(fù)制算法
Serial 是一個(gè)單線程工作的收集器。
它的 “單線程” 的意義不是指只會(huì)使用一個(gè)處理器或一條收集線程去完成垃圾收集工作, 而是強(qiáng)調(diào)在它進(jìn)行垃圾收集時(shí), 必須暫停其他所有工作線程, 直到它收集結(jié)束。
流程大體是這樣的:
優(yōu)點(diǎn): 簡(jiǎn)單高效, 內(nèi)存消耗 (Memory Footprint) 最小的。在 JVM 的 Client 模式下表現(xiàn)優(yōu)異 (Client 模式下內(nèi)存較小, CPU 較少, 能減少許多線程交互的開銷)。
缺點(diǎn): 回收工作需要 Stop The World, 不適用虛擬機(jī) Server 模式 (Server 模式下內(nèi)存較大, CPU 較多, 導(dǎo)致回收工作停頓時(shí)間過長(zhǎng))。
3.2 ParNew 并行收集器-復(fù)制算法
ParNew 收集器實(shí)質(zhì)上是 Serial 收集器的多線程并行版本, 除了同時(shí)使用多條線程進(jìn)行垃圾收集之外, 其他的行為, 調(diào)優(yōu)參數(shù)都和 Serial 一樣。
流程大體是這樣的:
優(yōu)點(diǎn): 多線程工作, 效率更高
缺點(diǎn): 回收工作需要 Stop The World, 只能和 CMS 收集器搭配使用。
3.3 Parallel Scavenge 并行收集器-復(fù)制算法
Parallel Scavenge 又稱為吞吐量?jī)?yōu)先收集器, 是 Java1.8 默認(rèn)的收集器, 特點(diǎn)是并行的多線程回收, 以吞吐量?jī)?yōu)先。
流程大體是這樣的:
Parallel Scavenge 收集器的關(guān)注點(diǎn)與其他收集器不同, CMS 等收集器的關(guān)注點(diǎn)是盡可能地縮短垃圾收集時(shí), 用戶線程的停頓時(shí)間 (響應(yīng)時(shí)間優(yōu)先)。
Parallel Scavenge 收集器的目標(biāo)則是達(dá)到一個(gè)可控制的吞吐量 (Throughput) (吞吐量?jī)?yōu)先)。
吞吐量 = 運(yùn)行用戶代碼時(shí)間 / 運(yùn)行用戶代碼時(shí)間 + 垃圾收集時(shí)間。
響應(yīng)時(shí)間優(yōu)先: 注重的是垃圾回收時(shí) STW 的時(shí)間最短
吞吐量?jī)?yōu)先: 讓單位時(shí)間內(nèi) STW 的時(shí)間最短
個(gè)人認(rèn)為: 就是每隔多少時(shí)間就進(jìn)行一次收集??梢酝ㄟ^
-XX:MaxGCPauseMillis 設(shè)置最大垃圾收集停頓時(shí)間
-XX:MaxGCPauseMillis 設(shè)置垃圾收集時(shí)間占總時(shí)間的比例
優(yōu)點(diǎn): 多線程工作, 注重系統(tǒng)吞吐量和 CPU 資源, 自適應(yīng)調(diào)節(jié)策略
缺點(diǎn): 回收工作需要 Stop The World;
3.4 Serial Old 串行收集器-標(biāo)記整理算法
Serial Old 是 Serial 收集器的老年代版本, 它同樣是一個(gè)單線程收集器, 使用標(biāo)記-整理算法。
主要是供 Client 模式下的 HotSpot 虛擬機(jī)使用。如果在 Server 模式下的話, 可能是
- 在 JDK5 及之前的版本中和 Parallel Scavenge 收集器搭配使用
- 作為 CMS 收集器發(fā)生失敗時(shí)的后備預(yù)案, 在并發(fā)收集發(fā)生 Concurrent Mode Failure 時(shí)使用。
流程大體是這樣的:
優(yōu)點(diǎn): 虛擬機(jī) Client 模式下表現(xiàn)尚可, CMS 收集器的后備預(yù)案 (在并發(fā)收集 Concurrent Mode Failure 時(shí)使用)
缺點(diǎn): 回收工作需要 Stop The World, 單線程
3.5 Parallel Old 并行收集器-標(biāo)記整理算法
Parallel Old 是 Parallel Scavenge 收集器的老年代版本, 支持多線程并發(fā)收集, 使用標(biāo)記-整理算法。
在注重吞吐量或者處理器資源較為稀缺的場(chǎng)合, 都可以優(yōu)先考慮 Parallel Scavenge 加 Parallel Old 收集器這個(gè)組合。
流程大體是這樣的:
優(yōu)點(diǎn): 在注重吞吐量或者處理器資源較為稀缺的場(chǎng)合, 都可以優(yōu)先考慮 Parallel Scavenge 加 Parallel Old 收集器這個(gè)組合
缺點(diǎn): 回收工作需要 Stop The World, 可搭配的新生代收集器僅有 Parallel Scavenge 收集器
3.6 CMS(Concurrent Mark Sweep) 并行收集器-標(biāo)記清除算法
CMS 是一款以獲取最短回收停頓時(shí)間為目標(biāo)的收集器, 是真正意義上與用戶線程并發(fā)運(yùn)行的收集器。
它的運(yùn)作過程分為 4 個(gè)步驟
- 初始標(biāo)記 (CMS initial mark): 只是標(biāo)記一下 GC Roots 能直接關(guān)聯(lián)到的對(duì)象, 速度很快
- 并發(fā)標(biāo)記 (CMS concurrent mark): 從 GC Roots 的直接關(guān)聯(lián)對(duì)象開始遍歷整個(gè)對(duì)象圖的過程, 這個(gè)過程耗時(shí)較長(zhǎng)但是不需要停頓用戶線程
- 重新標(biāo)記 (CMS remark): 修正并發(fā)標(biāo)記期間, 因用戶程序繼續(xù)運(yùn)作而導(dǎo)致標(biāo)記產(chǎn)生變動(dòng)的那一部分對(duì)象, 這個(gè)階段停頓的時(shí)間會(huì)比初始標(biāo)記階段稍長(zhǎng)一些, 但也比并
發(fā)標(biāo)記階段的時(shí)間短- 并發(fā)清除 (CMS concurrent sweep): 清理刪除標(biāo)記階段判斷的已經(jīng)失效的對(duì)象, 由于不需要移動(dòng)對(duì)象, 這個(gè)節(jié)點(diǎn)也是和用戶線程同時(shí)并發(fā)的
初始標(biāo)記和重新標(biāo)記這 2 個(gè)步驟仍然需要 Stop The World。
而耗時(shí)最長(zhǎng)的為并發(fā)標(biāo)記和并發(fā)清除 2 個(gè)階段, 都是與用戶線程一起工作的。
流程大體是這樣的:
優(yōu)點(diǎn): 并發(fā)收集, 低停頓, 對(duì)于大概 4GB 到 6GB 以下的堆內(nèi)存, CMS 一般處理的比較好
缺點(diǎn):
- CMS 收集器對(duì) CPU 資源非常敏感
會(huì)占用一定的 CPU 資源, 在并發(fā)標(biāo)記 / 清理的時(shí)候, 雖然不會(huì)導(dǎo)致用戶線程停頓, 但標(biāo)記 / 清理工作是要占用一部分 CPU 資源的, 這導(dǎo)致吞吐量降低。
CMS 默認(rèn)啟動(dòng)的回收線程數(shù)是 (CPU 數(shù)量 + 3) / 4。
- CMS 收集器無(wú)法處理浮動(dòng)垃圾, 可能出現(xiàn) “Concurrent Mode Failure” 失敗而導(dǎo)致另一次 Full GC 的產(chǎn)生。
由于在垃圾收集階段用戶線程還需要運(yùn)行, 那也就還需要預(yù)留有足夠的內(nèi)存空間給用戶線程使用, 因此 CMS 收集器不能像其他收集器那樣等到老年代幾乎完全
被填滿了再進(jìn)行收集, 需要預(yù)留一部分空間提供并發(fā)收集時(shí)的程序運(yùn)作使用。要是 CMS 運(yùn)行期間預(yù)留的內(nèi)存無(wú)法滿足程序需要, 就會(huì)出現(xiàn)一次
“Concurrent Mode Failure” 失敗, 這時(shí)虛擬機(jī)將啟動(dòng)后備預(yù)案: 停止用戶線程, 臨時(shí)啟用 Serial Old 收集器來(lái)重新進(jìn)行老年代的垃圾收集, 這樣停頓
時(shí)間就很長(zhǎng)了。
- CMS 收集器會(huì)產(chǎn)生大量空間碎片
CMS 是一款基于 “標(biāo)記-清除” 算法實(shí)現(xiàn)的收集器, 收集結(jié)束時(shí)會(huì)有大量空間碎片產(chǎn)生。 空間碎片過多時(shí), 將會(huì)給大對(duì)象分配帶來(lái)很大麻煩, 往往會(huì)出現(xiàn)老年代
還有很多剩余空間, 但就是無(wú)法找到足夠大的連續(xù)空間來(lái)分配當(dāng)前對(duì)象, 而不得不提前觸發(fā)一次 Full GC 的情況。
3.7 G1 (Garbage First) 并行收集器-標(biāo)記整理算法
G1 是一款主要面向服務(wù)端應(yīng)用的垃圾收集器, 在 JDK9 中正式使用。
G1 開創(chuàng)的基于 Region 的堆內(nèi)存布局使其能面向局部收集。
G1 雖然遵循分代收集理論設(shè)計(jì), 但內(nèi)部的堆內(nèi)存的布局和別的收集器有明顯不一樣的。G1 把 Java 堆劃分為多個(gè)大小相等的獨(dú)立區(qū)域 (Region),
每個(gè) Region 都可以根據(jù)需要, 扮演新生代的 Eden 空間, Survivor 空間或者老年代空間。
雖然 G1 仍保留了新生代和老年代的概念, 但新生代和老年代不再是固定的了, 它們都是一系列 Region (可以不連續(xù)) 的動(dòng)態(tài)集合。
收集器能夠?qū)Π缪莶煌巧?Region 采用不同的策略去處理。
Region 中還有一類特殊的 Humongous 區(qū)域, 專門用來(lái)存儲(chǔ)大對(duì)象。 G1 認(rèn)為只要大小超過了一個(gè) Region 容量一半的對(duì)象即可判定為大對(duì)象 (每個(gè) Region 的大小可以通過 -XX:G1HeapRegionSize 進(jìn)行配置, 大小在 1 - 32M, 同時(shí)必須是 2 的 N 次冪)。
對(duì)于那些超過了整個(gè) Region 容量的超級(jí)大對(duì)象, 將會(huì)被存放在 N 個(gè)連續(xù)的 Humongous Region 之中, G1 的大多數(shù)行為都把 Humongous Region 作為老年代的一部分進(jìn)行看待。
G1 將 Region 作為單次回收的最小單元, 即每次收集到的內(nèi)存空間都是 Region 大小的整數(shù)倍, 這樣可以有計(jì)劃地避免在整個(gè) Java 堆中進(jìn)行全區(qū)域的垃圾收集。
G1 收集器會(huì)跟蹤各個(gè) Region 里面的垃圾堆積的 “價(jià)值” 大小, 價(jià)值即回收所獲得的空間大小以及回收所需時(shí)間的經(jīng)驗(yàn)值, 然后在后臺(tái)維護(hù)一個(gè)優(yōu)先級(jí)列表,
每次根據(jù)用戶設(shè)定允許的收集停頓時(shí)間(通過參數(shù) -XX: MaxGCPauseMillis 設(shè)置, 默認(rèn)為 200 毫秒), 優(yōu)先處理回收價(jià)值收益最大的那些 Region。
G1 收集器的運(yùn)作過程大致分為 4 個(gè)步驟
- 初始標(biāo)記 (Initial Marking)
僅僅只是標(biāo)記一下 GC Roots 能直接關(guān)聯(lián)到的對(duì)象, 并且修改 TAMS 指針的值, 讓下一階段用戶線程并發(fā)運(yùn)行時(shí), 能正確地在可用的 Region 中分配新對(duì)象。
這個(gè)階段需要停頓用戶線程, 但耗時(shí)很短, 而且是借用進(jìn)行 Minor GC 的時(shí)候同步完成的, 所以這個(gè)階段實(shí)際沒有額外的停頓。
- 并發(fā)標(biāo)記 (Concurrent Marking)
從 GC Root 開始對(duì)堆中對(duì)象進(jìn)行可達(dá)性分析, 遞歸掃描整個(gè)堆里的對(duì)象圖, 找出要回收的對(duì)象, 這階段耗時(shí)較長(zhǎng), 但可與用戶程序并發(fā)執(zhí)行。當(dāng)對(duì)象圖掃描完成以后, 還要重新處理 SATB 記錄下的在并發(fā)時(shí)有引用變動(dòng)的對(duì)象。
- 最終標(biāo)記 (Final Marking)
對(duì)用戶線程做另一個(gè)短暫的暫停, 用于處理并發(fā)階段結(jié)束后仍遺留下來(lái)的最后那少量的 SATB 記錄。
- 篩選回收 (Live Date Counting and Evacuation)
負(fù)責(zé)更新 Region 的統(tǒng)計(jì)數(shù)據(jù)), 對(duì)各個(gè) Region 的回收價(jià)值和成本進(jìn)行排序, 根據(jù)用戶所期望的停頓時(shí)間來(lái)制定回收計(jì)劃, 可以自由選擇任意多個(gè) Region 構(gòu)成回收集, 然后把決定回收的那一部分 Region 的存活對(duì)象復(fù)制到空的 Region 中, 再清理掉整個(gè)舊 Region 的全部空間。這里的操作涉及存活對(duì)象的移動(dòng), 是必須暫停用戶線程, 由多條收集器線程并行完成的。
從上面的 4 個(gè)步驟可以看出, G1 收集器除了并發(fā)標(biāo)記外, 其余階段都是要暫停用戶線程的。也就是他并發(fā)純粹地追求低延遲, G1 的目標(biāo)是在延遲可控的情況下, 獲得盡可能高的吞吐量。
回收階段其實(shí)是可以設(shè)計(jì)成和用戶線程并發(fā)的, 但是考慮到實(shí)現(xiàn)成本高, 而且 G1只是回收一部分的 Region, 停頓時(shí)間是用戶可控的, 就沒迫切的實(shí)現(xiàn)。
同時(shí), 停頓用戶線程能最大幅度地提高垃圾收集效率。
G1 的流程大體是這樣的:
在 G1 收集器中, 可以通過設(shè)置不同的期望停頓時(shí)間, 使得其在不同應(yīng)用場(chǎng)景中取得吞吐量和關(guān)注延遲之間的最佳平衡。
但是這個(gè)設(shè)置的 “期望值” 必須符合實(shí)際, 如果將時(shí)間設(shè)置到很低, 可以導(dǎo)致每次選出來(lái)的回收集只占很小的一部分, 收集器收集的速度逐漸跟不上分配器的分配速度,
導(dǎo)致垃圾逐漸堆積, 最終占滿對(duì)引發(fā) Full GC, 所以這個(gè)期望停頓時(shí)間一般設(shè)置為一兩百毫秒或者兩三百毫秒。
從 G1 開始, 最先進(jìn)的垃圾收集器的設(shè)計(jì)導(dǎo)向都不約而同地變?yōu)樽非竽軌驊?yīng)付應(yīng)用的內(nèi)存分配速率 (Allocation Rate), 而不追求一次把整個(gè) Java 堆全部清理干凈。
這樣, 應(yīng)用在分配, 同時(shí)收集器在收集, 只要收集的速度能跟得上對(duì)象分配的速度, 那一切就能運(yùn)作得很完美。
G1 從整體來(lái)看是基于 “標(biāo)記-整理” 算法實(shí)現(xiàn)的收集器, 但從局部 (兩個(gè) Region 之間) 上看又是基于 “標(biāo)記-復(fù)制” 算法實(shí)現(xiàn),
無(wú)論如何, 這兩種算法都意味著 G1 運(yùn)作期間不會(huì)產(chǎn)生內(nèi)存空間碎片, 垃圾收集完成之后能提供規(guī)整的可用內(nèi)存。
這種特性有利于程序長(zhǎng)時(shí)間運(yùn)行, 在程序?yàn)榇髮?duì)象分配內(nèi)存時(shí)不容易因無(wú)法找到連續(xù)內(nèi)存空間而提前觸發(fā)下一次收集。
G1 和 CMS 比起來(lái)也是有缺點(diǎn)的, 比如用戶線程停頓的時(shí)間更長(zhǎng)一些, 復(fù)雜的卡表設(shè)置, 占用的堆內(nèi)存更多, 執(zhí)行過程中,
G1 為了垃圾回收產(chǎn)生的內(nèi)存和運(yùn)行的額外執(zhí)行負(fù)載都比 CMS 高。
最終是使用 CMS 和 G1 還是需要具體的場(chǎng)景進(jìn)行考慮。
3.7.1 G1 存在的問題
跨 Region 引用對(duì)象如何處理
使用記憶集, 避免全堆掃描。
G1 記憶集的特殊點(diǎn): 每個(gè) Region 都維護(hù)有自己的記憶集, 這些記憶集會(huì)記錄下別的 Region 指向自己的指針, 并標(biāo)記這些指針分別在哪些卡頁(yè)的范圍之內(nèi)。
G1 的記憶集是一個(gè)哈希表的結(jié)構(gòu), key 是 Region 的起始地址, Value 是一個(gè)集合, 存儲(chǔ)的元素是卡表的索引號(hào)。
基于此, G1 至少要消耗大約相當(dāng) Java 堆容量 10% 至 20% 的額外內(nèi)存來(lái)維持收集器的工作。
并發(fā)標(biāo)記階段如何保證收集線程和用戶線程互不干擾的運(yùn)行
G1 收集器通過原始快照 (STAB) 算法實(shí)現(xiàn)的。同時(shí) G1 為每個(gè) Region 設(shè)計(jì)了 2 個(gè)名為 TAMS (Top at Mark Start) 的指針, 把 Region 中的一部分空間劃分處理用用并發(fā)回收過程中的新對(duì)象分配。
并發(fā)回收時(shí), 新分配的對(duì)象地址都必須要在這兩個(gè)指針位置以上, G1 默認(rèn)在這個(gè)位置上的對(duì)象是被隱式標(biāo)記過的, 既默認(rèn)存活的, 不納入回收返回。
同樣的, 在回收的時(shí)候, 回收速度趕不上內(nèi)存分配的速度, 也會(huì)像 CMS 的 ”Concurrent Mode Failure", 凍結(jié)用戶線程的執(zhí)行, 導(dǎo)致 Full GC, 而產(chǎn)生長(zhǎng)時(shí)間的 STW。
如果建立可靠的停掉預(yù)測(cè)模型
用戶通過 -XX:MaxGCPauseMillis 參數(shù)指定的停頓時(shí)間只意味著垃圾收集發(fā)生之前的期望值, G1 是如何做到滿人用戶的期望的?
G1 收集器的停頓預(yù)測(cè)模型是以衰減均值 (Decaying Average) 為理論基礎(chǔ)來(lái)實(shí)現(xiàn)的, 在垃圾收集過程中, G1 收集器會(huì)記錄每個(gè) Region 的回收耗時(shí),
每個(gè) Region 記憶集里的臟卡數(shù)量等各個(gè)可測(cè)量的步驟花費(fèi)的成本, 并分析得出平均值, 標(biāo)準(zhǔn)偏差, 置信度等統(tǒng)計(jì)信息, 然后通過這些信息預(yù)測(cè)現(xiàn)在開始回收的話,
由哪些 Region 組成回收集才可以在不超過期望停頓時(shí)間的約束下獲得最高的收益。
4 低延遲垃圾收集器 (Low-Latency Garbage Collector)
衡量垃圾收集器的三項(xiàng)最重要的指標(biāo)是: 內(nèi)存占用 (Footprint), 吞吐量 (Throughput) 和延遲 (Latency), 三者共同構(gòu)成了一個(gè)"不可能三角",
一款優(yōu)秀的收集器通常最多可以同時(shí)達(dá)到其中的 2 項(xiàng)。
隨著計(jì)算機(jī)軟硬件的發(fā)展, 現(xiàn)在延遲的重要性更受關(guān)注。
大內(nèi)存的出現(xiàn), 使我們可以容忍收集器多占一點(diǎn)點(diǎn)內(nèi)存的, 吞吐量也會(huì)更高等, 但是這些軟硬件的提升, 對(duì)延遲反而帶來(lái)了負(fù)面效果。
4.1 Shenandoah
Shenandoah 是一款只有 OpenJDK 才會(huì)包含, 而 OracleJDK 沒有的收集器。
Shenandoah 是 RedHat 公司獨(dú)立發(fā)展的新型收集器項(xiàng)目, 項(xiàng)目的目標(biāo)是實(shí)現(xiàn)一種能在任何堆內(nèi)存大小下都可以把垃圾收集的停頓時(shí)間限制在 10 毫秒以內(nèi)的垃圾收集器。
Shenandoah 和 G1 類似, 也是使用基于 Region 的堆內(nèi)存布局), 使用大對(duì)象的 Humongours Region, 默認(rèn)的回收策略也是有限處理回收價(jià)值最大的 Region。
初始標(biāo)記, 并發(fā)標(biāo)記等階段的處理思路也是類似的。
但是在管理內(nèi)存方面, 它與 G1 至少有三個(gè)明細(xì)的不同之處
- 支持并發(fā)的整理算法
- 不使用分代收集
- 摒棄 G1 中耗費(fèi)大量?jī)?nèi)存和計(jì)算資源去維護(hù)的記憶集, 改用名為 “連接矩陣” (Connection Matrix) 的全局?jǐn)?shù)據(jù)結(jié)構(gòu)來(lái)記錄跨 Region 的引用關(guān)系, 降低了處理跨代指針是記憶集維護(hù)消耗, 也降低了偽共享問題的發(fā)生概率
連接矩陣可以簡(jiǎn)單理解為一張二維表格, 如果 Region N 中有對(duì)象指向了 Region M 就在表格的 N 行 M 列打上一個(gè)標(biāo)記。
Shenandoah 收集器的工作過程大致分為 9 個(gè)階段
- 初始標(biāo)記 (Initial Marking)
和 G1 一樣, 首先標(biāo)記與 GC Roots 直接關(guān)聯(lián)的對(duì)象, 這個(gè)階段同樣需要暫停用戶線程, 但是停頓的時(shí)間和堆大小無(wú)法, 只和 GC Roots 的數(shù)量相關(guān)。
- 并發(fā)標(biāo)記 (Concurrent Marking)
和 G1 一樣, 遍歷對(duì)象圖, 標(biāo)記處全部可達(dá)的對(duì)象, 這個(gè)階段是與用戶線程一起并發(fā)的, 時(shí)間長(zhǎng)短取決于堆中存活對(duì)象的數(shù)量以及對(duì)象圖的結(jié)構(gòu)復(fù)雜程度。
- 最終標(biāo)記 (Final Marking)
與 G1 一樣, 處理剩余的 SATB 掃描, 并在這個(gè)階段統(tǒng)計(jì)出回收價(jià)值最高的 Region, 將這些 Region 構(gòu)成一組回收集(Collection Set)。最終標(biāo)記階段也會(huì)有一小段短暫的停頓。
- 并發(fā)清理 (Concurrent Cleanup)
這個(gè)階段用于清理那些整個(gè)區(qū)域內(nèi)連一個(gè)存活對(duì)象都沒有找到的 Region (這類 Region 被稱為 Immediate Garbage Region)
- 并發(fā)回收 (Concurrent Evacuation)
這一步是 Shenandoah 和 HotSpot 中和其他收集器的核心差異。 在這個(gè)階段, Shenandoah 把回收集中存活對(duì)象先復(fù)制一份到其他未被使用的 Region 之中。
這個(gè)并發(fā)的過程是通過讀屏障和 “Brooks Pointers” 的轉(zhuǎn)發(fā)指針來(lái)保證過程中, 用戶線程的準(zhǔn)備性。并發(fā)回收階段運(yùn)行的時(shí)間長(zhǎng)短取決于回收集的大小
- 初始引用更新 (Initial Update Reference)
并發(fā)回收階段復(fù)制對(duì)象結(jié)束后, 還需要吧堆中所有指向舊對(duì)象的引用修正到復(fù)制后的新地址, 這個(gè)操作成為引用更新。
引用更新的初始化階段實(shí)際上并未做什么具體的處理, 設(shè)立這個(gè)階段只是為了建立一個(gè)線程集合點(diǎn), 確保所有并發(fā)回收階段中進(jìn)行的收集器線程都已完成分配給它們的對(duì)象移動(dòng)任務(wù)而已。
初始引用更新時(shí)間很短, 會(huì)產(chǎn)生一個(gè)非常短暫的停頓。
- 并發(fā)引用更新 (Concurrent Update Reference)
真正開始進(jìn)行引用更新操作, 這個(gè)階段是與用戶線程一起并發(fā)的, 時(shí)間長(zhǎng)短取決于內(nèi)存中涉及的引用數(shù)量的多少。
并發(fā)引用更新與并發(fā)標(biāo)記不同, 它不再需要沿著對(duì)象圖來(lái)搜索, 只需要按照內(nèi)存物理地址的順序, 線性地搜索出引用類型, 把舊值改為新值即可。
- 最終引用更新 (Final Update Reference)
解決了堆中的引用更新后, 還要修正存在于 GC Roots 中的引用。
這個(gè)階段是 Shenandoah 的最后一次停頓, 停頓時(shí)間只與 GC Roots 的數(shù)量相關(guān)。
- 并發(fā)清理 (Concurrent Cleanup)
經(jīng)過并發(fā)回收和引用更新之后, 整個(gè)回收集中所有的 Region 已再無(wú)存活對(duì)象, 這些 Region 都變成 Immediate Garbage Regions 了,
最后再調(diào)用一次并發(fā)清理過程來(lái)回收這些 Region 的內(nèi)存空間, 供以后新對(duì)象分配使用。
流程大體是這樣的:
Brooks Pointers 說明
Brooks 是一個(gè)人名, 其在 1984 年提出了使用轉(zhuǎn)發(fā)指針 (Forwarding Pointer/Indirection Pointer) 來(lái)實(shí)現(xiàn)對(duì)象移動(dòng)和用戶程序并發(fā)的一種解決方案。
為了實(shí)現(xiàn)對(duì)象移動(dòng)和用戶程序并發(fā)進(jìn)行的同時(shí), 數(shù)據(jù)的準(zhǔn)確, 舊的實(shí)現(xiàn)邏輯:
在被移動(dòng)對(duì)象原有的內(nèi)存上設(shè)置保護(hù)陷阱 (Memory Protection Trap), 一旦用戶程序訪問到歸屬于舊對(duì)象的內(nèi)存空間就會(huì)產(chǎn)生自陷中斷,
進(jìn)入預(yù)設(shè)好的異常處理器中, 再由其中的邏輯代碼把訪問轉(zhuǎn)發(fā)到復(fù)制后的新對(duì)象。這種操作如果沒有操作系統(tǒng)的直接支持, 會(huì)導(dǎo)致用戶態(tài)頻繁切換到核心態(tài)。
而 Brooks 的解決方案:
在原有的對(duì)象布局結(jié)構(gòu)的最前面統(tǒng)一增加一個(gè)新的引用字段, 在正常不處于并發(fā)移動(dòng)的情況下, 指向自身。
正常情況:
訪問這個(gè)對(duì)象, 通過這個(gè)對(duì)象的引用字段, 找到真正的對(duì)象
對(duì)象移動(dòng)時(shí):
向?qū)⑦@個(gè)對(duì)象的引用字段修改為移動(dòng)后的對(duì)象, 訪問這個(gè)對(duì)象, 通過這個(gè)對(duì)象的引用字段, 就能找到移動(dòng)后的對(duì)象
Brooks Pointers 和早期 JVM 的句柄定位類型。
4.1.1 存在的問題
(1) 所有間接對(duì)象訪問技術(shù)都有一個(gè)相同的缺點(diǎn): 每一次對(duì)象訪問都會(huì)帶來(lái)一次額外的轉(zhuǎn)向開銷 (這個(gè)開銷在系統(tǒng)層面已經(jīng)降到很低了), 但是對(duì)象的頻繁訪問, 也會(huì)成為一筆不小的執(zhí)行成本
(2) 轉(zhuǎn)發(fā)指針的作用, 當(dāng)對(duì)象擁有一份新的副本時(shí), 只需要修改一處指針的值, 即舊對(duì)象上轉(zhuǎn)發(fā)指針的引用位置, 使其執(zhí)行新的對(duì)象, 就可以將所有對(duì)該對(duì)象的訪問轉(zhuǎn)發(fā)到新的副本上。
這種設(shè)計(jì)必定存在并發(fā)問題, 在讀的情況基本沒問題, 但是但出現(xiàn)收集線程和用戶線程并發(fā)寫入, 就必須保證寫操作只能發(fā)生在新復(fù)制的對(duì)象上, 而不是寫入到舊對(duì)象的內(nèi)存。
假設(shè)
- 收集器線程復(fù)制了新的對(duì)象副本
- 用戶線程更新了對(duì)象的某個(gè)值
- 收集器線程更新轉(zhuǎn)發(fā)指針的引用值為新副本地址
讓事件 2 在事件 1 和事件 3 之間發(fā)生的話, 將導(dǎo)致的結(jié)果就是用戶線程對(duì)對(duì)象的變更發(fā)生在舊對(duì)象上, 新對(duì)象的值還是舊的。
Shenandoah 收集器通過 CAS 操作來(lái)保證并發(fā)是對(duì)象的訪問正確性。
(3) 執(zhí)行頻率的問題
對(duì)于面向?qū)ο蟮木幊陶Z(yǔ)言來(lái)說, “對(duì)象的訪問” 是一個(gè)很頻繁的事情, 讀寫, 加鎖等, 要覆蓋全部對(duì)象的訪問操作, Shennadoah 設(shè)置了讀, 寫屏障去攔截。
為了實(shí)現(xiàn) “Brooks Pointer”, Shennandoah 在原有的寫屏障內(nèi)加入了額外的轉(zhuǎn)發(fā)處理, 還使用了讀屏障, 代碼里面對(duì)象的讀取斌率的對(duì)對(duì)象的寫入頻率高很多的操作, 大量的讀屏障開銷會(huì)是一個(gè)性能問題。
而這個(gè)問題, Shenandoah 計(jì)劃在 JDK13 中使用基于引用訪問屏障 (Load Reference Barrier) 替代內(nèi)存屏障模型。即只攔截對(duì)象中數(shù)據(jù)類型為引用類型的讀寫操作, 而不去管原生數(shù)據(jù)類型等其他非引用字段的對(duì)象, 省去大量對(duì)原生類型、對(duì)象比較、對(duì)象加鎖等場(chǎng)景中設(shè)置內(nèi)存屏障所帶來(lái)的銷毀。
4.2 ZGC
ZGC 是一款在 JDK11 中加入的具有實(shí)驗(yàn)性的低延遲垃圾收集器。
其目標(biāo)和 Shenandoah 相似: 在盡可能對(duì)吞吐量影響不大的前提下, 實(shí)現(xiàn)在任意堆內(nèi)存大小下都可以把垃圾收集的停頓時(shí)間限制在 10 毫秒以內(nèi)的低延遲。
ZGC 收集器是一款基于 Region 內(nèi)存布局的, (暫時(shí)) 不設(shè)分代的,
使用了讀屏障, 染色指針和內(nèi)存多重映射等技術(shù)來(lái)實(shí)現(xiàn)可并發(fā)的標(biāo)記-整理算法的, 以低延遲為首要目標(biāo)的一款垃圾收集器。
4.2.1 ZGC 的特點(diǎn)
(1) 基于 Region 的堆內(nèi)存布局
ZGC 依舊是基于 Region 的堆內(nèi)存布局, 但是 ZGC 的 Region 具有動(dòng)態(tài)性 – 動(dòng)態(tài)創(chuàng)建和銷毀, 動(dòng)態(tài)的區(qū)域容量大小。
在 x64 (64 位系統(tǒng)) 硬件平臺(tái)下, ZGC 的 Region 具有如下的容量
-
小型 Region (Small Region): 容量固定為 2 MB, 用于放置小于 256 KB 的小對(duì)象
-
中型 Region (Medium Region): 容量固定為 32 MB, 用于放置大于 256 KB 但小于 4 mb 的對(duì)象
-
大型 Region (Large Region): 容量不固定, 可以動(dòng)態(tài)變化, 但必須是 2MB 的整數(shù)倍, 用于放置 4 MB 或以上的大對(duì)象, 每個(gè)大型 Region 只會(huì)存放一個(gè)大對(duì)象,
單它的世界容量完全有可能小于中型 Region, 最小容量可低至 4 MB。大型 Region 在 ZGC 的實(shí)現(xiàn)中是不會(huì)被重分配 (重分配是 ZGC 的一種處理動(dòng)作), 用于復(fù)制對(duì)象的收集器階段, 因?yàn)閺?fù)制一個(gè)大對(duì)象的代價(jià)很大。
(2) 使用染色指針技術(shù) (Colored Pointer) 和讀屏障實(shí)現(xiàn)并發(fā)整理
染色指針是一種直接將少量額外的信息存儲(chǔ)在指針上的技術(shù)。 這個(gè)技術(shù)怎么實(shí)現(xiàn)的呢?
在 64 位系統(tǒng)中, 理論可以訪問的內(nèi)存搞定 16EB (2 的 64 次冪)。 實(shí)際上, 基于需求 (用不到那么多內(nèi)存), 性能 (地址越寬在做地址轉(zhuǎn)換時(shí), 需要的頁(yè)表級(jí)數(shù)越多), 成本 (消耗更多晶體管) 等原因的考慮,
很多系統(tǒng)不會(huì)真正做到 16EB 的內(nèi)存支持。
- AMD64 架構(gòu), 只支持到 52 位 (4 PB) 的地址總線和 48 位 (256 TB) 的虛擬地址空間, 目前 64 位的硬件實(shí)際只支持到最大內(nèi)存為 256 TB
- 64 位 Linux 則支持 47 位 (128 TB) 的進(jìn)程虛擬地址空間和 46 位 (64 TB) 的物理地址空間
- 64 位 Windows 則支持 44 位 (16 TB) 的物理地址空間
在 Linux 64位指針中有 18 位不可用來(lái)尋址, 有用的只剩下 46 位, 也就是支持 64 TB 的內(nèi)存。
而 ZGC 的染色指針技術(shù)將這個(gè) 46 位的指針寬度利用起來(lái), 將其高 4 位提取出來(lái)存儲(chǔ)四個(gè)標(biāo)志信息。
通過這些標(biāo)志位, 虛擬機(jī)可以直接從指針中看到其引用對(duì)象的三色標(biāo)記, 是否進(jìn)入了重分配既 (被移動(dòng)過), 是否只能通過 finalize() 方法才能被訪問到。
由于這些標(biāo)志位進(jìn)一步壓縮了原本 46 位的地址空間, 也直接導(dǎo)致了 ZGC 能管理的內(nèi)存空間不能超過 4 TB (2 的 42 次冪)。
64 位 Linux 中的指針情況
雖然染色指針有 4 TB 的內(nèi)存限制, 不支持 32 位系統(tǒng), 不支持指針壓縮等約束, 但是其帶來(lái)的收益非??捎^。
- 染色指針可以使得一旦某個(gè) Region 的存活對(duì)象被移走之后, 這個(gè) Region 立即就能夠被釋放和重用, 而不必等待整個(gè)堆中的所有指向該 Region 的引用都被修正后才能清理,
使得理論上只要還有 1 個(gè)空閑 Region, ZGC 就能完成收集。而 Shenandoah 需要等到引用更新階段結(jié)束以后才能釋放回收集中的 Region, 這意味著堆中幾乎所有對(duì)象都存活的極端情況,
需要 1∶1 復(fù)制對(duì)象到新 Region 的話, 就必須要有一半的空閑 Region 來(lái)完成收集- 染色指針可以大幅減少在垃圾收集過程中內(nèi)存屏障的使用數(shù)量, 設(shè)置內(nèi)存屏障, 尤其是寫屏障的目的通常是為了記錄對(duì)象引用的變動(dòng)情況。
如果將這些信息直接維護(hù)在指針中, 顯然就可以省去一些專門的記錄操作。實(shí)際上, 到目前為止 ZGC 都并未使用任何寫屏障,
只使用了讀屏障 (一部分是染色指針的功勞, 一部分是 ZGC 現(xiàn)在還不支持分代收集, 天然就沒有跨代引用的問題) 。- 染色指針可以作為一種可擴(kuò)展的存儲(chǔ)結(jié)構(gòu)用來(lái)記錄更多與對(duì)象標(biāo)記, 重定位過程相關(guān)的數(shù)據(jù), 以便日后進(jìn)一步提高性能。 現(xiàn)在 Linux 下的 64 位指針中
還有前 18 位未使用, 雖然他們不能用來(lái)尋址, 卻可以用來(lái)做其他的事, 如果開發(fā)了這 18 位, 就能騰出當(dāng)前 46 位中占去的 4 位, 支持的堆內(nèi)存也能達(dá)到 64 TB。
要順利使用染色指針有一個(gè)必須解決的前置問題: Java 虛擬機(jī)作為一個(gè)普通的進(jìn)程, 隨意重新定義內(nèi)存中的某些指針的其中幾位, 操作系統(tǒng)/處理器是否支持等。
程序代碼最終都要轉(zhuǎn)換為機(jī)器指令流交給處理器執(zhí)行, 處理器是無(wú)法區(qū)分指針中哪部分是什么, 只會(huì)把整個(gè)指針當(dāng)做一個(gè)內(nèi)存地址來(lái)處理。
這個(gè)問題在 Solaris/SPARC 平臺(tái)很容易實(shí)現(xiàn), SPARC 硬件層面就支持虛擬地址掩碼, 設(shè)置后, 其機(jī)器指令直接忽略掉染色指針中標(biāo)志位。
而 x86-64 平臺(tái)采取了其他的措施 – 虛擬內(nèi)存映射技術(shù)。
Linux/x86-64 平臺(tái)上, ZGC 使用了多重映射將多個(gè)虛擬內(nèi)存地址映射到同一個(gè)物理內(nèi)存地址上, 這是一種多對(duì)一的映射, 意味著 ZGC 在虛擬內(nèi)存中看到的地址空間要比時(shí)間的堆內(nèi)存容量來(lái)得更大。
把染色指針紅的標(biāo)志位看著是地址的分段符, 那只需要將這些不同的地址段都映射到同一個(gè)物理內(nèi)存空間, 經(jīng)過多重映射轉(zhuǎn)換后, 就可以通過染色指針正常進(jìn)行尋址了。
4.2.2 ZGC 的運(yùn)行過程
ZGC 的運(yùn)行過程大致可以分為 4 個(gè)階段, 4 個(gè)階段都是并發(fā)執(zhí)行的, 僅 2 個(gè)階段中間存在短暫的停頓小階段。
簡(jiǎn)單的 4 個(gè)階段流程如下:
- 并發(fā)標(biāo)記 (Concurrent Mark)
并發(fā)標(biāo)記是遍歷對(duì)象圖做可達(dá)性分析的階段, 前后也要經(jīng)過類似于 G1 的初始標(biāo)記, 最終標(biāo)記 (盡管 ZGC 中的名字不叫這些) 的短暫停頓。
- 并發(fā)預(yù)備重分配 (Concurrent Prepare for Relocate)
要根據(jù)特定的查詢條件統(tǒng)計(jì)得出本次收集過程要清理哪些Region, 將這些 Region 組成重分配集 (Relocation Set)。 ZGC 劃分 Region 的目的并非為了
像 G1 那樣做收益優(yōu)先的增量回收, 相反, ZGC 每次回收都會(huì)掃描所有的 Region, 用范圍更大的掃描成本換取省去 G1 中記憶集的維護(hù)成本。 因此, ZGC 的
重分配集只是決定了里面的存活對(duì)象會(huì)被重新復(fù)制到其他的 Region 中, 里面的 Region 會(huì)被釋放, 而并不能說回收行為就只是針對(duì)這個(gè)集合里面的 Region
進(jìn)行, 因?yàn)闃?biāo)記過程是針對(duì)全堆的。 (JDK12 的 ZGC 開始支持的類卸載以及弱引用的處理, 也是在這個(gè)階段完成的)
- 并發(fā)重分配 (Concurrent Relocate)
重分配是 ZGC 執(zhí)行過程中的核心階段, 這個(gè)過程要把重分配集中的存活對(duì)象復(fù)制到新的 Region 上, 并為重分配集中的每個(gè) Region 維護(hù)一個(gè)轉(zhuǎn)發(fā)表 (Forward Table),
記錄從舊對(duì)象到新對(duì)象的轉(zhuǎn)向關(guān)系。得益于染色指針的支持, ZGC 收集器能僅從引用上就明確得知一個(gè)對(duì)象是否處于重分配集之中, 如果用戶線程此時(shí)并發(fā)訪問了
位于重分配集中的對(duì)象, 這次訪問將會(huì)被預(yù)置的內(nèi)存屏障所截獲, 然后立即根據(jù) Region 上的轉(zhuǎn)發(fā)表記錄將訪問轉(zhuǎn)發(fā)到新復(fù)制的對(duì)象上, 并同時(shí)修正更新該引用
的值, 使其直接指向新對(duì)象, ZGC 將這種行為稱為指針的 “自愈” (Self-Healing) 能力。
這樣做的好處是只有第一次訪問舊對(duì)象會(huì)陷入轉(zhuǎn)發(fā), 也就是只慢一次, 對(duì)比 Shenandoah 的 Brooks 轉(zhuǎn)發(fā)指針, 那是每次對(duì)象訪問都必須付出的固定開銷, 簡(jiǎn)
單地說就是每次都慢, 因此 ZGC 對(duì)用戶程序的運(yùn)行時(shí)負(fù)載要比 Shenandoah 來(lái)得更低一些。
另外一個(gè)直接的好處是由于染色指針的存在, 一旦重分配集中某個(gè) Region 的存活對(duì)象都復(fù)制完畢后, 這個(gè) Region 就可以立即釋放用于新對(duì)象的分配 (但是
轉(zhuǎn)發(fā)表還得留著不能釋放掉), 哪怕堆中還有很多指向這個(gè)對(duì)象的未更新指針也沒有關(guān)系, 這些舊指針一旦被使用, 它們都是可以自愈的。
- 并發(fā)重映射 (Concurrent Remap)
重映射所做的就是修正整個(gè)堆中指向重分配集中舊對(duì)象的所有引用, 但是 ZGC 的并發(fā)重映射并不是一個(gè)必須要 “迫切” 去完成的任務(wù)。因?yàn)榍懊嬲f過, 即使是舊
引用, 它也是可以自愈的, 最多只是第一次使用時(shí)多一次轉(zhuǎn)發(fā)和修正操作。重映射清理這些舊引用的主要目的是為了不變慢 (還有清理結(jié)束后可以釋放轉(zhuǎn)發(fā)表
這樣的附帶收益), 所以說這并不是很"迫切"。 因此, ZGC 很巧妙地把并發(fā)重映射階段要做的工作, 合并到了下一次垃圾收集循環(huán)中的并發(fā)標(biāo)記階段里去完成,
反正它們都是要遍歷所有對(duì)象的, 這樣合并就節(jié)省了一次遍歷對(duì)象圖的開銷。一旦所有指針都被修正之后, 原來(lái)記錄新舊對(duì)象關(guān)系的轉(zhuǎn)發(fā)表就可以釋放掉了。
簡(jiǎn)單地了解了過程后, 我們分析一下為什么需要 2 個(gè)標(biāo)記位?
首先 Mark0, Mark1 和 Remapped 三個(gè)任何時(shí)候只會(huì)有 1 個(gè)為 1。
假設(shè)標(biāo)記了兩個(gè)對(duì)象 ObjA 和 ObjB, 在第一次回收后, 地址視圖為 M0, 都是活躍對(duì)象。在轉(zhuǎn)移階段, ZGC 是按照頁(yè)面進(jìn)行部分內(nèi)存垃圾回收的, 也就是說
當(dāng)對(duì)象所在的頁(yè)面需要回收時(shí), 頁(yè)面里面的對(duì)象需要被轉(zhuǎn)移, 如果頁(yè)面不需要轉(zhuǎn)移, 頁(yè)面里面的對(duì)象也就不需要轉(zhuǎn)移。
假設(shè) ObjA 所在的頁(yè)面被回收, ObjB 所在的頁(yè)面在這一次垃圾回收中不會(huì)被回收。
ObjA 被轉(zhuǎn)移后, 它的地址視圖從 M0 調(diào)整為 Remapped, ObjB 不會(huì)被轉(zhuǎn)移, ObjB 的地址視圖仍然為 M0。
那么下一次垃圾回收標(biāo)記階段開始的時(shí)候, 存在兩種地址視圖的對(duì)象
- 地址視圖為 Remapped 的對(duì)象, 說明該對(duì)象在并發(fā)轉(zhuǎn)移階段被轉(zhuǎn)移或者被訪問過
- 地址視圖為 M0 的對(duì)象, 說明該對(duì)象在前一次垃圾回收的標(biāo)記階段已經(jīng)被標(biāo)記
如果本次垃圾回收標(biāo)記階段仍然使用 M0 這個(gè)地址視圖, 那么就不能區(qū)分出對(duì)象是活躍的, 還是上一次垃圾回收標(biāo)記過的
所以新標(biāo)記階段使用了另外一個(gè)地址視圖 M1, 則標(biāo)記結(jié)束后所有活躍對(duì)象的地址視圖都為 M1。
此時(shí)在這 3 個(gè)地址視圖代表的含義是
- M1: 本次垃圾回收中識(shí)別的活躍對(duì)象
- M0: 前一次垃圾回收的標(biāo)記階段被標(biāo)記過的活躍對(duì)象, 對(duì)象在轉(zhuǎn)移階段未被轉(zhuǎn)移, 但是在本次垃圾回收中被識(shí)別為不活躍對(duì)象
- Remapped: 前一次垃圾回收的轉(zhuǎn)移階段發(fā)生轉(zhuǎn)移的對(duì)象或者是被應(yīng)用程序線程訪問的對(duì)象, 但是在本次垃圾回收中被識(shí)別為不活躍對(duì)象
如果將上面的 4 個(gè)步驟擴(kuò)充出來(lái)這是這樣的
過程是這樣的
ZGC 提供了 2 個(gè)參數(shù) ParallelGCThreads 和 ConcGCThreads, 分別用于 STW 并行時(shí)候的線程數(shù)和并發(fā)階段的線程數(shù)。
不過 ConcGCThreads 數(shù)量需要注意, 因?yàn)榇穗A段是和應(yīng)用線程并發(fā), 如果線程數(shù)過多會(huì)影響應(yīng)用線程。
ZGC 沒有使用記憶集, 它甚至連分代都沒有, 減少各種中間結(jié)構(gòu)的維護(hù), 沒有使用寫屏障, 減少對(duì)用戶線程的運(yùn)行負(fù)擔(dān)等。 這些權(quán)衡必定要有優(yōu)有劣,
ZGC 的這種權(quán)衡也限制了它能承受的對(duì)象分配速率不會(huì)太高。 假設(shè) ZGC 準(zhǔn)備要對(duì)一個(gè)很大的堆做一次完整的并發(fā)收集, 在這段時(shí)間里面, 由于應(yīng)用的對(duì)象分配
速率很高, 將創(chuàng)造大量的新對(duì)象, 這些新對(duì)象很難進(jìn)入當(dāng)次收集的標(biāo)記范圍, 通常就只能全部當(dāng)作存活對(duì)象來(lái)看待 – 盡管其中絕大部分對(duì)象都是朝生夕滅的,
這就產(chǎn)生了大量的浮動(dòng)垃圾。 這種情況如果持續(xù)位置, 那么就會(huì)導(dǎo)致堆的可用空間越來(lái)越小。 目前唯一的解決方法就是增大堆空間, 獲得更多的執(zhí)行時(shí)間。
ZGC 還支持 “MUMA-Aware (Non-Uniform Memory Access, 非統(tǒng)一內(nèi)存訪問架構(gòu))” 的內(nèi)存分配。
在 NUMA 架構(gòu)下, ZGC 收集器會(huì)優(yōu)先嘗試在請(qǐng)求線程當(dāng)前所處的處理器的本地內(nèi)存上分配對(duì)象, 以保證高效內(nèi)存訪問。
5 垃圾收集器的選擇
(1) 應(yīng)用程序的主要關(guān)注點(diǎn)是什么
數(shù)據(jù)分析, 科學(xué)計(jì)算類的任務(wù), 目標(biāo)是能盡快算出結(jié)果, 那吞吐量就是主要關(guān)注點(diǎn)。
SLA 應(yīng)用, 那停頓時(shí)間直接影響服務(wù)質(zhì)量, 嚴(yán)重的甚至?xí)?dǎo)致事務(wù)超時(shí), 這樣延遲就是主要關(guān)注點(diǎn)。
客戶端應(yīng)用或者嵌入式應(yīng)用, 那垃圾收集的內(nèi)存占用則是不可忽視的。
(2) 運(yùn)行應(yīng)用的基礎(chǔ)設(shè)施如何
可以從硬件規(guī)格, 系統(tǒng)架構(gòu), 處理器數(shù)量, 分配的內(nèi)存大小等進(jìn)行考慮
(3) JDK 的發(fā)行商
OpenJDK, OracleJDK, ZingJDK 等
6 垃圾收集器日志
通過垃圾收集器的日志, 我們可以了解到每次 GC 前后的變化。 在 JDK9 之前, 每個(gè)收集器的日志輸入?yún)?shù)不一定都一致, 但是
在 JDK9 后, HotSpot 所有功能的日志都可以通過 “-Xlog” 參數(shù)進(jìn)行配置。
JDK9 之前
HotSpot 虛擬機(jī)提供了 -XX: +PrintGCDetails 這個(gè)收集器日志參數(shù), 告訴虛擬機(jī)在發(fā)生垃圾收集行為時(shí)打印內(nèi)存回收日志, 并且在進(jìn)程退出的時(shí)候輸出當(dāng)前
的內(nèi)存各區(qū)域分配情況
7 JVM 的一些參數(shù)
HotSpot 虛擬機(jī)提供了 -XX: PretenureSizeThreshold 參數(shù), 指定大于該設(shè)置值的對(duì)象直接在老年代分配, 這樣做的目的就是避免在 Eden 區(qū)及兩個(gè) Survivor
區(qū)之間來(lái)回復(fù)制, 產(chǎn)生大量的內(nèi)存復(fù)制操作。這個(gè)參數(shù)只對(duì) Serial 和 ParNew 兩款新生代收集器有效。
對(duì)象晉升老年代的年齡閾值, 可以通過參數(shù) -XX: MaxTenuringThreshold 設(shè)置
-XX: HandlePromotionFailure 參數(shù)設(shè)置值是否允許擔(dān)保失敗文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-839725.html
8 參考
《深入理解Java虛擬機(jī)》- 周志明
Java 12正式發(fā)布), 新特性解讀!
ZGC 詳解文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-839725.html
到了這里,關(guān)于【Java JVM】垃圾回收的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!