JAVA 垃圾回收讀書筆記

對象已死

  在JAVA代碼運行中,會不停的建立對象,由於內存空間不是無限的,Java虛擬機必須不停的回收無用的數據空間。那麼虛擬機是怎麼判斷對象空間是須要被回收的呢,也就是怎麼樣的數據算是垃圾數據呢?java

引用計數法

  引用計數法是指給沒一個對象中添加一個引用計數器,每當一個地方引用了該對象,就讓該對象的引用數+1,當一對象的引用值變爲0的時候,就表示該對象已經沒法訪問。垃圾回收器就能夠回收這個數據空間。這種算法簡單粗暴,有些語言會使用該算法來判斷對象是否已經死亡。程序員

  可是該算法有一個比較大的問題。當兩個對象互相引用對方,且沒有其餘引用時,則此時這兩個對象引用計數都是1,垃圾回收器將永遠沒法回收他們。以下方代碼所示:面試

 1 public class ReferenceCounter {
 2 
 3     private ReferenceCounter reference;
 4 
 5     public ReferenceCounter getReference() {
 6         return reference;
 7     }
 8 
 9     public void setReference(ReferenceCounter reference) {
10         this.reference = reference;
11     }
12 
13     public static void main(String[] args) {
14         ReferenceCounter referenceCounterA = new ReferenceCounter();
15 
16         ReferenceCounter referenceCounterB = new ReferenceCounter();
17         referenceCounterA.reference = referenceCounterB;
18         referenceCounterB.reference = referenceCounterA;
19 
20         referenceCounterA = null;
21         referenceCounterB = null;
22     }
23 }
View Code

  由於引用計數法沒法處理對象之間相互引用的問題,因此JAVA虛擬機沒有使用該算法來校驗對象是否能夠回收。算法

可達性分析算法

  可達性分析算法是大部分JAVA虛擬機所採用的主流算法。該算法有一個根節點稱之爲GCRoots,經過一系列的根節點引用,若是經過GCRoots可以訪問到的數據所有是有效數據,不可回收。反之,則爲垃圾數據,等待垃圾回收器的回收。以下圖所示:安全

  

  object一、object二、object3三個對象經過GCRoots能夠訪問到,所以這三個對象是不可回收的。object4,object5,object6經過GCRoots是不能訪問到的,由於這幾個對象是能夠回收的。那麼,GCRoots又是什麼呢?在JAVA語言裏,能夠做爲GCRoot的對象有這幾種服務器

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

  當一個對象對於GCRoots不可達時,併發直接被垃圾回收。會先執行該對象的fnalize()方法,此時該對象有一次自救機會,將本身關聯到GCRoot上。若是對象在finalize()方法中將本身關聯到GCRoots上,該對象將不會被垃圾回收器回收。可是虛擬機並不保finalize()執行完畢以後才進行垃圾回收,所以finalize()方法並不能必定自救成功。而且若是一個對象被自救過一次以後,仍舊脫離GCRoot,第二次將再也不執行finalize()方法。finalize()方法運行代價高昂,不穩定性高,只是JAVA誕生之初爲了讓C/C++程序員接受而作出的一種妥協,有些說法說finalize()能夠用來關閉外部資源,可是try{}finally{}能夠執行得更好,JAVA程序員徹底能夠無視finalize()的用法。多線程

方法區回收

  方法區的數據會被回收嗎?(方法區即HotSpot中的永久代,JDK8已經移除,用元空間替代)
