JVM學習(二)——GC垃圾回收機制

其餘更多java基礎文章:
java基礎學習(目錄)html


經過前一篇JVM學習(一)——內存結構對JVM內存結構的講解。咱們知道程序計數器、虛擬機棧、本地方法棧3個區域隨線程而生,隨線程而滅,在這幾個區域內就不須要過多考慮回收的問題,由於方法結束或者線程結束時,內存天然就跟隨着回收了。棧中的棧幀隨着方法的進入和退出就有條不紊的執行者出棧和入棧的操做,每個棧分配多少個內存基本都是在類結構肯定下來的時候就已經肯定了,這幾個區域內存分配和回收都具備肯定性java

而堆和方法區則不一樣,一個接口的實現是多種多樣的,多個實現類須要的內存可能不同,一個方法中多個分支須要的內存也不同,咱們只能在程序運行的期間知道須要建立那些對象,分配多少內存,這部分的內存分配和回收都是動態的。這篇所講的GC垃圾回收機制就是回收堆和方法區數據的機制。算法

1.判斷對象存活

1.1 引用計數器法

給對象添加一個引用計數器,每當由一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任什麼時候刻計數器爲0的對象就是不可能再被使用的。
引用計數法有一個重大的漏洞,那即是沒法處理循環引用對象。舉個例子,假設對象 a 與 b 相互引用,除此以外沒有其餘引用指向 a 或者 b。在這種狀況下,a 和 b 實際上已經死了,但因爲它們的引用計數器皆不爲 0,在引用計數法的心中,這兩個對象還活 着。所以,這些循環引用對象所佔據的空間將不可回收,從而形成了內存泄露。數組

1.2 可達性分析算法

經過一系列的成爲「GC Roots」的對象做爲起始點,從這些節點開始向下搜索,搜索所走過的路徑成爲引用鏈,當一個對象到GC ROOTS沒有任何引用鏈相連時,則證實此對象時不可用的 Java語言中GC Roots的對象包括(包括但不限於)下面幾種:安全

  1. 虛擬機棧(棧幀中的本地變量表)中引用的對象
  2. 方法區中類靜態屬性引用的對象
  3. 方法區中常量引用的對象
  4. 本地方法棧JNI(Native方法)引用的對象

雖然可達性分析的算法自己很簡明,可是在實踐中仍是有很多其餘問題須要解決的。
好比說,在多線程環境下,其餘線程可能會更新已經訪問過的對象中的引用,從而形成誤報(將引 用設置爲 null)或者漏報(將引用設置爲未被訪問過的對象)。多線程

誤報並無什麼傷害,Java 虛擬機至多損失了部分垃圾回收的機會。漏報則比較麻煩,由於垃圾回 收器可能回收事實上仍被引用的對象內存。一旦從原引用訪問已經被回收了的對象,則頗有可能會 直接致使 Java 虛擬機崩潰。併發

1.3 Stop-the-world 以及安全點

怎麼解決這個問題呢?在 Java 虛擬機裏,傳統的垃圾回收算法採用的是一種簡單粗暴的方式,那便 是 Stop-the-world,中止其餘非垃圾回收線程的工做,直到完成垃圾回收。這也就形成了垃圾回收 所謂的暫停時間(GC pause)。源碼分析

Java 虛擬機中的 Stop-the-world 是經過安全點(safepoint)機制來實現的。當 Java 虛擬機收到 Stop-the-world 請求,它便會等待全部的線程都到達安全點,才容許請求 Stop-the-world 的線程 進行獨佔的工做。post

safepoint 安全點顧名思義是指一些特定的位置,當線程運行到這些位置時,線程的一些狀態能夠被肯定(the thread's representation of it's Java machine state is well described),好比記錄OopMap的狀態,從而肯定GC Root的信息,使JVM能夠安全的進行一些操做,好比開始GC。性能

safepoint指的特定位置主要有:

  1. 循環的末尾 (防止大循環的時候一直不進入safepoint,而其餘線程在等待它進入safepoint)
  2. 方法返回前
  3. 調用方法的call以後
  4. 拋出異常的位置

之因此選擇這些位置做爲safepoint的插入點,主要的考慮是「避免程序長時間運行而不進入safepoint」,好比GC的時候必需要等到Java線程都進入到safepoint的時候VMThread才能開始執行GC。

參考資料:
聊聊JVM(六)理解JVM的safepoint
JVM源碼分析之安全點safepoint

2. JVM垃圾回收算法

常見的垃圾回收算法包括:標記-清除算法,複製算法,標記-整理算法,分代收集算法。

2.1 標記—清除算法(Mark-Sweep)

