《深刻理解Java虛擬機》讀書筆記(第三章)

 

垃圾收集器與內存分配策略(第三章)

前言,衆所周知,Java是由c++進化而來,c++在內存需本身申請,本身釋放,因而就有了Java的動態內存分配。書的第三章開篇,有這樣一句話描述的很妙——Java與C++之間有一堵由內存動態分配和垃圾收集技術所圍成的「高牆」,牆外的人想進去,牆內的人卻想出來。html

如何判斷對象已經死去

引用計數器法

概述:給對象添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;引用失效,計數器就減1;當一個對象的引用計數器爲0時,表示對象已死,可回收java

優勢:實現簡單,而且高效c++

缺點:沒法解決對象之間的循環引用問題算法

可達性分析算法

概述:解決引用計算器法的沒法判斷循環引用的問題,基本思路是,以一系列稱爲「GC Roots」的對象做爲起點,從這些節點開始向周圍搜索,經過引用鏈(Reference Chain)的方式。當對象A引用了對象B,那麼對象A和對象B之間便有了引用鏈,若是對象A是「GC Roots」,那麼對象B就被稱爲可達。當對象B引用了對象C,由於B是可達的,因此A經過B也能到達C,對象C也被稱做可達。引用鏈:A -> B -> C安全

哪些對象可做爲GC Roots的對象?數據結構

  • 虛擬機棧(棧幀中的本地變量)中引用的對象
  • 方法區(注意:方法區這個概念,在JDK1.8便移除了,改成了元空間)中類靜態屬性引用的對象
  • 方法區中常量引用的對象
  • 本地方法棧中引用的對象

優勢:全面的分析對象是否存活多線程

缺點:實現複雜,效率低下併發

引用

判斷對象是否存活,離不開引用。JDK1.2之前,定義引用的方法很純粹:若是reference類型的數據中存儲的數值表明的是另外一塊內存的起始地址,就稱這塊內存表明着一個引用。這就致使了引用只有兩種狀態,有或者無。兩個對象之間,存在着引用就是有,不存在引用就是沒有,沒有就被垃圾收集器給回收。可是咱們對於一些「食之無味,棄之惋惜」的對象就顯得無能爲力了。當咱們的內存足夠時,咱們並不想毀滅它,以此來避免後面的重複建立,浪費時間。因此有了如下四種定義高併發

  • 強引用:程序代碼中廣泛存在,相似「Objec obj = new Object()」,我的理解爲,就是GC Roots能可達的對象,這些對象之間的引用鏈就是強引用,只要強引用在,就永遠不會被回收掉。
  • 軟引用:SoftReference,描述一些還可能有用,可是非必需的對象。在系統將要發生內存溢出異常以前,將會把這些對象列進回收範圍之中進行第二次回收。固然,若是此次回收尚未收集足夠的空間供新對象使用,纔會拋出內存溢出的異常,致使OOM。
  • 弱引用:WeakReference,也是用來描述一些非必需的對象,可是它比軟引用更加的弱一些。每一次垃圾回收,無論內存夠不夠都要回收它們。只能生存在下一次垃圾回收以前。
  • 虛引用:PhantomReference,最弱的引用,一個對象是否有虛引用,徹底不會對其生存時間構成影響,惟一的目的是:能在這個對象被收集器回收時收到一個系統通知

回收方法區

即JDK1.8後的元空間,回收對象主要有兩部份內容:廢棄的常量和無用的類。性能

知足三個條件纔算是無用的類:

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

垃圾收集算法

標記-清除算法

  • 概述:算法分爲兩個階段,首先標記全部須要回收的對象,在標記完成後統一回收全部標記的對象。最基礎的收集算法。

  • 缺點:效率不高,空間上會產生大量的空間碎片。若是程序遇到大對象須要分配時,沒法找到足夠的連續內存而不得不提早觸發另外一次的垃圾收集動做。

