深入理解Java虛擬機(3)垃圾回收

本文主要解決3個問題:

  • 哪些內存需要回收
  • 什麼時候回收
  • 如何回收

一、哪些內存需要回收?

程序計數器、虛擬機棧、本地方法棧三個區域隨線程而生,隨線程而滅,而且每一個棧幀中分配多少內存基本在類結構確定下來時就是已知的,不需要考慮複雜的回收問題。線程結束,內存就直接回收了。
Java堆和方法區則只有處於運行時纔會知道存放哪些實例數據等。

  • Java堆回收類實例
  • 方法區主要回收廢棄常量和無用的類

對象回收判定算法

可達性分析算法
通過一系列的稱爲“GC Roots”的對象作爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈,當一個對象到GC Roots沒有任何引用鏈相連時(用圖論的話來說就是從GC Roots到這個對象不可達),則證明此對象是不可用的,其將會被判定爲可回收對象。

GC Roots:
  • 虛擬機棧
  • 方法區靜態屬性引用的對象
  • 方法區常量引用的對象
  • 本地方法棧中JNI引用的對象
Java引用類
  • 強引用就是在程序代碼中普遍存在的,類似Object obj=new Object()這類的引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象。
  • 軟引用是用來描述一些還有用但並非必須的元素。對於它在系統將要發生內存溢出異常之前,將會把這些對象列進回收範圍之中進行第二回收,如果這次回收還沒有足夠的內存纔會拋出內存溢出異常。
  • 弱引用是用來描述非必須對象的,但是它的強度比軟引用更弱一些,被引用關聯的對象只能生存到下一次垃圾收集發生之前,當垃圾收集器工作時,無論當前內存是否足夠都會回收掉只被弱引用關聯的對象
  • 虛引用的唯一目的就是能在這個對象被收集器回收時收到一個系統通知

二、什麼時候回收?

對象的垃圾回收:

宣告一個對象死亡,至少要經歷兩次標記過程:
若第一次通過可達性算法被判定爲可回收,那麼他將會被第一次標記且進行一次篩選,篩選條件是該對象是否有必要執行finalize()方法,若對象未覆寫finalize()方法,或finalize()方法已被虛擬機調用過,則沒有必要執行
若有必要執行:對象放置到一個隊列,並稍後由一個虛擬機自動建立的,低優先級的Finalize線程去執行
finalize()是對象逃脫死亡命運的最後一次機會,之後GC會對該隊列中的對象再次進行標記,若還是沒有可達引用鏈,則他會被真的回收。

  • 任何一個對象的finalize()方法只會被系統調用一次

方法區的垃圾回收:

廢棄的常量:當前沒有實例的屬性賦值爲該常量
無用的類:3個條件都要滿足

  • Java堆中不含該類的實例
  • 加載該類的ClassLoader已被回收
  • 該類對應的java.lang.Class對象沒有被引用,包括反射

三、如何回收?垃圾回收算法

Mark-Sweep算法

首先標記出所有需要回收的對象,在標記完成後統一回收被標記的對象

  1. 效率問題:標記和清除兩個過程的效率都不高
  2. 空間問題:會產生大量不連續的內存碎片

copying算法

將可用內存按容量大小分爲大小相等的兩塊,當這一塊的內存用完了,就將存活的對象複製到另一塊,然後清除已使用過的內存空間,每次對整個半區回收

  • 效率更高,無內存碎片
  • 對象存活率較高是,有較多的複製操作,效率變低
  • 不足是內存自動變爲了之前的一半,代價較高

解決辦法: 不按照1:1比例劃分內存,幾乎98%的對象都是 ‘朝生夕死’,HotSpot默認按照8:1,將內存劃分爲1塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor,只有10%內存被“浪費”,當另一個Survivor空間沒有足夠空間存放上一次新生代收集下來的存活對象時,這些對象將直接通過分配擔保機制進入老年代。

mark-compact算法

標記回收對象之後,讓所有存活的對象都像一端移動,然後直接清理邊界以外的內存。

分代收集算法(各算法結合)