之因此說標記/清除算法是幾種GC算法中最基礎的算法,是由於後續的收集算法都是基於這種思路並對其不足進行改進而獲得的。標記/清除算法的基本思想就跟它的名字同樣,分爲「標記」和「清除」兩個階段:首先標記出全部須要回收的對象,在標記完成後統一回收全部被標記的對象。

標記階段:標記的過程其實就是前面介紹的可達性分析算法的過程,遍歷全部的GC Roots對象,對從GC Roots對象可達的對象都打上一個標識,通常是在對象的header中,將其記錄爲可達對象;

清除階段:清除的過程是對堆內存進行遍歷,若是發現某個對象沒有被標記爲可達對象(經過讀取對象header信息),則將其回收。

不足:

  • 標記和清除過程效率都不高
  • 會產生大量碎片,內存碎片過多可能致使沒法給大對象分配內存。

2.2 複製算法(Copying)

將內存劃分爲大小相等的兩塊,每次只使用其中一塊,當這一塊內存用完了就將還存活的對象複製到另外一塊上面,而後再把使用過的內存空間進行一次清理。

如今的商業虛擬機都採用這種收集算法來回收新生代,可是並非將內存劃分爲大小相等的兩塊,而是分爲一塊較大的 Eden 空間和兩塊較小的 Survior 空間,每次使用 Eden 空間和其中一塊 Survivor。在回收時,將 Eden 和 Survivor 中還存活着的對象一次性複製到另外一塊 Survivor 空間上,最後清理 Eden 和 使用過的那一塊 Survivor。HotSpot 虛擬機的 Eden 和 Survivor 的大小比例默認爲 8:1,保證了內存的利用率達到 90 %。若是每次回收有多於 10% 的對象存活,那麼一塊 Survivor 空間就不夠用了,此時須要依賴於老年代進行分配擔保,也就是借用老年代的空間。

不足:

  • 將內存縮小爲原來的一半,浪費了一半的內存空間,代價過高;若是不想浪費一半的空間,就須要有額外的空間進行分配擔保,以應對被使用的內存中全部對象都100%存活的極端狀況,因此在老年代通常不能直接選用這種算法。
  • 複製收集算法在對象存活率較高時就要進行較多的複製操做,效率將會變低。

2.3 標記—整理算法(Mark-Compact)

標記—整理算法和標記—清除算法同樣,可是標記—整理算法不是把存活對象複製到另外一塊內存,而是把存活對象往內存的一端移動,而後直接回收邊界之外的內存,所以其不會產生內存碎片。標記—整理算法提升了內存的利用率,而且它適合在收集對象存活時間較長的老年代。

不足:

效率不高,不只要標記存活對象,還要整理全部存活對象的引用地址,在效率上不如複製算法。

2.4 分代收集算法(Generational Collection)

分代回收算法其實是把複製算法和標記整理法的結合,並非真正一個新的算法,通常分爲:老年代(Old Generation)和新生代(Young Generation),老年代就是不多垃圾須要進行回收的,新生代就是有不少的內存空間須要回收,因此不一樣代就採用不一樣的回收算法,以此來達到高效的回收算法。

新生代:因爲新生代產生不少臨時對象,大量對象須要進行回收,因此採用複製算法是最高效的。

老年代:回收的對象不多,都是通過幾回標記後都不是可回收的狀態轉移到老年代的,因此僅有少許對象須要回收,故採用標記清除或者標記整理算法。

3. JVM中的GC過程

3.1 JVM中的堆分區

在 Java 中,堆被劃分紅兩個不一樣的區域:新生代 ( Young )、老年代 ( Old )。 默認的,新生代 ( Young ) 與老年代 ( Old ) 的比例的值爲 1:2( 該值能夠經過參數 –XX:NewRatio 來指定 )
新生代 ( Young ) 又被劃分爲 三個區域:Eden、From Survivor、To Survivor。這樣劃分的目的是爲了使 JVM 可以更好的管理堆內存中的對象,包括內存的分配以及回收。

3.1.1 新生代

主要是用來存放新生的對象。通常佔據堆的1/3空間。因爲頻繁建立對象,因此新生代會頻繁觸發MinorGC進行垃圾回收。 新生代又分爲 Eden區、ServivorFrom、ServivorTo三個區,默認比例8:1:1

  • Eden區:Java新對象的出生地(若是新建立的對象佔用內存很大,則直接分配到老年代)。當Eden區內存不夠的時候就會觸發MinorGC,對新生代區進行一次垃圾回收。
  • ServivorTo:保留了一次MinorGC過程當中的倖存者。
  • ServivorFrom:上一次GC的倖存者,做爲這一次GC的被掃描者。

