GC與內存分配策略

3、垃圾收集


垃圾收集主要是針對堆和方法區進行。程序計數器、虛擬機棧和本地方法棧這三個區域屬於線程私有的,只存在於線程的生命週期內,線程結束以後就會消失,所以不須要對這三個區域進行垃圾回收。java

判斷一個對象是否可被回收


一、引用計數算法 建立一個引用計數器,當對象增長一個引用計數器就加1,引用失效時計數器減1,。引用計數器爲0的對象可被回收。 在兩個對象出現循環引用的狀況下,此時引用計數器永遠不爲0,致使沒法對他們進行回收。正式由於循環引用的存在,java虛擬機不使用引用計數算法。 二、可達性分析算法 以GC Roots 爲起始點開始向下搜索,當一個對象到GC Roots沒有任何引用鏈相連時則證實此對象是不可用的,將會被斷定爲可回收的對象。算法

Java 虛擬機使用該算法來判斷對象是否可被回收,GC Roots 通常包含如下內容:數組

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

三、方法區的垃圾回收 由於方法區主要存放永久代對象,而永久代對象的回收率比新生代低不少,因此在方法區上進行回收性價比不高。 主要是對常量池的回收和對類的卸載。 爲了不內存溢出,在大量使用反射和動態代理的場景都須要虛擬機具有類卸載功能。 類的卸載條件不少,須要知足如下三個條件,而且知足了條件也不必定會被卸載:安全

  • 該類全部的實例都已經被回收,此時堆中不存在該類的任何實例。
  • 加載該類的 ClassLoader 已經被回收。
  • 該類對應的 Class 對象沒有在任何地方被引用,也就沒法在任何地方經過反射訪問該類方法。

四、finalize 即便是可達分型分析算法中不可達的對象也不是非死不可的,要宣告一個對象的死亡至少要通過兩次標記的過程。bash

  1. GC進行第一次標記並進行一次篩選(篩選那些覆蓋了finalize方法而且finalize方法是第一次調用的對象);
  2. 另外一個低優先級的Finalizer線程去調用那些被篩選出來的對象的finalize方法;
  3. GC進行第二次標記,若是在前一步中那些篩選出來的對象沒有在finalize拯救本身,此時,那些未被篩選到的和這些這些篩選到的可是沒有拯救本身的對象都將會回收。

引用類型


1. 強引用 被強引用關聯的對象不會被回收。使用 new 一個新對象的方式來建立強引用。數據結構

Object obj = new Object();
複製代碼

2. 軟引用 被軟引用關聯的對象只有在內存不夠的狀況下才會被回收,一般用在對內存敏感的程序中。使用 SoftReference 類來建立軟引用。多線程

Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null;  // 使對象只被軟引用關聯
複製代碼

3. 弱引用 被弱引用關聯的對象必定會被回收,也就是說它只能存活到下一次垃圾回收發生以前。使用 WeakReference 類來建立弱引用。併發

Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;
複製代碼

4. 虛引用 又稱爲幽靈引用或者幻影引用,一個對象是否有虛引用的存在,不會對其生存時間形成影響,也沒法經過虛引用獲得一個對象。爲一個對象設置虛引用的惟一目的是能在這個對象被回收時收到一個系統通知。使用 PhantomReference 來建立虛引用。性能

Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj, null);
obj = null;
複製代碼

垃圾收集算法


一、標記-清除算法spa

  • 最基礎的收集算法,分爲標記和清除兩個階段。首先標記出全部須要回收的對象,在標記完成後統一回收全部被標記的對象。
  • 缺點:
    1. 效率問題:標記和清除兩個過程的效率都不高。 2.空間問題:標記清除以後會產生大量不連續的內存碎片,殘留空間碎片太多,可能繼續致使GC。