複製算法

  • 概述:把內存分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存快用完了,就將還存活着的對象複製到另外一塊上面,而後再把已使用過的內存空間一次性的清理掉。現代的商業機都是採用的這種收集算法,可是IBM研究代表,新生代中的對象98%都是「朝生夕死」的,並不須要按1:1來分配內存空間,而是將內存分爲一塊較大的Eden和兩塊較小的Survivor1空間,每次使用Eden和其中的一塊Survivor空間。當回收時,將Eden和Survivor中還存活的對象一次性的複製到另一塊Survivor上去,最後在清理掉Eden和剛纔使用過的Survivor空間。HotSpot虛擬機默認Eden和Survivor的大小比例是8:1。可是可能會出現複製到Survivor過程當中,存活的對象太多了,裝不下,Survivor空間不夠用。因此還須要進行分配擔保(Handle Promotion),依賴其餘內存來複制(這裏指老年代)。

  • 優勢:簡單高效,不用考慮內存碎片等複雜狀況。

  • 缺點:內存利用會減小一半

標記-整理算法

適用於老年代,存活率高的對象。讓全部存活的對象向一端移動,而後清理掉端邊界之外的內存。

分代收集算法

當前商業虛擬機都是採用的「分代收集」算法,把java堆分爲新生代和老年代,這樣就能夠根據各個年代的特色採用最適當的收集算法。新生代用複製算法,由於有大量對象死去,老年代使用「標記-清理」或者「標記整理」,由於老年代存活率高。

HotSpot如何實現垃圾回收

枚舉根節點

從GC Roots出發,也就是根節點,進行可達性分析,這個過程很是消耗時間。並且可達性分析的話,須要保持「一致性」,一致性是指整個可達性分析期間的執行過程應該看起來是要停留在某個時間點上。不能夠出現分析過程當中,對象之間的引用還在發生變化。要保證可達性分析結果的正確性,準確性。因此GC會致使系統的「Stop The World」

固然因爲枚舉根節點消耗大量的時間,因此引入了OopMap這種數據結構。GC時,當執行系統停頓下來時,並不須要一個不漏的把全部執行上下文和全局的引用位置,虛擬機而是經過OopMap直接得知哪些地方存放着對象的引用

安全點

爲了配合OopMap,引入了安全點。HotSpot經過OopMap確實能夠快速且準確地完成GC Roots的枚舉,可是OopMap內容的變化的指令很是的多,若是位每一條指令都生成對應的OopMap,那將會須要大量的額外空間。因此虛擬機只在特定的時間位置,記錄了這些引用信息,並無爲每條指令都生成OopMap,這些位置便稱做安全(SafePoint),即程序執行時並不是能在全部的地方停下來,而是隻能在達到安全點時才能暫停。

如何在GC發生時,讓全部的線程都「跑」到最近的安全點去?

  • 搶先式中斷:強制把全部的線程中斷,若是有線程不在安全點上,就恢復線程,讓它跑到安全點上,幾乎沒有虛擬機使用這種方法,
  • 主動式中斷:當線程須要中斷時,不直接對線程操做,而是經過一個標誌,各個線程執行時,主動的去輪詢這個標誌,須要中斷時,改變這個標誌。線程在輪詢的時候發現中斷標誌爲真,就本身中斷掛起。輪詢標誌的地方和安全點是重合的。

SafePoint很完美嗎?不見得,safePoint關注的點是正在運行的線程,那麼有些線程處於Sleep狀態或者Blocked狀態,這時候線程沒法響應中斷請求。因此引入了安全區域(Safe Region)

安全區域是指:在一段代碼中,引用關係不會發生變化。在這個區域中的任意地方開始GC都是安全的。具體描述:在線程執行到safe region中的代碼時,首先標識本身已經進入了safe region,那樣,當在這段時間裏JVM要發起GC時,就不用管標識本身爲safe region狀態的線程了。在線程要離開safe region時,它要檢查系統是否已經完成了根節點枚舉(即整個GC過程),若是完成了,那線程就繼續執行,不然他就必須等待直到收到能夠安全離開safe region的信號爲止