MinorGC的過程:MinorGC採用複製算法。首先,把Eden和ServivorFrom區域中存活的對象複製到ServicorTo區域(若是有對象的年齡以及達到了老年的標準,則賦值到老年代區),同時把這些對象的年齡+1(若是ServicorTo不夠位置了就放到老年區);而後,清空Eden和ServicorFrom中的對象;最後,ServicorTo和ServicorFrom互換,原ServicorTo成爲下一次GC時的ServicorFrom區。

3.1.2 老年代

老年代的對象比較穩定,因此fullGC不會頻繁執行。在進行fullGC前通常都先進行了一次MinorGC,使得有新生代的對象晉身入老年代,致使空間不夠用時才觸發。當沒法找到足夠大的連續空間分配給新建立的較大對象時也會提早觸發一次fullGC進行垃圾回收騰出空間。
fullGC根據不一樣垃圾回收器採用標記—清除算法或標記-整理算法:首先掃描一次全部老年代,標記出存活的對象,而後回收沒有標記的對象。fullGC的耗時比較長,由於要掃描再回收。fullGC會產生內存碎片,爲了減小內存損耗,咱們通常須要進行整理或者標記出來方便下次直接分配。
當老年代也滿了裝不下的時候,就會拋出OOM(Out of Memory)異常。

3.2 JVM對象分配策略

3.2.1 對象優先在Eden分配

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

3.2.2 大對象直接進入年老代

大對象即須要大量連續內存空間的Java對象,如長字符串及數組。常常出現大對象致使內存還有很多空間時就提早觸發垃圾收集以獲取足夠的連續空間來安置他們。 虛擬機提供了一個-XX:PretenureSizeThreshold參數,令大於這個設置值的對象直接在老年代分配。 這樣作的目的是避免在Eden區及兩個Survivor區之間發生大量的內存複製(新生代採用複製算法收集內存)。

3.2.3 長期存活的對象將進入年老代

虛擬機給每一個對象定義了一個對象年齡計數器,在對象在Eden建立並通過第一次Minor GC後仍然存活,並能被Suivivor容納的話,將會被移動到Survivor空間,並對象年齡設置爲1。每經歷過Minor GC,年齡就增長1歲,當到必定程度(默認15歲,能夠經過參數-XXMaxTenuringThreshold設置),就將會晉升年老代。

3.2.4 動態對象年齡斷定

爲了更好地適應不一樣程序內存情況,虛擬機並不硬性要求對象年齡達到MaxTenuringThreshold才能晉升老年代,若是在Survivor空間中相同年齡全部對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象就能夠直接進入年老代。

3.2.5 空間分配擔保

在發生Minor GC以前,虛擬機會先檢查年老代最大可用的連續空間是否大於新生代全部對象的總空間。

  • 若是條件成立,那麼Minor GC能夠確保是安全的。
  • 若是不成立,則虛擬機會查看HandlePromotionFailure設置值是否容許擔保失敗。
    • 若是容許,那麼會繼續檢查年老代最大可用連續空間是否大於歷次晉升到年老代對象的平均大小,若是大於,將嘗試進行一次Minor GC,儘管此次Minor GC是有風險的。
    • 若是小於,或者HandlePromotionFailure設置不容許冒險,那這時候改成進行一次Full GC。

下面解釋一下「冒險」是冒了什麼風險,新生代使用複製收集算法,但爲了內存利用率,只使用其中一個Survivor空間來做爲輪換備份,所以當出現大量對象在MinorGC後仍然存活的狀況(最極端的狀況就是內存回收後新生代中全部對象都存活),就須要老年代進行分配擔保,把Survivor沒法容納的對象直接進入老年代。

與生活中的貸款擔保相似,老年代要進行這樣的擔保,前提是老年代自己還有容納這些對象的剩餘空間,一共有多少對象會活下來在實際完成內存回收以前是沒法明確知道的,因此只好取以前每一次回收晉升到老年代對象容量的平均大小值做爲經驗值,與老年代的剩餘空間進行比較,決定是否進行Full GC來讓老年代騰出更多空間。

取平均值進行比較其實仍然是一種動態機率的手段,也就是說,若是某次Minor GC存活後的對象突增,遠遠高於平均值的話,依然會致使擔保失敗(Handle Promotion Failure)。

若是出現了HandlePromotionFailure失敗,那就只好在失敗後從新發起一次Full GC。 雖然擔保失敗時繞的圈子是最大的,但大部分狀況下都仍是會將HandlePromotionFailure開關打開,避免Full GC過於頻繁。

3.3 圖解JVM GC過程

這是我在學習過程當中,發現的一個簡單易懂的GC過程學習文章,經過圖的方式,清晰明瞭。圖解JVM GC過程