二、複製算法

  • 1:1複製:把內存分爲對等的兩塊,每次只是用其中的一塊,當GC以後,將還存活着的對象複製到另外一塊上面,而後再把已使用過的內存空間一次清理掉。

    • 缺點:要消耗一半內存形成浪費,對象多時,容易觸發GC。
  • 8:1複製:如今虛擬機的內存分配,通常採用一塊較大的 Eden 空間和兩塊較小的 Survivor 空間,每次使用 Eden 和其中一塊 Survivor。在回收時,將 Eden 和 Survivor 中還存活着的對象所有複製到另外一塊 Survivor 上,最後清理 Eden 和使用過的那一塊 Survivor。HotSpot 虛擬機的 Eden 和 Survivor 大小比例默認爲 8:1。

    • 缺點:若是另外一塊Survivor空間沒有足夠的空間存放上一次新生代收集下來的存活對象時,這些對象將直接經過分配擔保機制進入老年代。

三、標記-整理算法

  • 標記過程與標記-清除算法相同,整理是讓全部存活着的的對象都向一端移動,而後直接清理掉端邊界之外的內存。
  • 缺點:須要移動大量對象,處理效率比較低。
  • 優勢:不會產生內存碎片

四、分代收集

  • 通常將堆分爲新生代和老年代
    • 新生代:複製算法
    • 老年代:標記-整理算法或者標記-清除算法

HotSpot的算法實現


一、準確式GC

  • 當執行系統停頓下來以後,並不須要一個不漏的檢查全部的執行上下文和引用位置,在虛擬機中,使用一組被稱爲 OopMap 的數據結構來達到目的。在類加載完的時候,HotSpot (虛擬機的一種)就已經把對象內的什麼偏移量上是什麼類型的數據計算出來了,即位置也會被 OopMap 記錄下來。

二、安全點

  • 在OopMap 的幫忙下,能夠快速且準確的找到 GC Roots 枚舉,但可能致使引用關係的變化,或者說 OopMap 內容變化的指令很是多,若是爲每一條指令都生成對應的 OopMap ,那將會須要大量的額外空間,這樣GC成本也會變高。
  • 實際上系統並無爲每條指令都生成OopMap,而是在「特定的位置」記錄了這些信息,好比方法的調用、循環跳轉、異常跳轉等,這些位置被稱爲安全點,只有在達到安全點纔會去停頓。
    • 跑到安全點的防範有兩種:
      1. 搶斷式中斷:在GC發生時,首先把全部的線程所有中斷,若是發現有線程中斷的地方不在安全點上,則恢復線程,讓它「跑」在安全點上。不過如今沒有虛擬機採用這種方式。
      2. 主動式中斷:線程給本身設置一個標誌位,當輪詢到這個標誌位,就主動掛起,標誌位和安全點在同一個位置。

三、安全區域

  • 當若是線程處於 sleep 或者 Blocked 狀態時,這時線程沒法響應JVM的中斷請求,就須要安全區域來解決了。
  • 安全區域是指在一段代碼中,引用關係不會發現變化。在這個區域 GC 都是安全的,當在這個區域發現GC,線程能夠無論本身的安全點的狀態,當要離開這段區域時,須要檢查是否完成根節點枚舉,沒有則等待,只到收到能夠離開安全點的信號爲止。

垃圾收集器


若是收集算法是內存回收的方法論,那麼垃圾收集器就是內存回收的具體實現。下圖中兩個虛擬機之間存在連線說明能夠搭配使用。

  • 串行:垃圾收集器與用戶程序交替執行,這意味着在執行垃圾收集的時候須要停頓用戶程序。
  • 並行:指多條垃圾收集線程並行工做,但此時用戶線程仍然處於等待狀態
  • 併發:用戶線程與垃圾收集線程同時執行(但不必定是並行的,可能交替執行),用戶程序繼續運行,而垃圾收集程序運行於另外一個CPU上。
  • 吞吐量:CPU用於運行代碼所須要的時間與CPU總消耗時間的比值,即吞吐量=運行代碼時間/(運行用戶代碼時間+垃圾收集時間) **注:**除了CMS和G1以外,其餘垃圾收集器都是以串行方式執行。