併發

  這是一道常見的老面試題,先說這道題的答案,方法區的數據是會被回收的。方法區回收主要分爲兩部分:無用常量和無用的類。框架

  回收無用常量:假如咱們在代碼中生成一個常量"123456",該字符串會被儲存在常量池中。當該常量已經沒有被任何引用所持有,也就是代碼已經沒法經過引用獲取到該常量的時候,這個常量就是能夠被回收的。ide

  回收無用的類 : 回收一個無用的類相比起來就比較複雜,須要保證如下幾點

  • 該類的全部實例都已經被回收
  • 加載該類的classLoad已經被回收
  • 該類對應的java.lang.Class沒有被其餘任何地方引用,沒法在任何地方經過反射獲取到該類

  知足上面三個條件的java類能夠被回收,但不必定會被回收,須要經過-XnoClass參數進行控制,還能夠經過-verbose:class 和 -XX:+TraceClassLoading、-XX:+TraceClassUnLoding查看類加載和卸載信息。在大量使用反射、動態代理、CGLib等byteCode框架、動態生成JSP以及OSGi這類頻繁自定義ClassLoader的場景都須要虛擬機具有類卸載的功能,保證永久代不會溢出。

 

垃圾回收算法

標記-清除算法

   標記清除算法是最基礎的算法,算法分爲標記-清除兩個階段。第一階段將全部須要回收的對象進行標記,第二階段將全部被標記的數據進行清除。該算法有兩個不足:一個是效率問題,標記和清除兩個階段的效率都不高。二是空間問題,該算法在清除以後會有大量的不連續空間。大量不連續空間可能會致使JVM在分配大對象的時候,沒有足夠的空間。

複製算法

  複製算法是將內存空間分爲大小相同的兩塊,每次只使用其中一塊,當垃圾回收的時候將不須要回收的數據複製到另一塊內存中,清理剩餘的內存。由於年輕代的數據大部分都是朝生夕死,因此該算法在不少商用虛擬機的年輕帶上使用。常見的內存方式就是將年輕帶分爲Eden區和兩個Survivor區,每次年輕帶使用Eden區和一個Survivor區。當進行垃圾回收的時候,將Eden區和Survivor區中存活的對象複製到另一個Survivor區中(若是Survivor區中的內存不足,經過分配擔保原則,將存在對象放入年老代)。HotSpot中的Eden和Survivor的內存空間比例爲8:1:1,能夠經過-SurvivorRatio進行調整,假設將這個數據改成4,則Eden和Survivor區域的大小就分爲4:1:1。

標記-整理算法

  標記-整理算法感受更像是對標記-清楚算法的一種改良。標記整理算法在對內存進行標記完成後,將全部存活的對象往內存的一個地方進行移動,而後刪除邊界外的全部內存。由於年老代沒有能夠分配擔保的內存,所以沒辦法使用複製算法。

 

分代收集算法

  分代收集算法就沒啥好說的了,就是對年輕代和年老代使用不一樣的收集算法進行收集。

HotSpot的算法實現

枚舉根節點

  在如今的應用中,內存愈來愈大,分析對象是否能夠回收時,若是使用上面講過的可達性分析算法來計算,必然會花費不少時間。並且爲了保證JVM在計算時候,內存中的對象引用不是在一直變化中,必須暫停全部工做線程。這是STOP THE WORLD的一個重要緣由,爲了快速分析哪裏內存能夠回收,哪裏內存不能夠回收,HotSpot引入了一個OoPMap來記錄。在類加載過程完成的時候,HotSpot就將對象內什麼偏移量上是什麼類型的數據計算出來,在JIT編譯過程當中,也會在特定的位置記錄下棧和寄存器中哪些位置是引用。而後當GC發生時候,就不須要掃描整個棧,而只須要掃描OopMap就能夠獲取到想要的信息,完成可達性分析。

安全點

  在OopMap的協助下,HotSpot能夠快速且準確的完成GCRoot枚舉。可是若是爲每一條指令都生成OopMap,那麼將須要大量的額外空間,這個GC的空間成本將會很是高。其實,HotSpot只有在特定的位置上記錄這些信息,這些位置稱爲安全點,也就是並不是在全部地方都能停頓下來進行GC,必須停留在安全點才能進行GC,通常選擇在執行時間長的地方生成安全點,如如下:

  • 循環的末尾 
  • 方法臨返回前 / 調用方法的call指令後 
  • 可能拋異常的位置