垃圾收集器

若是說收集算法是內存回收的方法論,那麼垃圾收集器就是內存回收的具體實現。

並行(Parallel):指多條垃圾收集線程並行工做,但此時用戶線程仍然處於等待階段。

併發(Concurrent):指用戶線程與垃圾收集線程同時執行(但不必定是並行的,可能會交替執行),用戶程序在繼續運行,而垃圾收集程序運行在另外一個CPU上。

Serial收集器

最基本,發展歷史最悠久的收集器。特色:在他進行垃圾收集時,必須暫停其餘全部工做線程,直到它收集結束,Stop The World。優勢:簡潔又高效。在運行在client模式下的虛擬機來講是一個很好的選擇。

ParNew收集器

是serial收集器的多線程版本。它是許多運行在Server模式下的虛擬機中首選的新生代收集器。緣由很簡單,目前只有它與serial收集器能與CMS收集器配合工做。

Parallel Scavenge收集器

目標是達到一個可控的吞吐量。所謂吞吐量用於運行用戶代碼的時間與CPU總消耗時間的比值。停頓時間越短就越適合須要與用戶交互的程序,良好的響應速度能提高用戶體驗,而高吞吐量則能夠高效率地利用CPU時間,儘快完成程序的運算任務,主要適合在後臺運算而不須要太多交互的任務。它也被稱爲吞吐量優先的收集器。能夠設置一個參數,讓虛擬機GC自適應的調節。即虛擬機會根據當前系統的運行狀況收集性能監控信息,動態調整這些參數以提供最適合的停頓時間或最大吞吐量。這是它與parnew收集器的一個重要區別。

Serial Old收集器

是serial收集器的老年代版本,一樣是一個單線程收集器,使用標記_整理算法。

Parallel Old收集器

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

CMS(Concurrent Mark Sweep)

是一種以獲取最短回收停頓時間爲目標的收集器。重視服務的響應速度,但願系統停頓時間最短,以給用戶帶來較好的體驗。是基於標記-清除算法實現的,它的運做過程:初始標記,併發標記,從新標記,併發清除。第一步和第三步仍然須要Stop The World。

  • 初始標記:僅僅標記一下GC Roots能直接關聯到的對象,速度很快。須要STW
  • 併發標記:進行GC Roots Tracing的過程,時間較長
  • 從新標記:則是爲了修正併發標記期間因用戶程序繼續運做而致使標記產生變更的那些對象的標記記錄,比初始標記稍微長一些,但遠比並發標記的時間短。需STW
  • 併發清除:執行sweep,時間較長

優勢:併發收集,低停頓,又稱之爲併發低停頓收集器。

缺點

  1. CMS對CPU資源很是的敏感,在併發階段雖然不會致使用戶線程停頓,可是會由於佔用CPU資源而致使應用程序變慢,總吞吐量會下降。多CPU的時候,能夠用回收默認線程數(CPU數量+3)/4。使併發回收垃圾線程所佔的CPU資源使用百分比下降。CPU越多,降的多。可是CPU不多的狀況下,問題就很大,由此虛擬機提供了i-CMS增量式併發收集器,就是讓GC線程,用戶線程交替運行,儘可能減小GC線程獨佔CPU的時間,這樣會是整個垃圾回收時間增加,但對用戶程序的影響就少些。可是效果通常,不提倡使用。

  2. CMS沒法處理浮動垃圾,因爲在併發清理階段用戶線程還在運行,可能產生新的垃圾,這一部分垃圾出如今從新標記階段以後。所以須要預留提供一部分空間給併發時的用戶線程使用,以供用戶程序正常運行,而不能像其餘收集器同樣,等到老年代幾乎被填滿了再進行收集。

  3. 是標記-清除算法的缺點,會有大量的空間碎片產生,碎片過多時,將會給大對象帶來很大的麻煩,若是沒有空間給大對象,就不得不提早觸發一次fullGC。爲了解決這個問題,能夠設置一些參數來優化,如當FullGC執行了多少次的時候,來一次空間碎片壓縮,開啓內存碎片的整理過程。可是這樣使得停頓時間變長了。