一、Serial收集器(新生代)

  • 最基本、歷史最悠久的一個單線程垃圾收集器,它在垃圾收集時,必須暫停其餘全部的工做線程,直到它收集結束。
  • 它依然是虛擬機運行在Client模式下的默認新生代收集器。
  • 它簡單高效,對於限定單個CPU的環境來講,Serial收集器因爲沒有線程交互的開銷,專心作垃圾收集天然能夠得到更高的效率。

二、ParNew收集器(新生代)

  • 就是Serial收集器的多線程版本。
  • 是許多運行在Server模式下的虛擬機中首選的新生代收集器,與性能無關但很重要的一個緣由是,除了Serial收集器以外,目前只有它能與CMS收集器配合工做。

三、Parallel Scavenge收集器(新生代)

  • 使用複製算法,並行的多線程收集器。也被稱爲「吞吐量優先」收集器。其關注點是達到一個可控制的吞吐量,其餘收集器關注點是儘量的縮短垃圾收集時用戶線程的停頓時間。
  • 停頓時間越短越適合須要用戶交互的程序,良好的響應速度能提高用戶體驗吞吐量小則能夠高效率的利用CPU時間,儘快完成程序的運算任務,主要適合後臺運算並且不須要太多交互的任務。

四、Serial Old(老年代)

  • 是 Serial 收集器的老年代版本,也是給 Client 場景下的虛擬機使用。
  • 若是用在 Server 場景下,它有兩大用途:
    1. 在 JDK 1.5 以及以前版本(Parallel Old 誕生之前)中與 Parallel Scavenge 收集器搭配使用。
    2. 做爲 CMS 收集器的後備預案,在併發收集發生 Concurrent Mode Failure 時使用。

五、Parallel Old收集器(老年代)

  • 是 Parallel Scavenge 收集器的老年代版本。在注重吞吐量以及 CPU 資源敏感的場合,均可以優先考慮 Parallel Scavenge 加 Parallel Old 收集器。

六、CMS收集器(老年代)

  • 是一種以獲取最短回收停頓時間爲目標的收集器。基於標記-清除算法實現的,它的運做包含4個步驟:

    1. 初始標記:僅僅只是標記一下 GC Roots 能直接關聯到的對象,速度很快,須要停頓。
    2. 併發標記:進行 GC Roots Tracing 的過程,它在整個回收過程當中耗時最長,不須要停頓。
    3. 從新標記:爲了修正併發標記期間因用戶程序繼續運做而致使標記產生變更的那一部分對象的標記記錄,須要停頓。
    4. 併發清除:不須要停頓。
  • 在整個過程當中耗時最長的併發標記和併發清除過程當中,收集器線程均可以與用戶線程一塊兒工做,不須要進行停頓。

  • 缺點:

    1. 對CPU資源很是敏感。在併發階段,它雖然不會致使用戶線程停頓,可是會由於佔用了一部分線程而致使應用程序變慢,總吞吐量會下降。
    2. 沒法處理浮動垃圾,可能出現 「Concurrent Mode Failure」失敗而致使另有一次Full GC的產生。浮動垃圾是指併發清除階段因爲用戶線程繼續運行而產生的垃圾,這部分垃圾只能到下一次 GC 時才能進行回收。因爲浮動垃圾的存在,所以須要預留出一部份內存,意味着 CMS 收集不能像其它收集器那樣等待老年代快滿的時候再回收。若是預留的內存不夠存放浮動垃圾,就會出現 Concurrent Mode Failure,這時虛擬機將臨時啓用 Serial Old 來替代 CMS。
    3. 標記 - 清除算法致使的空間碎片,每每出現老年代空間剩餘,但沒法找到足夠大連續空間來分配當前對象,不得不提早觸發一次 Full GC。

