《深刻理解 Java 虛擬機》讀書筆記:垃圾收集器與內存分配策略

正文

垃圾收集器關注的是 Java 堆和方法區,由於這部份內存的分配和回收是動態的。只有在程序處於運行期間時才能知道會建立哪些對象,也才能知道須要多少內存。java

虛擬機棧和本地方法棧則不須要過多考慮回收的問題,由於棧中每個棧幀分配多少內存基本上是在類結構肯定下來時就已知的,所以這幾個區域的內存分配和回收具備肯定性。算法

1、對象已死嗎

垃圾收集器在對堆進行回收前,第一件事就是要肯定堆中對象哪些還「存活」着,哪些已「死去」(即不可能再被任何途徑使用的對象)。安全

一、 引用計數算法

給對象添加一個引用計數器,每當有一個地方引用它時,計數器值加 1;當引用失效時,計數器值減 1;任什麼時候刻計數器爲 0 的對象就是不可能再被使用的。數據結構

優勢:實現簡單,斷定效率高。多線程

缺點:很難解決對象之間相互循環引用的問題。併發

二、可達性分析算法

經過一系列被稱爲「GC Roots」的對象做爲起點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈,當一個對象到 GC Roots 沒有任何引用鏈相連時,則此對象不可用。性能

Java 語言中,可做爲 GC Roots 的對象:線程

  • 虛擬機棧(棧幀中的本地變量表)中引用的對象。
  • 方法區中類靜態屬性引用的對象。
  • 方法區中常量引用對象。
  • 本地方法棧中 JNI(即 Native 方法)引用的對象。

可達性分析算法中不可達的對象,至少要經歷兩次標記過程,纔會被回收。對象

  1. 發現沒有與 GC Roots 相連的引用鏈時,進行第一次標記。
  2. 當對象覆蓋了 finalize() 方法,而且沒有被調用過期,將會被放入一個叫作 F-Queue 的隊列中,稍後 GC 將對 F-Queue 中的對象進行第二次標記。若是在 finalize() 方法中,對象沒有從新與引用鏈上的一個對象創建關聯,那麼將會被回收。

三、四種引用

不管是引用計數算法,仍是可達性分析算法,判斷對象是否存活都與「引用」有關。Java 中有 4 種引用,按強度由強至弱依次爲:強引用、軟引用、弱引用、虛引用。排序

  • 強引用:相似「Object obj = new Object()」的引用。只要強引用還存在,對象就永遠不會回收。
  • 軟引用:用來描述一些還有用但並不是必需的對象。內存不足時,對象有可能被回收。可經過 SoftReference 類實現軟引用。
  • 弱引用:用來描述非必需的對象,但強度比軟引用弱。GC時,不管內存是否足夠,對象都會被回收。可經過 WeakReference 類來實現弱引用。
  • 虛引用:也稱幽靈引用或幻影引用,虛引用不會對對象的生存時間構成影響。虛引用的惟一做用就是能在對象被回收時收到一個系統通知。可經過 PhantomReference 類實現虛引用。

四、回收方法區

永久代的垃圾收集主要回收兩部份內容:廢棄常量和無用的類。

如何斷定廢棄常量:

  • 常量池中的常量(字面量、符號引用)沒有在任何地方被引用。

如何斷定無用的類:

  • 該類的全部實例都已被回收。
  • 加載該類的 ClassLoader 已被回收。
  • 該類對應的 java.lang.Class 對象沒有在任何地方被引用,沒法在任何地方經過反射訪問該類的方法。

2、垃圾收集算法

一、標記-清除算法

分爲「標記」和「清除」兩個階段。首先標記出全部須要回收的對象,而後再統一回收全部被標記的對象。

該算法會產生大量不連續的內存碎片,於是在分配較大對象時,可能會因爲沒法找到足夠的連續內存而不得不提早觸發一次 GC。

二、複製算法

將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中一塊。當一塊內存用完時,就將還存活的對象複製到另外一塊,而後再把已使用過的內存空間一次清理掉。

該算法的代價是始終會有一塊內存被「浪費」掉。

因爲新生代的對象 98% 是「朝生夕死」,所以並不須要按 1:1 的比例來劃份內存空間。如今的商業虛擬機,是將內存劃分爲一塊較大的 Eden 空間和兩塊較小的 Survivor 空間,每次使用 Eden 和其中一塊 Survivor。當回收時,將 Eden 和 Survivor 中還存活的對象複製到另外一塊 Survivor 上,最後清理掉 Eden 和使用過的 Survivor。

HotSpot 虛擬機默認 Eden 和 Survivor 的大小比例是 8:1。

分配擔保機制:
當另外一塊 Survivor 沒有足夠空間來存放存活對象時,則須要其餘內存(老年代)進行分配擔保,將對象移入其餘內存(老年代)。

三、標記-整理算法

首先標記出全部須要回收的對象,而後將全部存活對象向一端移動,最後直接清理掉端邊界之外的內存。