G1(Garbage-First)收集器

G1是一款面向服務端應用的垃圾收集器,它的使命是替換掉CMS,彌補CMS空間碎片等缺點。不一樣於其餘的分代回收算法、G1將堆空間劃分紅了互相獨立的區塊。每塊區域既有可能屬於O區、也有多是Y區,且每類區域空間能夠是不連續的(對比CMS的O區和Y區都必須是連續的)。這種將O區劃分紅多塊的理念源於:當併發後臺線程尋找可回收的對象時、有些區塊包含可回收的對象要比其餘區塊多不少。雖然在清理這些區塊時G1仍然須要暫停應用線程、但能夠用相對較少的時間優先回收包含垃圾最多區塊。這也是爲何G1命名爲Garbage First的緣由:第一時間處理垃圾最多的區塊。

  • 並行與併發:G1能充分利用多CPU、多核環境的硬件優點,使用多個CPU來減小STW的停頓時間
  • 分代收集:仍是與其餘收集器同樣採用分代收集,不過older與young再也不是連續的空間了
  • 空間整合:總體是感受採用「標記-整理」;局部之間是靠兩個region基於「複製算法」;這兩種策略因此不會產生空間碎片
  • 可預測停頓:創建可預測的停頓時間模型,能讓使用者明確指定一個長度爲M毫秒的時間片斷內,消耗在垃圾收集器上的時間不得超過N毫秒,這幾乎是實時Java(RTSJ)的垃圾收集器的特徵了

平時工做中大多數系統都使用CMS、即便靜默升級到JDK7默認仍然採用CMS、那麼G1相對於CMS的區別在:

  • G1在壓縮空間方面有優點
  • G1經過將內存空間分紅區域(Region)的方式避免內存碎片問題
  • Eden, Survivor, Old區再也不固定、在內存使用效率上來講更靈活
  • G1能夠經過設置預期停頓時間(Pause Time)來控制垃圾收集時間避免應用雪崩現象
  • G1在回收內存後會立刻同時作合併空閒內存的工做、而CMS默認是在STW(stop the world)的時候作
  • G1會在Young GC中使用、而CMS只能在O區使用

就目前而言、CMS仍是默認首選的GC策略、可能在如下場景下G1更適合:

  • 服務端多核CPU、JVM內存佔用較大的應用(至少大於4G)
  • 應用在運行過程當中會產生大量內存碎片、須要常常壓縮空間
  • 想要更可控、可預期的GC停頓週期;防止高併發下應用雪崩現象

