JVM之垃圾收集器與分配策略

       通過以上JVM兩篇文章的介紹,我們大致瞭解了Java的內存模型以及對象的存儲和分配,本文在基於以上知識開始講解內存的回收,本文嘗試解決以下的幾個問題:

 (1)哪些內存需要回收?

 (2)什麼時候進行內存回收?

 (3)如何回收內存?

      上篇文章文章我們已經講解了Java內存運行時內存區域的各個部分,其中程序計數器、虛擬機棧和本地方法棧三個區域隨線程而生隨線程而滅;棧中的棧幀隨着方法的進入和退出而有條不紊的執行着出棧和入棧的操作,每一個棧幀中分配多少內存基本上是在類結構確定下來時就是已知的,因此這幾個區域的內存分配和回收都具有確定性,在這幾個區域內疚不需要過多考慮回收的問題,因爲方法結束或者線程結束時,內存自然就隨着回收了。而Java堆和方法區則不一樣,一個接口中的多個實現類需要的內存可能不一樣,一個方法中的多個分支需要的內存也可能不一樣,我們只有在程序處於運行期間才能知道會創建哪些對象,這部分內存的分配和回收都是動態的,垃圾回收器所關注的是這部分內存。


如何確定需要回收的對象?

      在堆裏面存放着Java世界中幾乎所有的對象實例,垃圾收集器在對堆進行會收錢,首先要做的事情就是確定這些對象之中哪些還“活着”,哪些已經“死去”(即不可能再被任何途徑使用的對象)。目前主要有兩種方法來確定對象是死還是活。

引用計數法

      引用計數法是給對象中添加一個引用計數器,每當有一個地方引用它時,計數器就加1,當引用失效時,計數器值就減一:任何時刻計數器爲0的對象就是不可能再被使用的,但是這個算法有個問題,那就是它很難解決對象之間的相互循環引用的問題。

可達性分析法

      在主流的商用程序語言中都是通過可達性分析來判定對象是否存活的。這個算法的基本思路就是通過一些列的稱爲“GC Roots”的對象作爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連時,則證明此對象是不可用的。在Java語言中,可作爲GC Roots的對象包括:

   (1)虛擬機棧(棧幀中的本地變量表)中引用的對象。

   (2)方法區中類靜態屬性引用的對象。

   (3)方法區中常量引用的對象。

   (4)本地方法中JNI引用的對象。

引用的幾種類型

      無論是通過引用計數算法判斷對象的引用數量,還是通過可達性分析算法判斷對象的引用鏈是否可達,判定對象是否存活都與“引用”有關,有這樣一類對象:當內存空間還足夠時,則保留在內存之中,如果內存空間在進行垃圾收集後還是非常緊張,則可以拋棄這些對象。也就是如下四種引用類型

強引用

      強引用是在代碼中非常普遍的,類似 Object obj = new Object()這類引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象。

軟引用

      軟引用是用來描述一些還有用但並非必須的對象,對於引用關聯着的對象,在系統將要發生內存溢出異常之前,將會把這些對象列進回收範圍之中進行第二次回收。如果這次回收還有足夠的額內存,纔會拋出內存溢出異常,可以通過SoftReference類來實現軟引用。

弱引用

      弱引用也是用來描述非必需對象的,但是它的強度比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次垃圾收集發生之前。當垃圾收集器工作時,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象。可以通過WeakReference類來實現弱引用。

虛引用

       虛引用也稱爲幽靈引用或者幻影引用,他是最弱的一種引用類型,一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象實例。爲一個對象設置許虛引用關聯的唯一目地就是能在·這個對象被收集器回收時收到一個系統通知,可以通過PhantomReference類來實現虛引用。

小結

       強引用對象是永遠也不會被回收的;軟引用對象在內存不足時列入回收的範圍,下次回收,如果下次回收內存還不足則會包內存溢出異常;弱引用只能生存到下一次垃圾回收之前。虛引用的唯一目地就是在這個對象被收集時收到一個系統通知,可見,強引用、軟引用、弱引用、虛引用他們的引用強度是依次減弱

回收方法區

       方法區(永久代)的垃圾回收主要回收兩部分內容:廢棄常量和無用的類。回收廢棄常量與回收堆中的對象非常類似,以如常量池中的字面量爲例,加入一個字符串“abc”已經進入了常量池,但是當前系統沒有任何一個String對象是叫做“abc”的,換句話說,就是沒有任何String對象引用常量池中的"abc"常量,也沒有其他地方引用了這個字面量,如果這時發生內存回收,而且必要的話這個“abc”常量就會被系統清理。而判定一個類是否是一個無用的類則需要滿足以下條件:
  (1)該類所有的實例都已經被回收,也就是Java堆中不存在該類的任何實例。
  (2)加載該類的ClassLoader已經被回收。
  (3)該類對於的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。

