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

3.2 對象已死嗎

3.2.1 引用計數算法

  • 給對象中添加一個引用計數器,
    • 每當有一個地方引用它時,計數器值就加1;
    • 當引用失效時,計數器值就減1;
    • 任什麼時候刻計數器都爲0的對象就是不可能再被使用的
  • 缺點:很難解決對象之間相互循壞的問題

3.2.2 可達性分析

  • 根搜索算法:經過一系列名爲「GC Roots」的對象做爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連時,則證實此對象是不可用的。
  • 在Java語言裏,可做爲GC Roots的對象包括下面幾種:
    • 虛擬機棧(棧幀中的本地變量表)中的引用的對象。
    • 方法區中的類靜態屬性引用的對象。
    • 方法區中的常量引用對象。
    • 本地方法棧中的JNI的引用的對象。

3.2.3 再談引用(四種引用)

  • 強引用
    • 相似於Obejct obj = new Obejct()這類的引用
    • 只要強引用存在,垃圾收集器永遠不會回收掉被引用的對象。
  • 軟引用
    • 用來描述一些還有用,但並不是必要的對象。
    • 對於軟引用關聯着的對象,在系統將要發生內存溢出異常以前,將會把這些對象列進回收範圍並進行二次回收。
    • 可使用SoftReference類來實現軟引用。
  • 弱引用
    • 比軟引用更弱一些,被弱引用關聯的對象只能生產到下一次垃圾收集發生以前。
    • 當垃圾收集器工做室,不管內存是否足夠,都會回收弱引用關聯的對象
    • 可使用WeakReference類來實現弱引用
  • 虛引用
    • 也稱爲幽靈引用或者幻影引用,它是最弱的一種引用關係。
    • 一個對象是否有虛引用的存在,徹底不會對其生存時間構成影響,也沒法經過虛引用來取得一個對象實例。
    • 爲一個對象設置虛引用關聯的惟一目的就是但願能在這個對象被收集器回收時收到一個系統通知。
    • 可使用PhantomReference類來實現虛引用。

3.2.4 生成仍是死亡

  • 要真正宣告一個對象死亡,須要兩個標記過程:
    • 若對象在進行可達性分析後發現沒有與GC Roots相鏈接的引用鏈,會被第一次標記並篩選。篩選的條件是此對象是否有必要執行finalize()方法
      • 當對象沒有覆蓋finalize()方法或者finalize()方法已經由虛擬機調用過,都視爲finalize()沒有必要執行
    • finalize()方法是對象逃脫死亡的最後一次機會,稍後GC會對對象進行第二次標記
      • 這種自救的機會只有一次,由於一個對象的finalize()方法最多隻會被系統自動調用一次
  • 原書做者不建議你們這樣使用

3.2.5 回收方法區

  • 不僅是Java堆,方法區也有垃圾收集機制
  • 方法區(永久代)的垃圾收集主要回收兩種內容:廢棄常量和無用的類
  • 回收廢棄常量與Java堆的對象相似,即該常量沒有在任何地方被引用
  • 類須要同時知足一下三個條件才能算是「無用的類」:
    • 該類全部的實例都已經被回收,即Java堆中再也不有該類的實例
    • 加載該類的ClassLoader已經被回收
    • 該類對應的java.lang.Class對象沒有在任何地方被調用,沒法在任何地方經過反射訪問該類的方法
  • 在大量使用反射、動態代理、CGLib等bytecode框架的場景,以及動態生成JSP和OSGI這類頻繁自定義ClassLoader的場景都須要虛擬機具有類卸載的功能,以保證永久代不會溢出。

3.3 垃圾收集算法

3.3.1 標記-清除算法

  • 算法分爲標記和清除兩個階段,首先標記出全部須要回收的對象,而後回收全部已標記的對象。java

  • 這個算法主要有兩個缺點:算法

    • 一個是效率不高,標記和清除的效率都不高;
    • 二是標記清除以後會產生大量不連續的碎片。分配較大對象時,沒法找到足夠的連續內存空間,會提早觸發GC

    img