4. JVM中的垃圾回收器

4.1 七種垃圾回收器

在瞭解垃圾回收器以前先講兩個容易混淆的概念:

  • 並行:指多條垃圾收集線程並行工做,但此時用戶線程仍然處於等待狀態
  • 併發:指用戶線程與垃圾收集線程同時執行(不必定是並行的,可能會交替執行),用戶程序在繼續執行,而垃圾收集程序運行於另外一個CPU上

這幾篇是我在學習過程當中,以爲講得不錯的文章。

CMS和G1詳解
JVM之幾種垃圾收集器歸納介紹
JVM 垃圾回收算法及回收器詳解

我將學習資料中的內容簡單歸納爲下表:

名字 特色 線程 回收區域 回收算法
Serial收集器 最高的單線程收集效率 單線程 新生代 複製
ParNew收集器 能夠認爲是Serial收集器的多線程版本,在多核CPU環境下有着比Serial更好的表現。 多線程 新生代 複製
Parallel Scavenge收集器 關注系統吞吐量,目標是達到一個可控制的吞吐量,也常常稱爲「吞吐量優先」收集器 多線程 新生代 複製
Serial Old收集器 年老代收集器,能夠和全部的年輕代收集器組合使用(Serial收集器的年老代版本) 單線程 老年代 標記-整理
Parallel Old收集器 Parallel Scavenge收集器的老年代版本,關注吞吐量,這個收集器是在JDK 1.6中才開始提供的。 多線程 老年代 標記-整理
CMS收集器 一種以獲取最短回收停頓時間爲目標的收集器。目前很大一部分的Java應用集中在互聯網站或者B/S系統的服務端上,這類應用尤爲重視服務的響應速度,但願系統停頓時間最短,以給用戶帶來較好的體驗。CMS收集器存在3個缺點:1.對CPU資源敏感。通常併發執行的程序對CPU數量都是比較敏感的。2.沒法處理浮動垃圾。在併發清理階段用戶線程還在執行,這時產生的垃圾沒法清理。3.因爲標記-清除算法產生大量的空間碎片此外除了CMS的GC,其實其餘針對old gen的回收器都會在對old gen回收的同時回收young gen。 多線程併發收集 老年代 標記-清除
G1收集器 1.能夠像CMS收集器同樣,GC操做與應用的線程一塊兒併發執行。2.緊湊的空閒內存區間且沒有很長的GC停頓時間(標記整理算法,複製算法)。3.須要可預測的GC暫停耗時。4.不想犧牲太多吞吐量性能。5.啓動後不須要請求更大的Java堆。 多線程併發收集 整個Java堆 標記-整理

4.2 各垃圾收集參數設置

  • -Xmx: 設置堆內存的最大值。
  • -Xms: 設置堆內存的初始值。
  • -Xmn: 設置新生代的大小。
  • -Xss: 設置棧的大小。
  • -PretenureSizeThreshold: 直接晉升到老年代的對象大小,設置這個參數後,大於這個參數的對象將直接在老年代分配。
  • -MaxTenuringThrehold: 晉升到老年代的對象年齡。每一個對象在堅持過一次Minor GC以後,年齡就會加1,當超過這個參數值時就進入老年代。
  • -UseAdaptiveSizePolicy: 在這種模式下,新生代的大小、eden 和 survivor 的比例、晉升老年代的對象年齡等參數會被自動調整,以達到在堆大小、吞吐量和停頓時間之間的平衡點。在手工調優比較困難的場合,能夠直接使用這種自適應的方式,僅指定虛擬機的最大堆、目標的吞吐量 (GCTimeRatio) 和停頓時間 (MaxGCPauseMills),讓虛擬機本身完成調優工做。
  • -SurvivorRattio: 新生代Eden區域與Survivor區域的容量比值,默認爲8,表明Eden: Suvivor= 8: 1。
  • -XX:ParallelGCThreads:設置用於垃圾回收的線程數。一般狀況下能夠和 CPU 數量相等。但在 CPU 數量比較多的狀況下,設置相對較小的數值也是合理的。
  • -XX:MaxGCPauseMills:設置最大垃圾收集停頓時間。它的值是一個大於 0 的整數。收集器在工做時,會調整 Java 堆大小或者其餘一些參數,儘量地把停頓時間控制在 MaxGCPauseMills 之內。
  • -XX:GCTimeRatio:設置吞吐量大小,它的值是一個 0-100 之間的整數。假設 GCTimeRatio 的值爲 n,那麼系統將花費不超過 1/(1+n) 的時間用於垃圾收集。

相關文章
相關標籤/搜索