垃圾收集算法

       由於垃圾收集算法的實現設計大量的程序細節,而且各個平臺的虛擬機操作內存的方法各不相同,一次你本節不打算過多的討論算法的實現,只是介紹幾種算法的思想及其發展·過程。

標記-清除算法

        最基礎的收集算法就是“標記-清除”(Mark-Sweap)算法,如同他的名子一樣,算法分爲“標記”和“清除”兩個階段:首先標記出所有需要回收的對象,在標記完成後統一回收所有被標記的對象,它的標記過程上面已經說過了。但是此算法有兩個不足:
  (1)效率不高,標記和清除兩個過程的效率都不高。
  (2)空間碎片問題,標記清除之後會產生大量不連續的內存碎片,空間碎片太多可能會導致以後在程序運行過程中需要分配較大對象時,無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集的動作,標記清除的執行過程如下:
  

複製算法

       爲了解決效率問題,一種稱爲“複製(Copying)”的算法出現了,它講課用的內存按照容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活着的對象複製到另外一塊上面,然後再把已使用過的內存空間一次清理掉。這樣是的每次都是整個半區進行內存回收,內存分配時也就不用考慮內存碎片等複雜情況,只要移動堆頂指針,按順序分配內存即可,實現簡單,運行高效。只是這種算法的代價是將內存縮小爲原來的一般,代價有點高,其執行過程如下:

標記-整理算法

       複製算法在對象存活率較高時就要進行較多的複製操作,效率將會降低,更關鍵的是,如果不想浪費50%的空間,就需要有額外的空間進行分配擔保,以應對被使用的內存中所有對象都100%存活的極端情況,所以在老年代一般不能直接選用這種算法。
標記整理算法的標記仍然和標記-清除一樣,但是後續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然後直接清理掉端邊界以外的內存,其執行過程如下:

分代收集算法

       當前商業虛擬機的垃圾收集器都是採用“分代收集”(Generational Collection)算法,這種算法是根據對象存活週期的不同將內存劃分爲幾塊。一般是把Java堆分爲新生代和老年代,這樣就可以根據各個年代的特點採用最適當的收集算法。在新生代中,每次垃圾收集時都會發現有大批對象死去,只有少量存貨,那就選用複製算法,只需要付出少量存貨對象的複製成本就可以完成收集。而老年代中因爲對象存活率高,沒有額外空間對他進行分配擔保,就必須使用標記-清理或者標記-整理算法進行回收了。

垃圾收集器

       如果說垃圾收集算法是內存回收的方法論,那麼垃圾收集器就是內存回收的具體實現。java虛擬機規範中隊垃圾收集器應該如何實現沒有任何規定,因此不同的廠商和不同的虛擬機版本提供的垃圾收集器都有可能會有很大差別。並且一般都會提供參數供用戶根據自己的應用特點和要求組合出各個年代所使用的收集器。HotSpot虛擬機的垃圾收集器如下圖所示:
    
       圖中展示了7中作用於不同分代的收集器,如果兩個收集器之間存在連線,就說明它們可以搭配使用。虛擬機所處的區域則表示它是屬於新生代收集器還是老年代收集器,首先我們得明確一個觀點,那就是垃圾收集器在工作時,所有的線程都會暫停工作。

Serial收集器

       Serial收集器是最基本、發展歷史最悠久的收集器,其名字已經揭示了它是單線程的收集器,但是它”單線程“的意義不僅僅說明它只會使用一個CPU或一條收集線程區完成垃圾收集工作,更重要的是在它運行垃圾收集時,必須暫停所有的工作線程,直到它收集結束。

ParNew收集器

        ParNew收集器其實就是Serial收集器的多線程版本,除了使用多條線程進行垃圾收集之外,其餘行爲和Serial收集器相比並沒有多大區別,同時,ParNew收集器在單CPU的環境中不會有比Serial有更好的效果。