將Java堆分爲新生代,老年代,根據各個年代的特點採用適合的算法。
新生代:copying算法
老年代:mark-sweep算法或者mark-compact

HotSpot的算法實現

1,枚舉根節點
VM 的準確式內存管理
虛擬機有能力知道內存中某個位置數據具體是引用類型還是其他類型,這樣在GC的時候能夠準確的判斷堆上的數據是否還可能被使用。
HotSpot實現中,通過使用一組OopMap的數據結構來實現該特性。在類加載完成之後,HotSpot就把對象內各個偏移量上數據類型是什麼都計算出來,這樣GC掃描時也知道這些信息,直接去順着引用類型的數據掃描就行了,快速準確
2,安全點
僅在安全點中斷,GC設置中斷標誌,各線程主動去輪詢標誌,從而觸發線程中斷,開始執行GC
3, 安全區域
在一段代碼片段如sleep,blocked狀態中,內存引用不會發生變化,在這片區域的任何位置GC都是安全的。即連續的安全點。
線程執行到安全區域的代碼之後,標識自己進入安全區域,這樣,JVM發起GC時,就不用管該線程了。

四、HotSpot中的垃圾收集器

JVM收集器
上面爲新生代收集器,下面是老年代收集器。如果兩個收集器之間存在連線,就說明它們可以搭配使用。

併發和並行
  先解釋下什麼是垃圾收集器的上下文語境中的並行和併發:

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

Serial收集器(串行GC)

   是Jvm client模式下默認的新生代收集器 jdk1.3
   這個收集器是一個單線程的收集器,使用Copying算法。它在進行垃圾收集時,它不僅只會使用一個CPU或者一條收集線程去完成垃圾收集作,而且必須暫停其他所有的工作線程(用戶線程),直到它收集完成。

ParNew收集器(並行GC)

   是運行在Service模式下虛擬機中首選的新生代收集器
   Serial收集器的多線程版本,除了使用多線程進行收集以外,其餘行爲和Serial收集器一樣。
   PreNew收集器在單CPU環境中絕對沒有Serial的效果好,由於存在線程交互的開銷。
   可通過-XX:parallelGCThreads參數來限制收集器線程數

Parallel Scanvenge(並行回收GC)

   新生代收集器,它是使用複製算法的收集器,又是並行的多線程收集器。
   parallel Scavenge收集器的目標則是達到一個可控制的吞吐量。
   吞吐量:就是CPU用於運行用戶代碼的時間與CPU總消耗時間的比值。即吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間)

Serial old收集器(串行GC)

   是Serial收集器的老年代版本,是一個單線程收集器,使用Mark-Compact算法。

Parallel Old收集器(併發GC)

   是Parallel Scavenge收集器的老年代版本,使用多線程和Mark-Compact算法。

CMS收集器(Concurrent Mark Sweep併發GC)

以獲取最短回收停頓時間爲目標的收集器,JDK1.5發佈

   CMS收集器是基於標記清除算法實現的,整個過程分爲4個步驟:
   ①.初始標記(CMS initial mark)
②.併發標記(CMS concurrent mark)
③.重新標記(CMS remark)
④.併發清除(CMS concurrent sweep)
優點:併發收集、低停頓
缺點:
1, CMS收集器對CPU資源非常敏感,在併發(併發標記、併發清除)階段,雖然不會導致用戶線程停頓,但是會佔用CPU資源而導致應用程序變慢,總吞吐量下降,此時始終不會佔用少於25%的CPU。CMS默認啓動的回收線程是(CPU數量+3)/4;
2, CMS收集器無法處理浮動垃圾,可能出現“Concurrent Mode Failure“,失敗後而導致另一次Full GC的產生。由於CMS併發清理階段用戶線程還在運行,伴隨程序的運行自熱會有新的垃圾不斷產生,這一部分垃圾出現在標記過程之後,CMS無法在本次收集中處理它們,只好留待下一次GC時將其清理掉。這一部分垃圾稱爲“浮動垃圾”。
3, CMS是基於標記清除算法實現的,會產生大量碎片。

