
正文
垃圾收集器關(guān)注的是 Java 堆和方法區(qū),因為這部分內(nèi)存的分配和回收是動態(tài)的。只有在程序處于運(yùn)行期間時才能知道會創(chuàng)建哪些對象,也才能知道需要多少內(nèi)存。
虛擬機(jī)棧和本地方法棧則不需要過多考慮回收的問題,因為棧中每一個棧幀分配多少內(nèi)存基本上是在類結(jié)構(gòu)確定下來時就已知的,因此這幾個區(qū)域的內(nèi)存分配和回收具有確定性。
一、對象已死嗎
垃圾收集器在對堆進(jìn)行回收前,第一件事就是要確定堆中對象哪些還“存活”著,哪些已“死去”(即不可能再被任何途徑使用的對象)。
1、 引用計數(shù)算法
給對象添加一個引用計數(shù)器,每當(dāng)有一個地方引用它時,計數(shù)器值加 1;當(dāng)引用失效時,計數(shù)器值減 1;任何時刻計數(shù)器為 0 的對象就是不可能再被使用的。
優(yōu)點:實現(xiàn)簡單,判定效率高。
缺點:很難解決對象之間相互循環(huán)引用的問題。
2、可達(dá)性分析算法
通過一系列被稱為“GC Roots”的對象作為起點,從這些節(jié)點開始向下搜索,搜索所走過的路徑稱為引用鏈,當(dāng)一個對象到 GC Roots 沒有任何引用鏈相連時,則此對象不可用。
Java 語言中,可作為 GC Roots 的對象:
- 虛擬機(jī)棧(棧幀中的本地變量表)中引用的對象。
- 方法區(qū)中類靜態(tài)屬性引用的對象。
- 方法區(qū)中常量引用對象。
- 本地方法棧中 JNI(即 Native 方法)引用的對象。
可達(dá)性分析算法中不可達(dá)的對象,至少要經(jīng)歷兩次標(biāo)記過程,才會被回收。
- 發(fā)現(xiàn)沒有與 GC Roots 相連的引用鏈時,進(jìn)行第一次標(biāo)記。
- 當(dāng)對象覆蓋了 finalize() 方法,并且沒有被調(diào)用過時,將會被放入一個叫做 F-Queue 的隊列中,稍后 GC 將對 F-Queue 中的對象進(jìn)行第二次標(biāo)記。如果在 finalize() 方法中,對象沒有重新與引用鏈上的一個對象建立關(guān)聯(lián),那么將會被回收。
3、四種引用
無論是引用計數(shù)算法,還是可達(dá)性分析算法,判斷對象是否存活都與“引用”有關(guān)。Java 中有 4 種引用,按強(qiáng)度由強(qiáng)至弱依次為:強(qiáng)引用、軟引用、弱引用、虛引用。
- 強(qiáng)引用:類似“Object obj = new Object()”的引用。只要強(qiáng)引用還存在,對象就永遠(yuǎn)不會回收。
- 軟引用:用來描述一些還有用但并非必需的對象。內(nèi)存不足時,對象有可能被回收??赏ㄟ^ SoftReference 類實現(xiàn)軟引用。
- 弱引用:用來描述非必需的對象,但強(qiáng)度比軟引用弱。GC時,無論內(nèi)存是否足夠,對象都會被回收??赏ㄟ^ WeakReference 類來實現(xiàn)弱引用。
- 虛引用:也稱幽靈引用或幻影引用,虛引用不會對對象的生存時間構(gòu)成影響。虛引用的唯一作用就是能在對象被回收時收到一個系統(tǒng)通知??赏ㄟ^ PhantomReference 類實現(xiàn)虛引用。
4、回收方法區(qū)
永久代的垃圾收集主要回收兩部分內(nèi)容:廢棄常量和無用的類。
如何判定廢棄常量:
- 常量池中的常量(字面量、符號引用)沒有在任何地方被引用。
如何判定無用的類:
- 該類的所有實例都已被回收。
- 加載該類的 ClassLoader 已被回收。
- 該類對應(yīng)的 java.lang.Class 對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。
二、垃圾收集算法
1、標(biāo)記-清除算法
分為“標(biāo)記”和“清除”兩個階段。首先標(biāo)記出所有需要回收的對象,然后再統(tǒng)一回收所有被標(biāo)記的對象。
該算法會產(chǎn)生大量不連續(xù)的內(nèi)存碎片,因而在分配較大對象時,可能會由于無法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)一次 GC。
2、復(fù)制算法
將可用內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中一塊。當(dāng)一塊內(nèi)存用完時,就將還存活的對象復(fù)制到另一塊,然后再把已使用過的內(nèi)存空間一次清理掉。
該算法的代價是始終會有一塊內(nèi)存被“浪費(fèi)”掉。
由于新生代的對象 98% 是“朝生夕死”,因此并不需要按 1:1 的比例來劃分內(nèi)存空間?,F(xiàn)在的商業(yè)虛擬機(jī),是將內(nèi)存劃分為一塊較大的 Eden 空間和兩塊較小的 Survivor 空間,每次使用 Eden 和其中一塊 Survivor。當(dāng)回收時,將 Eden 和 Survivor 中還存活的對象復(fù)制到另一塊 Survivor 上,最后清理掉 Eden 和使用過的 Survivor。
HotSpot 虛擬機(jī)默認(rèn) Eden 和 Survivor 的大小比例是 8:1。
分配擔(dān)保機(jī)制:
當(dāng)另一塊 Survivor 沒有足夠空間來存放存活對象時,則需要其他內(nèi)存(老年代)進(jìn)行分配擔(dān)保,將對象移入其他內(nèi)存(老年代)。
3、標(biāo)記-整理算法
首先標(biāo)記出所有需要回收的對象,然后將所有存活對象向一端移動,最后直接清理掉端邊界以外的內(nèi)存。
4、分代收集算法
根據(jù)對象存活周期的不同,將 Java 堆劃分為新生代和老年代,然后根據(jù)各個年代的特點采用最適當(dāng)?shù)氖占惴ā?/p>
- 新生代:采用復(fù)制算法。因為新生代每次 GC 都有大量對象死去,故只需付出少量存活對象的復(fù)制成本即可完成 GC。
- 老年代:采用“標(biāo)記-清除”或“標(biāo)記-整理”算法。因為老年代中對象存活率高,而且沒有額外空間進(jìn)行分配擔(dān)保。
三、HotSpot 的算法實現(xiàn)
1、枚舉根節(jié)點
可達(dá)性分析時,需要枚舉 GC Roots 節(jié)點,以便標(biāo)記出所有的不可用對象。
可作為 GC Roots 的節(jié)點主要在全局引用(例如常量或類靜態(tài)屬性)與執(zhí)行上下文(例如棧幀中的本地變量表)中。如果逐個檢查里面的引用,會消耗很多時間。因此,目前主流的 Java 虛擬機(jī)使用準(zhǔn)確式 GC 來完成 GC Roots 枚舉。
Stop The World(STW):
可達(dá)性分析期間,不可以出現(xiàn)對象引用關(guān)系還在不斷變化的情況。因此 GC 時,必須停頓所有 Java 執(zhí)行線程,此時整個執(zhí)行系統(tǒng)看起來就像被凍結(jié)某個時間點上。
準(zhǔn)確式 GC:
虛擬機(jī)可以直接得知哪些地方存放著對象引用,因此 STW 時,不需要一個不漏地檢查所有執(zhí)行上下文和全局的引用位置。
HotSpot 中準(zhǔn)確式 GC 的實現(xiàn):
HotSpot 使用一組稱為 OopMap 的數(shù)據(jù)結(jié)構(gòu)來記錄對象的引用位置。這樣,GC 在掃描時就可以直接得知對象的引用位置信息。
類加載完成時,HotSpot 會把對象內(nèi)什么偏移量上是什么類型的數(shù)據(jù)計算出來記錄到 OopMap 中。JIT 編譯過程中,也會在 OopMap 中記錄下棧和寄存器中哪些位置是引用。
2、安全點
HotSpot 只在特定的位置上記錄了 OopMap,這些位置稱為安全點。
程序執(zhí)行時,只有到達(dá)安全點才能停頓下來進(jìn)行 GC。因為只有到達(dá)安全點,才能訪問到 OopMap 記錄。
如何在 GC 時讓線程跑到最近的安全點再停頓下來:
- 搶先式中斷:GC 時,先中斷所有線程,如果發(fā)現(xiàn)有線程中斷的地方不在安全點上,就恢復(fù)線程,讓它跑到安全點上。
- 主動式中斷:GC 時,設(shè)置一個中斷標(biāo)志,各個線程執(zhí)行時主動去輪詢這個標(biāo)志,發(fā)現(xiàn)中斷標(biāo)志為真時就自己中斷掛起。
3、安全區(qū)域
安全區(qū)域是指一段代碼片段中,引用關(guān)系不會發(fā)生變化。在這個區(qū)域中的任意地方開始 GC 都是安全的??梢园寻踩珔^(qū)域看做是被擴(kuò)展了的安全點。
為什么需要安全區(qū)域:
當(dāng)線程沒有分配 CPU 時間時,將無法響應(yīng) JVM 的中斷請求,跑到安全點中斷掛起,JVM 也不太可能等待線程重新被分配 CPU 時間。這種情況就需要安全區(qū)域來解決。
安全區(qū)域的使用:
- 線程執(zhí)行到安全區(qū)域的代碼時,標(biāo)識自己進(jìn)入了安全區(qū)域。
- JVM 發(fā)起 GC 時,不用管進(jìn)入安全區(qū)域的線程。
- 線程要離開安全區(qū)域時,必須檢查系統(tǒng)是否完成了根節(jié)點枚舉(或整個 GC 過程)。如果完成了,線程就繼續(xù)執(zhí)行,否則必須等待,直到收到可以離開安全區(qū)域的信號。
四、垃圾收集器
1、Serial 收集器
- 最基本、歷史最悠久的收集器。
- 單線程收集器:使用一個 CPU 或一條線程進(jìn)行垃圾收集。
- 新生代收集器,是運(yùn)行在 Client 模式下的虛擬機(jī)的默認(rèn)新生代收集器。
- 簡單而高效,單個 CPU 下,沒有線程交互的開銷。
2、ParNew 收集器
- Serial 收集器的多線程版本。
- 新生代收集器,是許多運(yùn)行在 Server 模式下的虛擬機(jī)中首選的新生代收集器。
- 除了 Serial 收集器外,目前只有它能與 CMS 收集器配合工作。
- 默認(rèn)開啟的收集線程數(shù)與 CPU 數(shù)量相同。
3、Parallel Scavenge 收集器
- 多線程收集器。
- 新生代收集器。
- 關(guān)注吞吐量,即 CPU 用于運(yùn)行用戶代碼的時間與 CPU 總消耗時間的比值。高吞吐量可以高效利用 CPU 時間,盡快完成程序的運(yùn)算任務(wù),適合于在后臺運(yùn)算而不需要太多交互的任務(wù)。
- 可開啟自適應(yīng)調(diào)節(jié)策略,把內(nèi)存管理的調(diào)優(yōu)任務(wù)交給虛擬機(jī)去完成。
自適應(yīng)調(diào)節(jié)策略:
虛擬機(jī)根據(jù)當(dāng)前系統(tǒng)的運(yùn)行情況收集性能監(jiān)控信息,動態(tài)調(diào)整虛擬機(jī)參數(shù)以提供最合適的停頓時間或最大的吞吐量。
4、Serial Old 收集器
- Serial 收集器的老年代版本。
- 單線程收集器。
- 使用“標(biāo)記-整理”算法。
- 給 Client 模式下的虛擬機(jī)使用。
5、Parallel Old 收集器
- Parallel Scavenge 收集器的老年代版本。
- 多線程收集器。
- 使用“標(biāo)記-整理”算法。
- 在注重吞吐量以及 CPU 資源敏感的場合,可優(yōu)先考慮 Parallel Scavenge 加 Parallel Old 收集器。
6、CMS 收集器
- CMS:Concurrent Mark Sweep。
- 并發(fā)收集器:垃圾收集線程與用戶線程(基本上)同時工作。
- 使用“標(biāo)記-清除”算法。
- 關(guān)注點是如何縮短垃圾收集時用戶線程的停頓時間。停頓時間短意味著響應(yīng)速度快,因此它適合于需要與用戶交互的應(yīng)用。
CMS 運(yùn)作過程:
- 初始標(biāo)記:標(biāo)記 GC Roots 能直接關(guān)聯(lián)到的對象,需要 STW。
- 并發(fā)標(biāo)記:進(jìn)行 GC Roots Tracing 的過程,即可達(dá)性分析。
- 重新標(biāo)記:修正并發(fā)標(biāo)記期間引用關(guān)系發(fā)生變化的那一部分對象的標(biāo)記記錄,需要 STW。
- 并發(fā)清除:清除垃圾對象。
CMS 的缺點:
- 對 CPU 資源非常敏感。并發(fā)階段雖然不會導(dǎo)致用戶線程停頓,但是會因為占用了一部分線程(或者說 CPU 資源)導(dǎo)致應(yīng)用程序變慢,總吞吐量會降低。
- 無法處理浮動垃圾。并發(fā)清除階段產(chǎn)生的垃圾稱為“浮動垃圾”,這部分垃圾只能等下次 GC 再清除。
- 會產(chǎn)生大量內(nèi)存碎片。內(nèi)存碎片過多時會提前觸發(fā) Full GC,CMS 收集器默認(rèn)會在 Full GC 時開啟內(nèi)存碎片的合并整理過程。
7、G1 收集器
- G1:Garbage-First。
- 是一款面向服務(wù)端應(yīng)用的垃圾收集器。
G1 特點:
- 并行與并發(fā)
- 分代收集
- 空間整合:G1 從整體上看是基于“標(biāo)記-整理”算法,從局部上(兩個 Region 之間)看是基于復(fù)制算法。因此,不會產(chǎn)生內(nèi)存空間碎片。
- 可預(yù)測的停頓:G1 能通過建立可預(yù)測的停頓時間模型,讓使用者明確指定在 M 毫秒的時間片段內(nèi),消耗在垃圾收集上的時間不得超過 N 毫秒。
Region:
G1 將整個 Java 堆劃分為多個大小相等的獨立區(qū)域(Region),雖然還保留新生代和老年代的概念,但新生代和老年代不再是物理隔離的,而是一部分 Region(不需要連續(xù))的集合。
可預(yù)測的時間停頓模型:
G1 之所以能建立可預(yù)測的時間停頓模型,是因為它可以有計劃地避免在整個 Java 堆中進(jìn)行全區(qū)域的垃圾收集。
G1 跟蹤各個 Region 的垃圾堆積的價值大小(回收所獲得的空間大小及所需時間),在后臺維護(hù)一個優(yōu)先列表,每次根據(jù)允許的收集時間,優(yōu)先回收價值最大的 Region(Garbage-First 名稱的由來)。
G1 運(yùn)作過程:
- 初始標(biāo)記:標(biāo)記 GC Roots 能直接關(guān)聯(lián)的對象,并修改 TAMS(Next Top at Mark Start)的值,讓下一階段用戶程序并發(fā)運(yùn)行時,能在正確可用的 Region 中創(chuàng)建新對象。需要 STW。
- 并發(fā)標(biāo)記:進(jìn)行可達(dá)性分析。
- 最終標(biāo)記:修正并發(fā)標(biāo)記期間引用關(guān)系發(fā)生變化的那一部分對象的標(biāo)記記錄。需要 STW。
- 篩選回收:對各個 Region 的回收價值和成本進(jìn)行排序,根據(jù)用戶所期望的 GC 停頓時間制定回收計劃。
五、內(nèi)存分配與回收策略
1、對象優(yōu)先在 Eden 分配
- 大多數(shù)情況下,對象在新生代的 Eden 區(qū)中分配。
- 當(dāng) Eden 區(qū)沒有足夠空間進(jìn)行分配時,虛擬機(jī)將發(fā)起一次 Minor GC。
2、大對象直接進(jìn)入老年代
- 大對象是指需要大量連續(xù)內(nèi)存空間的 Java 對象。
- 經(jīng)常出現(xiàn)大對象容易導(dǎo)致內(nèi)存還有不少空間時,就提前觸發(fā) GC 以獲取足夠的連續(xù)空間來安置它們。
- 由于新生代采用復(fù)制算法收集內(nèi)存,因此為了避免在 Eden 區(qū)及兩個 Survivor 區(qū)之間發(fā)生大量的內(nèi)存復(fù)制,大對象將直接進(jìn)入老年代。
3、 長期存活的對象進(jìn)入老年代
- 虛擬機(jī)給每個對象定義了一個對象年齡計數(shù)器。
- 對象在 Eden 出生并經(jīng)過一次 Minor GC 后仍然存活,并且能被 Survivor 容納的話,將移入 Survivor 中,并且對象年齡設(shè)為 1。
- 對象在 Survivor 中每“熬過”一次 Minor GC,則年齡加 1,當(dāng)對象年齡增加到一定程度(默認(rèn) 15 歲),將會晉升到老年代。
4、動態(tài)對象年齡判定
- 為了更好地適應(yīng)不同程序的內(nèi)存狀況,虛擬機(jī)并不要求對象必須達(dá)到某個年齡才能晉升老年代。
- 如果 Survivor 中相同年齡的對象大小總和,大于 Survivor 空間的一半,則大于等于該年齡的對象直接進(jìn)入老年代。
5、空間分配擔(dān)保
- 當(dāng)出現(xiàn)大量對象在 Minor GC 后仍然存活的情況,就需要老年代進(jìn)行分配擔(dān)保,讓 Survivor 無法容納的對象直接進(jìn)入老年代。