《深入理解 Java 虛擬機(jī)》讀書筆記:垃圾收集器與內(nèi)存分配策略

正文

垃圾收集器關(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)記過程,才會被回收。

  1. 發(fā)現(xiàn)沒有與 GC Roots 相連的引用鏈時,進(jìn)行第一次標(biāo)記。
  2. 當(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ū)域的使用:

  1. 線程執(zhí)行到安全區(qū)域的代碼時,標(biāo)識自己進(jìn)入了安全區(qū)域。
  2. JVM 發(fā)起 GC 時,不用管進(jìn)入安全區(qū)域的線程。
  3. 線程要離開安全區(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)作過程:

  1. 初始標(biāo)記:標(biāo)記 GC Roots 能直接關(guān)聯(lián)到的對象,需要 STW。
  2. 并發(fā)標(biāo)記:進(jìn)行 GC Roots Tracing 的過程,即可達(dá)性分析。
  3. 重新標(biāo)記:修正并發(fā)標(biāo)記期間引用關(guān)系發(fā)生變化的那一部分對象的標(biāo)記記錄,需要 STW。
  4. 并發(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)作過程:

  1. 初始標(biāo)記:標(biāo)記 GC Roots 能直接關(guān)聯(lián)的對象,并修改 TAMS(Next Top at Mark Start)的值,讓下一階段用戶程序并發(fā)運(yùn)行時,能在正確可用的 Region 中創(chuàng)建新對象。需要 STW。
  2. 并發(fā)標(biāo)記:進(jìn)行可達(dá)性分析。
  3. 最終標(biāo)記:修正并發(fā)標(biāo)記期間引用關(guān)系發(fā)生變化的那一部分對象的標(biāo)記記錄。需要 STW。
  4. 篩選回收:對各個 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)入老年代。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容