天天看點

深入了解Java虛拟機----(三)記憶體配置設定政策和垃圾收集器

垃圾回收:     垃圾回收面臨着三個問題:回收什麼、什麼時候回收、怎麼回收。     哪些對象已經不再被需要了,就需要被回收。

  • 引用計數法:教科書式解釋,每個對象維護對它的引用的個數。但是主流虛拟機不适用,因為難解決循環引用。
  • 可達性分析算法:主流适用的算法。虛拟機標明一些GCRoot對象,如果一個對象和GCRoot對象間沒有引用鍊,則這個對象是無用的、可以回收的。可以作為GCRoot的有:
    • 虛拟機棧中引用的對象
    • 方法區中,類靜态屬性引用的對象
    • 方法區中常量引用的對象
    • 本地方法棧中引用的對象

    這兩種方法都和引用相關。JDK1.2對引用進行了擴充:

  • 強引用:代碼常用的引用,存在就不會被回收
  • 軟引用:有用但非必須的引用。如果快OOM了,會把這部分對象進行回收。SoftReference
  • 弱引用:非必須引用。隻能活到下一次回收之前。WeakReference
  • 虛引用:不會對生存時間有影響。唯一作用是可以在對象被回收時受到通知。PhantomReference

    可達性分析過後,如果一個對象和GCRoot間沒有可達的引用鍊了,它并不是立即被回收。首先要看它有沒有finalize()方法,有的話被沒被執行過。如果有而且沒執行過,把它放到一個F-Queue隊列,虛拟機一個單獨的線程區調用對象的finalize方法。但是并不保證一定執行完。對象可以在這個方法中救自己--給自己一個引用。如果沒救成功,則就被回收了。救成功的對象,再次被可達性分析篩選為可回收時,不會再執行fianlize, 因為隻會被執行一次! finalize方法代價高、不确定性大, 非常不建議使用 。     方法區----常說的HotSport中的永久代,也可以被回收,但是效果通常不如堆好。而且堆内對象不使用了一定會回收,而方法區是可以被回收,是有差别的!字面常量等比較容易處理,沒有引用就可以被回收。而類回收條件酒比較嚴苛:

  • 類的所有執行個體對象已經被回收
  • 加載類的ClassLoader已經被回收
  • 沒有對該類對象的引用

    可以使用-Xnoclassgc控制。大量使用反射、動态代理、動态JSP等的系統,建議開啟,防止方法區溢出。     垃圾收集算法:

  • 标記清除:基礎算法。标記的方法上面說過了。這個算法的缺點是會産生不連續的記憶體碎片,導緻明明有很多記憶體剩餘,但由于新對象太大,沒有足夠的連續空間而引發又一次的GC。
  • 複制算法:将空間劃分為相等的兩塊區域,目前的滿了,就把存活的對象複制到第二塊塊,整齊排列,然後在第二塊開始為新對象配置設定記憶體。好處是解決了标記清除的碎片問題,但代價太大----記憶體縮小一半。商業虛拟機絕大部分采用這種算法回收新生代,但新生代一般98%的對象都是很快死掉,是以不用1:1的配置設定,而是将新生代分為一塊eden和兩塊survivor,每次把eden和一塊survivor的存活對象複制到另一塊,回收剩下的。預設比例eden:suvivor = 8:1,是以這種方法隻有10%的空間被“浪費”。但是,我們并不能保證存活下來的對象永遠少于新生代10%的大小,如果不夠了,需要依賴老年代配置設定擔保。
  • 标記整理算法:像老年代這種區域,對象的存活率較高複制算法的效率就會比較低。而且不想浪費50%就要配置設定擔保,是以老年代一般不使用複制算法。标記整理是标記後,将存活對象移動到一端,将剩下的記憶體清理。
  • 分代收集算法:分代隻是将整個記憶體區域劃分為幾個小區域,可以在每個區域上選擇各自的收集算法。例如新生代,大多短命,采用複制算法。老年代存活率高,沒有額外空間擔保,采用标記清除或标記整理。

HotSport實作:     虛拟機實作必須保證高效、準确。HotSport在下面幾個方面的實作方法:

  • 枚舉根節點:虛拟機必須對一個一緻性快照分析才能準确,不能一邊分析一邊還在變,是以枚舉根節點要暫停所有線程(Stop-The-World)。如果挨個的分析方法區和棧中的引用,會非常耗時,HotSport用一個叫做OopMap的結構記錄了這些資訊,進而可以直接快速的拿到資料。
  • 安全點SafePoint:OopMap的狀态随時都會改變,不可能為每條指令都生成OopMap,是以定義了安全點的概念。隻有在安全點才能生成OopMap,才能暫停線程進行回收。安全點選擇原則是方法調用、循環跳轉、異常跳轉等具有讓程式長時間執行的特征的指令。
  • 安全區域:安全點的方案,線上程不配置設定cpu時間的情況下,如sleep,就無法走到安全點暫停,GC就被卡住了,這時需要安全區域解決。安全區域是指在一個區域内,引用關系不會發生變化,任意位置開始GC都是可以的。離開安全區域時,需要等GC枚舉根節點結束。