四、分代收集算法

根據對象存活週期的不一樣,將 Java 堆劃分爲新生代和老年代,而後根據各個年代的特色採用最適當的收集算法。

  • 新生代:採用複製算法。由於新生代每次 GC 都有大量對象死去,故只需付出少許存活對象的複製成本便可完成 GC。
  • 老年代:採用「標記-清除」或「標記-整理」算法。由於老年代中對象存活率高,並且沒有額外空間進行分配擔保。

3、HotSpot 的算法實現

一、枚舉根節點

可達性分析時,須要枚舉 GC Roots 節點,以便標記出全部的不可用對象。

可做爲 GC Roots 的節點主要在全局引用(例如常量或類靜態屬性)與執行上下文(例如棧幀中的本地變量表)中。若是逐個檢查裏面的引用,會消耗不少時間。所以,目前主流的 Java 虛擬機使用準確式 GC 來完成 GC Roots 枚舉。

Stop The World(STW):
可達性分析期間,不能夠出現對象引用關係還在不斷變化的狀況。所以 GC 時,必須停頓全部 Java 執行線程,此時整個執行系統看起來就像被凍結某個時間點上。

準確式 GC:
虛擬機能夠直接得知哪些地方存放着對象引用,所以 STW 時,不須要一個不漏地檢查全部執行上下文和全局的引用位置。

HotSpot 中準確式 GC 的實現:
HotSpot 使用一組稱爲 OopMap 的數據結構來記錄對象的引用位置。這樣,GC 在掃描時就能夠直接得知對象的引用位置信息。

類加載完成時,HotSpot 會把對象內什麼偏移量上是什麼類型的數據計算出來記錄到 OopMap 中。JIT 編譯過程當中,也會在 OopMap 中記錄下棧和寄存器中哪些位置是引用。

二、安全點

HotSpot 只在特定的位置上記錄了 OopMap,這些位置稱爲安全點。

程序執行時,只有到達安全點才能停頓下來進行 GC。由於只有到達安全點,才能訪問到 OopMap 記錄。

如何在 GC 時讓線程跑到最近的安全點再停頓下來:

  • 搶先式中斷:GC 時,先中斷全部線程,若是發現有線程中斷的地方不在安全點上,就恢復線程,讓它跑到安全點上。
  • 主動式中斷:GC 時,設置一箇中斷標誌,各個線程執行時主動去輪詢這個標誌,發現中斷標誌爲真時就本身中斷掛起。

三、安全區域

安全區域是指一段代碼片斷中,引用關係不會發生變化。在這個區域中的任意地方開始 GC 都是安全的。能夠把安全區域看作是被擴展了的安全點。

爲何須要安全區域:
當線程沒有分配 CPU 時間時,將沒法響應 JVM 的中斷請求,跑到安全點中斷掛起,JVM 也不太可能等待線程從新被分配 CPU 時間。這種狀況就須要安全區域來解決。

安全區域的使用:

  1. 線程執行到安全區域的代碼時,標識本身進入了安全區域。
  2. JVM 發起 GC 時,不用管進入安全區域的線程。
  3. 線程要離開安全區域時,必須檢查系統是否完成了根節點枚舉(或整個 GC 過程)。若是完成了,線程就繼續執行,不然必須等待,直到收到能夠離開安全區域的信號。

4、垃圾收集器

一、Serial 收集器

  • 最基本、歷史最悠久的收集器。
  • 單線程收集器:使用一個 CPU 或一條線程進行垃圾收集。
  • 新生代收集器,是運行在 Client 模式下的虛擬機的默認新生代收集器。
  • 簡單而高效,單個 CPU 下,沒有線程交互的開銷。

二、ParNew 收集器

  • Serial 收集器的多線程版本。
  • 新生代收集器,是許多運行在 Server 模式下的虛擬機中首選的新生代收集器。
  • 除了 Serial 收集器外,目前只有它能與 CMS 收集器配合工做。
  • 默認開啓的收集線程數與 CPU 數量相同。

三、Parallel Scavenge 收集器

  • 多線程收集器。
  • 新生代收集器。
  • 關注吞吐量,即 CPU 用於運行用戶代碼的時間與 CPU 總消耗時間的比值。高吞吐量能夠高效利用 CPU 時間,儘快完成程序的運算任務,適合於在後臺運算而不須要太多交互的任務。
  • 可開啓自適應調節策略,把內存管理的調優任務交給虛擬機去完成。

自適應調節策略:
虛擬機根據當前系統的運行狀況收集性能監控信息,動態調整虛擬機參數以提供最合適的停頓時間或最大的吞吐量。

四、Serial Old 收集器

  • Serial 收集器的老年代版本。
  • 單線程收集器。
  • 使用「標記-整理」算法。
  • 給 Client 模式下的虛擬機使用。