3.3.2 複製算法

  • 把可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活的對象複製到另一塊上面,而後再把已使用過的內存空間一次清理掉。
  • 優勢:內存分配時不用考慮內存碎片等複雜狀況,實現簡單,運行高效。
  • 缺點:這種算法的代價是將內存縮小爲原來的一半,利用率低。
  • 新生代的GC使用複製算法
  • 由於新生代98%的對象都是「朝生夕死」,因此不須要1:1來劃份內存。而是劃分一塊較大的Eden空間和兩塊較小的Survivor空間
  • HotSpot虛擬機默認的Eden和Survivor比例是8:1,只有10%的內存被「浪費」
  • 若是另外一塊Survivor空間沒有足夠空間存放上一次新生代手機下來的對象,對象將經過分配擔保知己進入老年代

img

3.3.3 標記-整理算法

  • 複製算法在對象存活率高的狀況(好比老年代)要進行較多的複製操做,效率低。
  • 標記過程仍然與「標記——清除」算法同樣,後續步驟是將全部存活的對象都向一端移動,而後直接清理掉端邊界意外的內存

img

3.3.4 分代收集算法

  • 根據對象的存活週期的不一樣將內存劃分爲幾塊。通常是把Java堆分爲新生代和老年代
  • 新生代:每次垃圾收集時都發現有大批對象死去,只有少許存活,那就選用複製算法,只須要付出少許存活對象的複製成本就能夠完成收集。
  • 老年代:由於對象存活率高、沒有額外空間對它進行分配擔保,就必須使用**「標記——清除」「標記——整理」**算法來進行回收。

3.4 HotSpot的算法實現

3.4.1 枚舉根節點

  • GC鏈逐個檢查引用,會消耗比較多時間數組

  • GC停頓,爲了保持「一致性」,須要「Stop the world」安全

  • 目前主流的Java虛擬機使用的都是準確式GC,當執行系統停頓下來後並不須要一個不漏的檢查完全部執行上下文和全局的引用變量,虛擬機應當有辦法直接得知哪些地方存着對象的引用數據結構

  • HotSpot使用一組稱爲OopMap的數據結構來記錄哪些地方存着對象的引用多線程

  • 在類加載過程當中,HotSpot就把對象內什麼偏移量上是什麼類型的數據計算出來,在JIT編譯過程當中會在特定的位置記錄下棧和寄存器中哪些位置是引用併發

3.4.2 安全點

  • HotSpot沒有爲每條指令都生成OopMap,只是在特定位置記錄了這些信息,這些位置稱爲安全點
  • 程序執行時並不是在全部地方都能停頓下來開始GC,只有在到達安全點時才能暫停
  • 給多線程記錄安全點時有兩種方案:搶先式中斷 (幾乎再也不使用)和 主動式中斷
  • 主動式中斷的思想是當GC須要中斷線程時不直接對線程進行操做,僅僅簡單的設置一個標誌,各個線程執行時主動去輪詢這個標誌,發現中斷標誌爲真時就本身中斷掛起

3.4.3 安全區域

  • 爲安全區域是指在一段代碼片斷之中,引用關係不會發生變化,在這個區域內的任何地方進行GC都是安全的。能夠當作是擴展的安全點

3.5 垃圾收集器

3.5.1 Serial收集器

  • Serail收集器是「單線程」的,他在進行垃圾收集時必須暫停其餘的全部線程。「Stop the world 」
  • 對於單個CPU壞境來講,Serial收集器因爲沒有線程交互的開銷,專心作垃圾收集,能夠得到很高的單線程收集效率

3.5.2 ParNew收集器

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