G1收集器:

  它是一款面向服務器應用的垃圾收集器,JDK1.7發佈,其目標就是替換掉JDK1.5發佈的CMS收集器
  優點:
  1.併發與並行:G1能充分利用多CPU、多核環境下的硬件優勢,使用多個CPU(CPU或CPU核心)來縮短停頓(Stop The World)時間。
  2.分代收集:G1不需要與其他收集器配合就能獨立管理整個GC堆,但他能夠採用不同方式去處理新建對象和已經存活了一段時間、熬過多次GC的老年代對象以獲取更好收集效果。
  3.空間整合:從整體來看是基於“標記-整理”算法實現,從局部(兩個Region之間)來看是基於“複製”算法實現的,但是都意味着G1運行期間不會產生內存碎片空間,更健康,遇到大對象時,不會因爲沒有連續空間而進行下一次GC,甚至一次Full GC。
  4.可預測的停頓:降低停頓是G1和CMS共同關注點,但G1除了追求低停頓,還能建立可預測的停頓模型,可以明確地指定在一個長度爲M的時間片內,消耗在垃圾收集的時間不超過N毫秒
  5.跨代特性:之前的收集器進行收集的範圍都是整個新生代或老年代,而G1擴展到整個Java堆(包括新生代,老年代)。
如何實現:
1.如何實現新生代和老年代全範圍收集:其實它的Java堆佈局就不同於其餘收集器,它將整個Java堆劃分爲多個大小相等的獨立區域(Region),仍然保留新生代和老年代的概念,可是不是物理隔離的,都是一部分Region(不需要連續)的集合。
2.如何建立可預測的停頓時間模型:是因爲有了獨立區域Region的存在,就避免在Java堆中進行全區域的垃圾收集,G1跟蹤各個Region裏面的垃圾堆積的價值大小(回收可以獲得的空間大小和回收所需要的時間的經驗值),後臺維護一個優先隊列,根據每次允許的收集時間,優先回收價值最大的RegionGarbage-First理念)。因此使用Region劃分內存空間以及有優先級的區域回收方式,保證了有限時間獲得儘可能高的收集效率。
3.如何保證垃圾回收真的在Region區域進行而不會擴散到全局:由於Region並不是孤立的,一個Region的對象可以被整個Java堆的任意其餘Region的對象所引用,在做可達性判定確定對象是否存活時,仍然會關聯到Java堆的任意對象,G1中這種情況特別明顯。而以前在別的分代收集裏面,新生代規模要比老年代小許多,新生代收集也頻繁得多,也會涉及到掃描新生代時也會掃描老年代的情況,相反亦然。解決:G1收集器Region之間的對象引用以及新生代和老年代之間的對象引用,虛擬機都是使用Remembered Set來避免全堆掃描。G1中每個Region都有一個與之對應的Remembered Set,虛擬機發現程序在對Reference類型的數據進行寫操作時,會產生一個Write Barrier暫時中斷寫操作,檢查Reference引用的對象是否處於不同的Region之中(分代的例子中就檢查是否老年代對象引用了新生代的對象),如果是則通過CardTable把相關引用信息記錄到被引用對象所屬的Region的Remembered Set之中,當進行內存回收時,在GC根節點的枚舉範圍中加入Remembered Set即可避免全堆掃描。

  運行步驟:
1.初始標記:初始標記僅僅標記GC Roots能直接關聯到的對象,並且修改TAMS(Next Top at Mark Start)的值,讓下一階段用戶程序併發運行時,能在正確可用的Region中創建新的對象。這階段需要停頓線程,不可並行執行,但是時間很短。
2.併發標記:此階段是從GC Roots開始對堆中對象進行可達性分析,找出存活對象,此階段時間較長可與用戶程序併發執行。
3.最終標記:此階段是爲了修正在併發標記期間因爲用戶線程繼續運行而導致標記產生變動的那一份標記記錄,虛擬機將這段時間對象變化記錄在線程Remembered Set Logs裏面,最終標記階段需要把Remembered Set Logs的數據合併到Remembered Set中,這段時間需要停頓線程,但是可並行執行。
4.篩選回收:對各個Region的回收價值和成本進行排序,根據用戶期望的GC停頓時間來制定回收計劃。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章