對於安全點的另一個考慮,是讓GC時候,讓全部的線程都跑到一個安全的地方再停下來。有兩種方案:搶斷式中斷和主動式中斷。

搶斷式中斷:不須要其餘線程配合,當GC發生的時候,先暫停全部的線程,若是線程不在安全點上,則讓線程跑到下一個安全點。(幾乎沒有虛擬機採用這種方式)

主動式中斷:GC發生的時候,設置一個標識符,全部的線程跑到安全點的時候就去輪詢這個標識符,發現GC標識符爲真的時候,就將本身掛起。輪詢標識符的位置和安全點的位置和建立對象須要分配內存的地方。

安全區域

  在使用SafePoint時候,能夠基本實現GC的安全執行。但還有一種狀況,當線程沒有分配CPU時間的時候,例如線程處於Sleep或者Block狀態的時候。JVM將這種一段時間內關係不會發生變化的區域稱爲安全區。當線程執行到安全區,先將本身標識進入Safe Region,表示已經進入了安全區域。當這段時間裏發生了GC,就不用管標識本身爲Safe Region狀態的線程。若是線程要離開安全區域的時候,要先檢查系統是否完成了根節點枚舉,或者完成了整個GC。

垃圾收集器

  如上圖所示,年輕代採用的垃圾收集器爲Serial、ParNew、Parallel Scavenge。年老代的垃圾收集器爲CMS、Serial Old、Parallel Old。G1收集器能夠對年輕代和年老代共同收集。圖中的連線表示能夠搭配使用,例如能夠年老代若是選用CMS,則年輕代可使用serial和ParNew來進行垃圾收集。

新生代收集器

  Serial垃圾收集器:serial垃圾收集器是歷史最古老的垃圾收集器,並且從名字就能知道它是經過單線程進行垃圾收集。Stop The World這個詞在當時就從這裏產生。雖然這個收集器很老了,可是由於它的簡單高效,而且客戶端的內存通常不會太大,因此在客戶端仍是默認的垃圾收集器。Serial收集器在新生代使用複製算法。

  ParNew收集器:ParNew跟Serial相比,惟一的區別就是將Serial的單線程變動爲多線程,ParNew和Serial之間也公用不少代碼。可是它確是JDK8以前新生代首選收集器,由於只有ParNew和Serial能和CMS收集器配合。在單CPU的環境中,效率不如Serial,即使雙核也不能保證必定比Serilal強,可是隨着核數逐漸增多,ParNew多線程的優點才逐漸體現。

  Parallel Scavenge收集器:Parallel Scavenge所用算法跟ParNew差很少,可是Parallel Scavenge所要達到的目標是實現可控制的吞吐量,吞吐量=(運行代碼時間)/(運行代碼時間+垃圾回收時間)。停頓時間短的虛擬機適合用於用戶交互,吞吐量大的收集器適合用於服務器計算。