GC模式:G1中提供了三種模式垃圾回收模式,young gc、mixed gc 和 full gc,在不一樣的條件下被觸發。

  • young gc:發生在年輕代的GC算法,通常對象(除了巨型對象)都是在eden region中分配內存,當全部eden region被耗盡沒法申請內存時,就會觸發一次young gc,這種觸發機制和以前的young gc差很少,執行完一次young gc,活躍對象會被拷貝到survivor region或者晉升到old region中,空閒的region會被放入空閒列表中,等待下次被使用。

    參數 含義
    -XX:MaxGCPauseMillis 設置G1收集過程目標時間,默認值200ms
    -XX:G1NewSizePercent 新生代最小值,默認值5%
    -XX:G1MaxNewSizePercent 新生代最大值,默認值60%
  • mixed gc:當愈來愈多的對象晉升到老年代old region時,爲了不堆內存被耗盡,虛擬機會觸發一個混合的垃圾收集器,即mixed gc,該算法並非一個old gc,除了回收整個young region,還會回收一部分的old region,這裏須要注意:是一部分老年代,而不是所有老年代,能夠選擇哪些old region進行收集,從而能夠對垃圾回收的耗時時間進行控制。

    那麼mixed gc何時被觸發?

    先回顧一下cms的觸發機制,若是添加了如下參數:

    `-XX:CMSInitiatingOccupancyFraction=``80` `-XX:+UseCMSInitiatingOccupancyOnly`

    當老年代的使用率達到80%時,就會觸發一次cms gc。相對的,mixed gc中也有一個閾值參數 -XX:InitiatingHeapOccupancyPercent,當老年代大小佔整個堆大小百分比達到該閾值時,會觸發一次mixed gc.

    mixed gc的執行過程有點相似cms,主要分爲如下幾個步驟:

    1. initial mark: 初始標記過程,整個過程STW,標記了從GC Root可達的對象
    2. concurrent marking: 併發標記過程,整個過程gc collector線程與應用線程能夠並行執行,標記出GC Root可達對象衍生出去的存活對象,並收集各個Region的存活對象信息
    3. remark: 最終標記過程,整個過程STW,標記出那些在併發標記過程當中遺漏的,或者內部引用發生變化的對象
    4. clean up: 垃圾清除過程,若是發現一個Region中沒有存活對象,則把該Region加入到空閒列表中
  • full gc:若是對象內存分配速度過快,mixed gc來不及回收,致使老年代被填滿,就會觸發一次full gc,G1的full gc算法就是單線程執行的serial old gc,會致使異常長時間的暫停時間,須要進行不斷的調優,儘量的避免full gc.

G1摘抄借鑑:

http://www.importnew.com/27793.html

http://ifeve.com

https://juejin.im/entry/5af0832c51882567244deb44

內存分配與回收策略

主要是如下五種策略,目的都是在結合JVM實際狀況下,儘量的提升效率,且作到安全可靠

  • 對象優先在Eden分配:大多數狀況下,對象在新生代Eden區中分配。當Eden區沒有足夠空間進行分配時,虛擬機將發起一次Minor GC(新生代GC)。
  • 大對象直接進入老年代:大對象對虛擬機來講是一個很壞的消息,常常出現大對象會容易致使內存還有很多空間,可是知足不了這個大對象,因此提早觸發垃圾收集以獲取足夠的空間來容納安置它們。能夠開啓一個參數,是大對象直接進入老年代,在老年代分配,這樣作的目的是避免在Eden區以及兩個Survivor區之間大量的內存複製(新生代採用的是複製算法)。
  • 長期存活的對象將進入老年代:給對象加一個年齡計數器,每熬過一次Minor GC後仍然存活,而且可以被Survivor容納的話,對象年齡就加一。能夠設置一個參數,當年齡足夠就晉升老年代之中去。
  • 動態對象年齡斷定:並不必定要求,全部對象的年齡必需要達到某一個年齡值,若是Survivor空間中相同年齡全部對象的大小的總和大於Survivor空間的一半,年齡大於等於該年齡對象就能夠直接進入老年代,無須必定要達到某個年齡才能進去。
  • 空間分配擔保:發生在Minor GC以前,虛擬機會先檢查老年代最大可用的連續空間是否大於新生代全部對象總空間,若是這個條件成立,那麼Minor GC能夠確保是安全的。若是不成立,那麼會查看HandlePromotionFailure設置值是否容許擔保失敗。若是容許,那麼就會繼續檢查老年代的最大可連續空間是否大於歷次晉升到老年代對象的平均大小,若是大於就嘗試進行一次Minor GC,雖然有風險。若是小於就進行Full GC,來回收老年代的空間。圈子雖然繞的大,可是能夠避免頻繁的FULL GC。
相關文章
相關標籤/搜索