3.5.3 Parallel Scavenge收集器

  • Parallel Scavenge收集器是一個新生代收集器,採用複製算法
  • Parallel Scavenge收集器的特色是他的關注點與其餘收集器不一樣。其餘收集器的目標是儘量的縮短用戶線程的停頓時間,而Parallel Scavenge收集器的目標是達到一個可控的吞吐量
  • 高吞吐量能夠高效的利用CPU時間,儘快得完成程序的運算任務,主要適合在後臺運算而不須要太多交互的任務
  • GC停頓時間的縮短是以犧牲吞吐量和新生代空間來換取的
  • Parallel Scavenge收集器擁有自適應調節策略,在對收集器不太瞭解,手工優化困難的時候可使用

3.5.4 Serail Old收集器

  • Serial Old收集器是Serail收集器的老年代版本
  • Serail Old收集器主要用於Clinet模式下
  • Serail Old收集器另外一種用途是做爲CMS收集器的後備預案

3.5.5 Parallel Old收集器

  • Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多線程和標記-整理算法

3.5.6 CMS收集器

  • CMS收集器是一種以獲取最短的回收停頓時間爲目標的收集器
  • CMS收集器基於標記-清除算法實現,分爲四個步驟:初始標記、併發標記、從新標記、併發清除
  • 步驟詳解:
    • 初始標記:標記一下GC Roots能直接關聯到的對象,速度很快
    • 併發標記:進行GC Roots Tracing
    • 從新標記:是爲了修正那些在併發標記期間因用戶程序繼續運做而致使標記產生變更的那一部分對象的標記記錄,在這一階段的停頓時間會比初始標記階段稍長一點
  • CMS收集器的缺點:
    • CMS收集器對CPU資源很是敏感,面向併發設計的程序都對CPU資源敏感
    • CMS收集器沒法處理浮動垃圾,可能出現「Concurrent Mode Failure」失敗而致使另外一次Full FC的產生
    • 因爲CMS收集器採用了標記-清除算法,因此在回收結束時會有大量空間碎片產生,碎片過多時,在給大對象分配內存時會有很大麻煩

3.5.7 G1收集器

  • G1收集器是一款面向服務端應用的垃圾收集器
  • G1收集器具有如下特色:
    • 並行與併發,縮短停頓時間
    • 分代收集
    • 空間整合:從總體上來看是基於「標記-整理」算法實現的,在局部上是基於複製算法實現的
    • 可預測的停頓
  • G1收集器將整個Java堆劃分爲多個大小相等的獨立區域,雖然還保留有新生代和老生代的概念,但新生代和老生代再也不是物理隔的了,他們是一部分Region的集合
  • G1收集器能夠有計劃地避免在整個Java堆中進行全區域的垃圾收集:跟蹤各個Region裏面的垃圾堆積的價值大小,在後臺維護一個優先列表,每次根據容許的收集時間,優先回收價值最大的Region
  • 在G1收集器中,使用Remembered Set來避免全堆掃描,提升效率

3.6 內存分配與回收策略

3.6.1 對象優先分配在Eden分配

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

3.6.2 大對象直接進入老年代

  • 所謂大對象就是指須要大量連續內存空間的Java對象,最典型的大對象就是那種很長的字符串及數組。

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

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

3.6.4 動態對象年齡斷定

  • 虛擬機並不老是要求對象的年齡必須達到MaxTenuringThreshold才能晉升老年代,若是在Survivor空間中相同年齡全部對象大小的總和大於Survivor空間的一半,年齡大於或等於改年齡的對象就能夠直接進入老年代,無需等到MaxTenuringThreshold要求的年齡。

3.6.5空間分配擔保

  • 在發送Minor GC時,虛擬機會檢測以前每次晉升到老年代的平均大小是否小於老年代的剩餘空間大小,若是大於,則改成直接進行一次Full GC。若是小於,則查看HandlePromotionFailure設置是否容許擔保失敗;若是容許,那隻會進行Minor GC;若是不容許,則也要改成進行一次Full GC
相關文章
相關標籤/搜索