老年代收集器

  SerialOld垃圾收集器:Serial Old收集器是Serial的老年代版本,一樣是單線程收集器,它採用的是標記整理算法。

  Parallel Old垃圾收集器:Parallel Old是Parallel Scavenge老年代版本,使用多線程的標記-整理算法。直到Parallel Old出現,Parallel Scavenge在有了合適的應用組合。在Parallel Old出現以前,Parallel Scavenge只能和SerialOld配合,總體性能被拖累,並不實用。

  CMS垃圾收集器:CMS收集器是以最短回收停頓爲目標的收集器不少B/S系統的服務器上很注重響應速度,長時間的回收停頓是沒法接受的,所以CMS垃圾收集器就很是符合他們的需求。CMS基於標記-清除算法,相比其餘收集器稍微複雜。CMS回收過程分爲4個步驟:

  • 初始標記(標記GCRoots能直接關聯到的目標,STOP THE WORLD)
  • 併發標記(進行併發的GCRoot  Tracing操做,GCRoots Tracing就是可達性分析,能夠跟工做線程一塊兒運行)
  • 從新標記(修改由於併發標記時候,工做線程引發的標記變化,STOP THE WORLD
  • 併發清除(能夠跟工做線程一塊兒運行)

  由於CMS收集器能讓工做線程和垃圾回收線程一塊兒進行,因此程序響應時間受GC影響比較小。可是CMS也有三個明顯缺點:一、CMS對CPU資源敏感,由於併發收集,佔用了工做線程。二、CMS沒法處理浮動垃圾,由於CMS在清除工做的時候跟工做線程一塊兒併發,此時有可能產生新的垃圾。致使GC完成後,垃圾回收後,系統內存仍然不足,引發Full GC。三、CMS採用的"標記-清除"算法會致使內存不連續。

 

G1收集器(Garbage-First)

  G1垃圾收集器在JDK9中,被設置爲默認垃圾收集器。G1垃圾收集器再也不是以上面的分區方式來進行垃圾收集。而是在保留eden、Survivor、Tenured區的前提下,將內存劃分爲一個個更小Region區。G1跟蹤每一個區域內存的回收價值,在後臺維護一個優先列表,每次根據垃圾回收容許的時間,選擇回收價值最大的Region進行回收,這就是Garbage-First名字的由來。

  經過對每一個小Region區進行垃圾回收,實現了化整爲零的回收思路。可是若是某個Region裏的對象被其餘Region的對象所引用呢?其實這個問題再其餘垃圾回收器中也有相同問題,可是由於G1的分區更多,因此這個問題在G1中會更加突出。爲了避免用在收集某個Region的時候,掃描整個堆引用,虛擬機都是採用Remembered Set來記錄引用信息(年老代引用年輕帶也是經過這個)。G1中每個Region區都維護一個RemerberSet來記錄,假設A區Region中的對象被B Region區對象所引用時候,就經過CardTable被相關引用信息記錄在A區的Remerber Set中。

 

   Remerber Set是個hashMap格式,key指向被引用的區域,Value指向引用該對象對應的Card Table。而Card Table記錄數據爲0或者1,且對應某個區域的內存。所以,當GC發生時候,只須要對當前Region GC根節點枚舉的範圍中加入Remerber Set便可保證不對全堆掃描也不會有遺漏。

     G1收集器的運做大體能夠劃分爲如下幾個步驟:

  • 初始標記
  • 併發標記
  • 最終標記
  • 篩選回收

  這四個步驟跟CMS的回收大體相同,篩選回收的時候,G1對各個Region進行回收價值和回收成本進行排序,而後用戶指望的GC停頓進行回收。可是SUN公司透露其實這個階段也是能夠併發的。

內存分配

   JVM的內存分配根據選擇不一樣的垃圾回收器和不一樣的虛擬參數會略有不一樣,大體規則以下:

  

  

Tips

對象變老

  在Eden區正常發育的對象,沒經歷一次Minor GC,就給活下來的對象年齡+1,當年齡達到默認的15歲的時候,這些對象晉升到老年代。15歲是默認參數,能夠經過+XX:MaxTenuringThreshold來進行設置。

動態年齡斷定

  新對象除了常規等年齡大於年齡閾值晉升老年代,若是Survivor區中相同年齡全部對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的直接進入老年代。好比Survior區中8歲的對象佔了Survior一半空間以上,則8歲或者8歲以上的直接進入老年代。

空間分配擔保

  在發生Minor GC以前,虛擬機先檢查老年代最大的連續空間是否大於新生代全部對象總空間,若是這個條件成立,那麼Minor GC能夠確保安全。不然,要看HandlePromotionFailure是否容許,若是不容許,則直接進行Full GC。設置HandlePromotionFailure爲容許的狀況下,則取以前每一次晉升到老年代大小的平均值做爲參考,決定是否使用Full GC讓老年代騰出更多空間。通常狀況下,HandlePromotionFailure會打開,避免Full GC執行得太過頻繁。

相關文章
相關標籤/搜索