Parallel Scavenge收集器

       Parallel Scavenge收集器是一個新生代收集器,它採用複製算法,又是並行的多線程收集器,他的特別之處在於它的關注點與其他收集器不同,CMS等收集器是儘可能的縮短垃圾收集時用戶線程的停頓時間,而Parallel Scavenge收集器的目地則是達到一個可控制的吞吐量。所謂的吞吐量就是CPU可用於運行用戶代碼的時間與CPU總小號時間的比值。比如如果虛擬機總共運行了100分鐘,其中垃圾收集花掉了1分鐘,那吞吐量就是99%。

Serial Old收集器

       Serial Old是Serial收集器的老年代版本,它同樣也是一個單線程收集器,使用標記-整理算法,這個收集器的主要意義也是在於給Client模式下的虛擬機使用。

Parallel Old收集器

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

CMS收集器(Concurrent  Mark Sweap)

       CMS收集器是一種以獲取最短回收停頓時間爲目的的收集器。從名字就可以看出來CMS收集器是基於標記-清除算法實現的,它的運作包括以下四個步驟:
(1)初始化標記(CMS initial mark)
  (2)併發標記(CMS concurrent mark)
  (3)重新標記(CMS remark)
  (4)併發清除(CMS concurrent sweep)
       其中,初始標記、重新標記着連個步驟仍然需要暫停。出事標記僅僅只是表一以下GC Roots能直接關聯到的對象,速度很快。

G1收集器

       G1收集器與其它收集器相比具有以下特點:
  (1)並行與併發:G1能夠充分利用多CPU、多核環境下的硬件優勢,使用多個CPU來縮短停頓時間。
  (2)分代收集:與其它收集器一樣,分代概念在G1中仍然得以保留。
  (3)空間整合:與CMS的標記-清理算法不同,G1從整體來看是基於標記-整理算法實現的收集器,從局部上來看是基於複製算法實現的,但是無論如何,這兩種算法都意味着G1運作期間不會產生內存空間碎片,手機後能提供規整的可用內存。
  (4)可預測的停頓:這是G1相對於CMS的另一大優勢。

內存分配與回收策略

       Java計數體系中所提倡的自動內存管理最終可以爲結尾自動化的解決了兩個問題:給對象分配內存以及回收分配給對象的內存。
       對象的分配從大的方向說就是在堆上分配,對象主要分配在新生代的Eden區上,如果啓動了本地線程分配緩存,將按照線程有限在TLAB上分配。先介紹兩種類型的回收:
  (1)新生代GC(Minor GC):指發生在新生代的垃圾收集動作,因爲Java對象大多具備朝生夕死的特性,所以Minor GC非常頻繁,一般回收速度也比較快。
  (2)老年代GC(Major GC):指發生在老年代的GC,出現Major GC,經常會伴隨至少一次的Minor GC(但不是絕對的)。Major GC的速度一般迴避Minor GC的速度慢10倍以上。

對象優先在Eden上分配

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

大對象直接進入老年代

      所謂的大對蝦就是指,需要大量連續內存空間的Java對象,最典型的大對蝦就是那種很長的字符串以及數組(byte[]數組就是典型的大對象)。大對象對虛擬機的內存分配來說是一個壞消息,但是比遇到一個大對蝦更加壞的消息是遇到一羣“朝生夕滅”的大對象,經常出現大對象容易導致內存還有不少空間時就提前出發垃圾收集以獲取足夠的連續空間來安置他們。

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

       既然虛擬機採用了分代收集的思想來管理內存,那麼內存回收時就必須能識別哪些對象應放在新生代,哪些對象應放在老年代中,爲了做到這點,虛擬機給每個對象定義了一個對象年齡計數器,如果對象在Eden出生並經歷過第一次Minor GC後仍然存活並且能被Survivor容納的話將會被移動到Survivor空間中,並且對象年齡設置爲1.對象在Survivor區中每熬過一次Minor GC,年齡就增加1歲,當它的年齡增加到一定程度(默認爲15)就將會被晉升到老年代中。

動態對象年齡判定

       爲了能更好的適應不同程序的內存狀況,虛擬機並不是永遠的要求對象的年齡必須達到了閾值要求的年齡才能晉升到老年代,如果在Survivor空間中相同年齡所有對象大小的綜合大於Survivor空間的一般,年齡大於或等於該年齡的對象就可以直接進入老年代。

總結

       本節主要介紹了對象存活的判定、垃圾回收的幾種算法以及幾種已經實現的垃圾收集器。並且對內存分配的策略也簡單的介紹了下。通過本章的點撥後,至少能夠知道垃圾是如何被回收的、何時被回收的以及回收的順序。
    
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章