五、Parallel Old 收集器

  • Parallel Scavenge 收集器的老年代版本。
  • 多線程收集器。
  • 使用「標記-整理」算法。
  • 在注重吞吐量以及 CPU 資源敏感的場合,可優先考慮 Parallel Scavenge 加 Parallel Old 收集器。

六、CMS 收集器

  • CMS:Concurrent Mark Sweep。
  • 併發收集器:垃圾收集線程與用戶線程(基本上)同時工做。
  • 使用「標記-清除」算法。
  • 關注點是如何縮短垃圾收集時用戶線程的停頓時間。停頓時間短意味着響應速度快,所以它適合於須要與用戶交互的應用。

CMS 運做過程:

  1. 初始標記:標記 GC Roots 能直接關聯到的對象,須要 STW。
  2. 併發標記:進行 GC Roots Tracing 的過程,便可達性分析。
  3. 從新標記:修正併發標記期間引用關係發生變化的那一部分對象的標記記錄,須要 STW。
  4. 併發清除:清除垃圾對象。

CMS 的缺點:

  • 對 CPU 資源很是敏感。併發階段雖然不會致使用戶線程停頓,可是會由於佔用了一部分線程(或者說 CPU 資源)致使應用程序變慢,總吞吐量會下降。
  • 沒法處理浮動垃圾。併發清除階段產生的垃圾稱爲「浮動垃圾」,這部分垃圾只能等下次 GC 再清除。
  • 會產生大量內存碎片。內存碎片過多時會提早觸發 Full GC,CMS 收集器默認會在 Full GC 時開啓內存碎片的合併整理過程。

七、G1 收集器

  • G1:Garbage-First。
  • 是一款面向服務端應用的垃圾收集器。

G1 特色:

  • 並行與併發
  • 分代收集
  • 空間整合:G1 從總體上看是基於「標記-整理」算法,從局部上(兩個 Region 之間)看是基於複製算法。所以,不會產生內存空間碎片。
  • 可預測的停頓:G1 能經過創建可預測的停頓時間模型,讓使用者明確指定在 M 毫秒的時間片斷內,消耗在垃圾收集上的時間不得超過 N 毫秒。

Region:
G1 將整個 Java 堆劃分爲多個大小相等的獨立區域(Region),雖然還保留新生代和老年代的概念,但新生代和老年代再也不是物理隔離的,而是一部分 Region(不須要連續)的集合。

可預測的時間停頓模型:
G1 之因此能創建可預測的時間停頓模型,是由於它能夠有計劃地避免在整個 Java 堆中進行全區域的垃圾收集。

G1 跟蹤各個 Region 的垃圾堆積的價值大小(回收所得到的空間大小及所需時間),在後臺維護一個優先列表,每次根據容許的收集時間,優先回收價值最大的 Region(Garbage-First 名稱的由來)。

G1 運做過程:

  1. 初始標記:標記 GC Roots 能直接關聯的對象,並修改 TAMS(Next Top at Mark Start)的值,讓下一階段用戶程序併發運行時,能在正確可用的 Region 中建立新對象。須要 STW。
  2. 併發標記:進行可達性分析。
  3. 最終標記:修正併發標記期間引用關係發生變化的那一部分對象的標記記錄。須要 STW。
  4. 篩選回收:對各個 Region 的回收價值和成本進行排序,根據用戶所指望的 GC 停頓時間制定回收計劃。

5、內存分配與回收策略

一、對象優先在 Eden 分配

  • 大多數狀況下,對象在新生代的 Eden 區中分配。
  • 當 Eden 區沒有足夠空間進行分配時,虛擬機將發起一次 Minor GC。

二、大對象直接進入老年代

  • 大對象是指須要大量連續內存空間的 Java 對象。
  • 常常出現大對象容易致使內存還有很多空間時,就提早觸發 GC 以獲取足夠的連續空間來安置它們。
  • 因爲新生代採用複製算法收集內存,所以爲了不在 Eden 區及兩個 Survivor 區之間發生大量的內存複製,大對象將直接進入老年代。

三、長期存活的對象進入老年代

  • 虛擬機給每一個對象定義了一個對象年齡計數器。
  • 對象在 Eden 出生並通過一次 Minor GC 後仍然存活,而且能被 Survivor 容納的話,將移入 Survivor 中,而且對象年齡設爲 1。
  • 對象在 Survivor 中每「熬過」一次 Minor GC,則年齡加 1,當對象年齡增長到必定程度(默認 15 歲),將會晉升到老年代。

四、動態對象年齡斷定

  • 爲了更好地適應不一樣程序的內存情況,虛擬機並不要求對象必須達到某個年齡才能晉升老年代。
  • 若是 Survivor 中相同年齡的對象大小總和,大於 Survivor 空間的一半,則大於等於該年齡的對象直接進入老年代。

五、空間分配擔保

  • 當出現大量對象在 Minor GC 後仍然存活的狀況,就須要老年代進行分配擔保,讓 Survivor 沒法容納的對象直接進入老年代。
相關文章
相關標籤/搜索