七、G1收集器(整個堆)

  • G1是一款面向服務端的垃圾收集器。
  • 它將整個java堆劃分紅爲多個大小相等的獨立區域(Region),雖然還保留有新生代和老年代的概念,但再也不是物理隔離的的了。他們都是一部分Region(不須要連續的)的集合。
  • 每一個 Region 都有一個 Remembered Set,用來記錄該 Region 之間的對象引用以及其餘收集器新生代與老年代之間的對象引用。經過使用 Remembered Set,在作可達性分析的時候就能夠避免全堆掃描。
  • 若是不計算維護 Remembered Set 的操做,G1 收集器的運做大體可劃分爲如下幾個步驟:
    1. 初始標記:初始標記僅僅只是標記一下GC Roots能直接關聯到的對象。須要停頓線程,速度很快。
    2. 併發標記:從GC Roots開始對堆中的對象進行可達性分析,找出存活的對象。耗時較長,可與用戶程序併發執行。
    3. 最終標記:爲了修正在併發標記期間因用戶程序繼續運做而致使標記產生變更的那一部分標記記錄,虛擬機將這段時間對象變化記錄在線程的 Remembered Set Logs 裏面,最終標記階段須要把Remembered Set Logs 的數據合併到 Remembered Set 中。這階段須要停頓線程,可是可並行執行。
    4. 篩選回收:首先對各個 Region 中的回收價值和成本進行排序,根據用戶所指望的 GC 停頓時間來制定回收計劃。此階段其實也能夠作到與用戶程序一塊兒併發執行,可是由於只回收一部分 Region,時間是用戶可控制的,並且停頓用戶線程將大幅度提升收集效率。
  • 特色:
    1. 並行與併發:使用多個CPU來縮短Stop-The-World停頓的時間,部分其餘收集器本來須要停頓Java線程執行的GC動做,G1收集器仍然能夠經過併發的方式讓Java程序繼續執行。
    2. 分代收集:新生代和老年代再也不是物理隔離的的了。他們都是一部分Region(不須要連續的)的集合。
    3. 空間整合:總體來看是基於「標記 - 整理」算法實現的收集器,從局部(兩個 Region之間)上來看是基於「複製」算法實現的,這意味着運行期間不會產生內存空間碎片。
    4. 可預測的停頓:能讓使用者明確指定在一個長度爲 M 毫秒的時間片斷內,消耗在 GC 上的時間不得超過 N 毫秒。

4、內存分配與回收策略


Minor GC與Full GC

  • Minor GC:回收新生代,指發生在新生代的垃圾收集動做,由於Java對象大多都具有朝生夕滅的特徵,因此Minor GC很是頻繁,通常回收速度也比較快。
  • Full GC:回收老年代和新生代,通常老年代對象存活時間比較長,所以Full GC的速度通常會比Minor GC慢10倍以上。

對象優先在Eden分配

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

大對象直接進入老年代

  • 大對象是指,須要大量連續內存空間的Java對象,最典型的大對象就是那種很長的字符串以及數組。
  • 常常出現大對象容易致使內存還有很多內存空間時就提早觸發垃圾收集以獲取足夠的連續空間來安置它們。
  • 提供了一個-XX:PretenureSizeThreshoid參數,令大於這個設置值的對象直接在老年代分配。這樣作的目的是避免在Eden區以及兩個Survivor區之間發生大量的內存複製。

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

  • 定義一個年齡計數器。若是對象在Eden出生並通過第一次Minor GC後仍然存活,而且能被Survivor容納的話。將被移動到Survivor空間中,而且對象年齡設爲1。對象在Survivor區中每熬過一次Minor GC,年齡就增長1歲,年齡增長到必定程度就會被晉升到老年代。
  • 對象年齡的閾值能夠經過參數,-XX:MaxTenuringThreshold進行設置,默認年齡爲15。

對象年齡斷定

  • 若是Survivor空間中相同年齡全部對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象就能夠直接進入老年代,無須等到MaxTenuringThreshold中要求的年齡。

空間擔保

  • 在發生 Minor GC 以前,虛擬機先檢查老年代最大可用的連續空間是否大於新生代全部對象總空間,若是條件成立的話,那麼 Minor GC 能夠確認是安全的。若是不成立的話虛擬機會查看 HandlePromotionFailure 的值是否容許擔保失敗,若是容許那麼就會繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代對象的平均大小,若是大於,將嘗試着進行一次 Minor GC;若是小於,或者 HandlePromotionFailure 的值不容許冒險,那麼就要進行一次 Full GC。
相關文章
相關標籤/搜索