垃圾收集器:     垃圾收集器是上面介紹的垃圾收集算法的具體實作。在HotsSport中包含以下幾種:

深入了解Java虛拟機----(三)記憶體配置設定政策和垃圾收集器

    先解釋:并行是指多個收集線程并行,并發是收集線程和使用者線程同時執行。     這7中作用于不同的代,連線表示可以配合使用。

  • Serial:最基本的單線程收集器。而且會暫停所有線程直到收集結束。Serial作用于新生代用複制算法。它有弊端,但是是現在client模式下的預設新生代收集器。
  • ParNew:就是Serial的多線程并行版本。除了多線程,其他與Serial一模一樣,複制算法,暫停所有線程。是新生代收集器的首選,可以與CMS配合使用。
  • Parallel Seavenge:也是作用于新生代複制算法的實作。差別是追求的是吞吐量,可以參數設定最大停頓時間和吞吐量。收集器會收集系統資訊,自動調節各區域大小、比例等來優化,達到設定的目标。
  • SerialOld:是Serial的老年代版本,單線程,标記整理算法。主要用于client模式。另一個用途是作為CMS的後備方案。
  • Parallel Old:Parallel Seavenge的老年代版本,标記整理算法。
  • CMS:追求最短停頓時間,老年代,标記清除算法。分為4個步驟:初始标記、并發标記、重新标記、并發清除。初始标記和重新标記需要stop the world。初始标記隻是标記GCRoot直接關聯的對象,非常快。并發标記是GCRoot的tracing過程。重新标記是為了修正并發标記期間,使用者線程繼續運作導緻的變動,也很迅速。第一步和第三部都很快,是以使用者線程停頓時間很短,這就是CMS的目的。它很優秀:并發、停頓短。但是也有缺點:
    • 并發階段和使用者線程并行,會占用cpu資源,影響使用者程式,總吞吐量降低。
    • 并發期間程式會需要空間給新對象,是以不能等100%滿了再開始回收。這個比例需要控制。太小導緻浪費,太大可能會并發階段不夠了,而且用備用的SrialOld,等待時間很長。
    • 因為是用的标記清除算法,會有碎片,如果無法找到足夠的連續空間,會因為一次full gc。可以參數設定在要full gc時,合并整理。這個整理操作無法并發,是以停頓時間會變長。還有一個參數設定多少次的不整理的full gc後,跟随一次帶整理的full gc。
  • G1收集器:
    • 縮短stop the world時間
    • 不用配合其他算法就能對整個堆管理,但可以用不同的方法管理不同的對象區域。
    • 總體看是标記整理,局部看是複制算法。是以不會産生碎片。
    • 與CMS相比,不知追求低停頓,同時建立可預測停頓時間的模型。

          G1将堆劃分為多個相同大小的區域,新生代和老年代的概念仍被保留,但是隻是一系列區域的集合而已。它将每個區域估算回收價值,在使用者允許的停頓時間内,盡可能的回收價值高的區域。它的步驟和CMS非常類似:

  • 初始标記
  • 并發标記
  • 最終标記
  • 篩選回收

          但是回收階段不是并發的,因為暫停使用者線程能大幅提高回收效率。如果追求低停頓、可預測停頓,G1可以嘗試。如果追求吞吐量,沒有什麼好處。

GC日志:     

深入了解Java虛拟機----(三)記憶體配置設定政策和垃圾收集器

    開頭數字式虛拟機運作時長,機關秒。中括号然後跟着本次gc類型:GC或Full GC。再中括号,跟着回收區域。然後是已使用記憶體->回收後已使用記憶體(區域總記憶體)。然後跟着區域回收用時,機關秒。然後中括号外是堆的總記憶體情況和總用時。

常用參數:

深入了解Java虛拟機----(三)記憶體配置設定政策和垃圾收集器
深入了解Java虛拟機----(三)記憶體配置設定政策和垃圾收集器

記憶體配置設定政策:

  • 在eden區配置設定記憶體,也是大多數情況
  • 在老年代直接配置設定,因為配置設定擔保機制。
  • 新生代熬過了一定次數的Minor GC,年齡足夠大的就進入老年代。預設年齡閥值為15,可配置。
  • 并不是永遠炒鍋閥值才進入老年代。當新生代内同一年齡的對象大小總和超過了Survivor區的一半,年齡大于等于該年齡的對象,直接進入老年代。
  • 配置設定擔保:使用複制收集算法時,可能某次minor gc存活對象很多,survivor不足以承受,就需要老年代配置設定擔保。是以在進行Minor GC前,虛拟機會檢查老年代剩餘空間是否大于新生代對象大小綜合。如果大于則安全;如果小于,看參數設定是否允許冒險,如果允許冒險,會比較以往每次從新生代到老年代的對象大小的平均值和目前老年代的空餘空間大小,如果剩餘空間大,則允許此次Minor GC,如果剩餘空間小或者不允許冒險,則Full GC。如果Minor後,老年代空間還是不夠用,則還要FullGC。雖然如果冒險失敗了,繞了很大很大的圈子,但是一般情況下,還是設定允許冒險,避免FullGC的頻繁發生。這個開關是HandlePromotionFailure。