Java GC機制詳解

垃圾收集 Garbage Collection 一般被稱爲「GC」,本文詳細講述Java垃圾回收機制。html

導讀:java

一、什麼是GC程序員

二、GC經常使用算法算法

三、垃圾收集器數組

四、finalize()方法詳解性能優化

五、總結--根據GC原理來優化代碼數據結構

正式閱讀以前須要瞭解相關概念:多線程

Java 堆內存分爲新生代和老年代,新生代中又分爲1個 Eden 區域 和 2個 Survivor 區域。架構

1、什麼是GC:

每一個程序員都遇到過內存溢出的狀況,程序運行時,內存空間是有限的,那麼如何及時的把再也不使用的對象清除將內存釋放出來,這就是GC要作的事。併發

理解GC機制就從: 「GC的區域在哪裏」 , 「GC的對象是什麼」 , 「GC的時機是什麼」 , 「GC作了哪些事」 幾方面來分析。

一、須要GC的內存區域

jvm 中,程序計數器、虛擬機棧、本地方法棧都是隨線程而生隨線程而滅,棧幀隨着方法的進入和退出作入棧和出棧操做,實現了自動的內存清理,所以,咱們的內存垃圾回收主要集中於 java 堆和方法區中,在程序運行期間,這部份內存的分配和使用都是動態的。

二、GC的對象

須要進行回收的對象就是已經沒有存活的對象,判斷一個對象是否存活經常使用的有兩種辦法:引用計數和可達分析。

(1)引用計數:每一個對象有一個引用計數屬性,新增一個引用時計數加1,引用釋放時計數減1,計數爲0時能夠回收。此方法簡單,沒法解決對象相互循環引用的問題。

(2)可達性分析(Reachability Analysis):從GC Roots開始向下搜索,搜索所走過的路徑稱爲引用鏈。當一個對象到GC Roots沒有任何引用鏈相連時,則證實此對象是不可用的。不可達對象。

在Java語言中,GC Roots包括:

虛擬機棧中引用的對象。

方法區中類靜態屬性實體引用的對象。

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

本地方法棧中JNI引用的對象。

三、何時觸發GC

(1)程序調用System.gc時能夠觸發

(2)系統自身來決定GC觸發的時機(根據Eden區和From Space區的內存大小來決定。當內存大小不足時,則會啓動GC線程並中止應用線程)

GC又分爲 minor GC 和 Full GC (也稱爲 Major GC )

Minor GC觸發條件:當Eden區滿時,觸發Minor GC。

Full GC觸發條件:

a.調用System.gc時,系統建議執行Full GC,可是沒必要然執行

b.老年代空間不足

c.方法去空間不足

d.經過Minor GC後進入老年代的平均大小大於老年代的可用內存

e.由Eden區、From Space區向To Space區複製時,對象大小大於To Space可用內存,則把該對象轉存到老年代,且老年代的可用內存小於該對象大小

四、GC作了什麼事

主要作了清理對象,整理內存的工做。Java堆分爲新生代和老年代,採用了不一樣的回收方式。(回收方式即回收算法詳見後文)

2、GC經常使用算法

GC經常使用算法有: 標記-清除算法 , 標記-壓縮算法 , 複製算法 , 分代收集算法。

目前主流的JVM(HotSpot)採用的是分代收集算法。

一、標記-清除算法

爲每一個對象存儲一個標記位,記錄對象的狀態(活着或是死亡)。分爲兩個階段,一個是標記階段,這個階段內,爲每一個對象更新標記位,檢查對象是否死亡;第二個階段是清除階段,該階段對死亡的對象進行清除,執行 GC 操做。

優勢

最大的優勢是,標記—清除算法中每一個活着的對象的引用只須要找到一個便可,找到一個就能夠判斷它爲活的。此外,更重要的是,這個算法並不移動對象的位置。

缺點

它的缺點就是效率比較低(遞歸與全堆對象遍歷)。每一個活着的對象都要在標記階段遍歷一遍;全部對象都要在清除階段掃描一遍,所以算法複雜度較高。沒有移動對象,致使可能出現不少碎片空間沒法利用的狀況。

圖例

二、標記-壓縮算法(標記-整理)

標記-壓縮法是標記-清除法的一個改進版。一樣,在標記階段,該算法也將全部對象標記爲存活和死亡兩種狀態;不一樣的是,在第二個階段,該算法並無直接對死亡的對象進行清理,而是將全部存活的對象整理一下,放到另外一處空間,而後把剩下的全部對象所有清除。這樣就達到了標記-整理的目的。

優勢

該算法不會像標記-清除算法那樣產生大量的碎片空間。

缺點

若是存活的對象過多,整理階段將會執行較多複製操做,致使算法效率下降。

圖例

左邊是標記階段,右邊是整理以後的狀態。能夠看到,該算法不會產生大量碎片內存空間。

三、複製算法

該算法將內存平均分紅兩部分,而後每次只使用其中的一部分,當這部份內存滿的時候,將內存中全部存活的對象複製到另外一個內存中,而後將以前的內存清空,只使用這部份內存,循環下去。

注意:

這個算法與標記-整理算法的區別在於,該算法不是在同一個區域複製,而是將全部存活的對象複製到另外一個區域內。

優勢

實現簡單;不產生內存碎片

缺點

每次運行,總有一半內存是空的,致使可以使用的內存空間只有原來的一半。

圖例

四、分代收集算法

如今的虛擬機垃圾收集大多采用這種方式,它根據對象的生存週期,將堆分爲 新生代(Young)和老年代(Tenure) 。在新生代中,因爲對象生存期短,每次回收都會有大量對象死去,那麼這時就採用複製算法。老年代裏的對象存活率較高,沒有額外的空間進行分配擔保,因此可使用標記-整理 或者 標記-清除。

具體過程: 新生代(Young)分爲 Eden區,From區與To區

當系統建立一個對象的時候,老是在Eden區操做,當這個區滿了,那麼就會觸發一次 YoungGC ,也就是 年輕代的垃圾回收 。通常來講這時候不是全部的對象都沒用了,因此就會把還能用的對象複製到From區。 

這樣整個Eden區就被清理乾淨了,能夠繼續建立新的對象,當Eden區再次被用完,就再觸發一次YoungGC,而後呢,注意,這個時候跟剛纔稍稍有點區別。此次觸發YoungGC後, 會將Eden區與From區還在被使用的對象複製到To區 , 

再下一次YoungGC的時候,則是將 Eden區與To區中的還在被使用的對象複製到From區 。

通過若干次YoungGC後,有些對象在From與To之間來回遊蕩,這時候From區與To區亮出了底線(閾值),這些傢伙要是到如今還沒掛掉,對不起,一塊兒滾到(複製)老年代吧。

老年代通過這麼幾回折騰,也就扛不住了(空間被用完),好,那就來次集體大掃除( Full GC),也就是全量回收。若是Full GC使用太頻繁的話,無疑會對系統性能產生很大的影響。因此要合理設置年輕代與老年代的大小,儘可能減小Full GC的操做。

3、垃圾收集器

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

1.Serial收集器

串行收集器是最古老,最穩定以及效率高的收集器

可能會產生較長的停頓,只使用一個線程去回收

-XX:+UseSerialGC

  • 新生代、老年代使用串行回收
  • 新生代複製算法
  • 老年代標記-壓縮

2. 並行收集器

2.1 ParNew

-XX:+UseParNewGC(new表明新生代,因此適用於新生代)

  • 新生代並行
  • 老年代串行

Serial收集器新生代的並行版本

在新生代回收時使用複製算法

多線程,須要多核支持

-XX:ParallelGCThreads 限制線程數量

2.2 Parallel收集器

相似ParNew 

新生代複製算法 

老年代標記-壓縮 

更加關注吞吐量 

-XX:+UseParallelGC  

  • 使用Parallel收集器+ 老年代串行

-XX:+UseParallelOldGC 

  • 使用Parallel收集器+ 老年代並行

2.3 其餘GC參數

-XX:MaxGCPauseMills

  • 最大停頓時間,單位毫秒
  • GC盡力保證回收時間不超過設定值

-XX:GCTimeRatio  

  • 0-100的取值範圍
  • 垃圾收集時間佔總時間的比
  • 默認99,即最大容許1%時間作GC

這兩個參數是矛盾的。由於停頓時間和吞吐量不可能同時調優

3. CMS收集器

  • Concurrent Mark Sweep 併發標記清除(應用程序線程和GC線程交替執行)
  • 使用標記-清除算法
  • 併發階段會下降吞吐量(停頓時間減小,吞吐量下降)
  • 老年代收集器(新生代使用ParNew)
  • -XX:+UseConcMarkSweepGC

CMS運行過程比較複雜,着重實現了標記的過程,可分爲

1. 初始標記(會產生全局停頓)

  • 根能夠直接關聯到的對象
  • 速度快

2. 併發標記(和用戶線程一塊兒)  

  • 主要標記過程,標記所有對象

3. 從新標記 (會產生全局停頓)  

  • 因爲併發標記時,用戶線程依然運行,所以在正式清理前,再作修正

4. 併發清除(和用戶線程一塊兒)  

  • 基於標記結果,直接清理對象

這裏就能很明顯的看出,爲何CMS要使用標記清除而不是標記壓縮,若是使用標記壓縮,須要多對象的內存位置進行改變,這樣程序就很難繼續執行。可是標記清除會產生大量內存碎片,不利於內存分配。 

CMS收集器特色:

儘量下降停頓

會影響系統總體吞吐量和性能

  • 好比,在用戶線程運行過程當中,分一半CPU去作GC,系統性能在GC階段,反應速度就降低一半

清理不完全  

  • 由於在清理階段,用戶線程還在運行,會產生新的垃圾,沒法清理

由於和用戶線程一塊兒運行,不能在空間快滿時再清理(由於也許在併發GC的期間,用戶線程又申請了大量內存,致使內存不夠)  

  • -XX:CMSInitiatingOccupancyFraction設置觸發GC的閾值
  • 若是不幸內存預留空間不夠,就會引發concurrent mode failure

一旦 concurrent mode failure產生,將使用串行收集器做爲後備。

CMS也提供了整理碎片的參數:

-XX:+ UseCMSCompactAtFullCollection Full GC後,進行一次整理

  • 整理過程是獨佔的,會引發停頓時間變長

-XX:+CMSFullGCsBeforeCompaction   

  • 設置進行幾回Full GC後,進行一次碎片整理

-XX:ParallelCMSThreads  

  • 設定CMS的線程數量(通常狀況約等於可用CPU數量)

CMS的提出是想改善GC的停頓時間,在GC過程當中的確作到了減小GC時間,可是一樣致使產生大量內存碎片,又須要消耗大量時間去整理碎片,從本質上並無改善時間。  

4. G1收集器

G1是目前技術發展的最前沿成果之一,HotSpot開發團隊賦予它的使命是將來能夠替換掉JDK1.5中發佈的CMS收集器。

與CMS收集器相比G1收集器有如下特色:

(1) 空間整合,G1收集器採用標記整理算法,不會產生內存空間碎片。分配大對象時不會由於沒法找到連續空間而提早觸發下一次GC。

(2)可預測停頓,這是G1的另外一大優點,下降停頓時間是G1和CMS的共同關注點,但G1除了追求低停頓外,還能創建可預測的停頓時間模型,能讓使用者明確指定在一個長度爲N毫秒的時間片斷內,消耗在垃圾收集上的時間不得超過N毫秒,這幾乎已是實時Java(RTSJ)的垃圾收集器的特徵了。

上面提到的垃圾收集器,收集的範圍都是整個新生代或者老年代,而G1再也不是這樣。使用G1收集器時,Java堆的內存佈局與其餘收集器有很大差異,它將整個Java堆劃分爲多個大小相等的獨立區域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代再也不是物理隔閡了,它們都是一部分(能夠不連續)Region的集合。

G1的新生代收集跟ParNew相似,當新生代佔用達到必定比例的時候,開始出發收集。

和CMS相似,G1收集器收集老年代對象會有短暫停頓。

步驟:

(1)標記階段,首先初始標記(Initial-Mark),這個階段是停頓的(Stop the World Event),而且會觸發一次普通Mintor GC。對應GC log:GC pause (young) (inital-mark)

(2)Root Region Scanning,程序運行過程當中會回收survivor區(存活到老年代),這一過程必須在young GC以前完成。

(3)Concurrent Marking,在整個堆中進行併發標記(和應用程序併發執行),此過程可能被young GC中斷。在併發標記階段,若發現區域對象中的全部對象都是垃圾,那個這個區域會被當即回收(圖中打X)。同時,併發標記過程當中,會計算每一個區域的對象活性(區域中存活對象的比例)。

(4)Remark, 再標記,會有短暫停頓(STW)。再標記階段是用來收集 併發標記階段 產生新的垃圾(併發階段和應用程序一同運行);G1中採用了比CMS更快的初始快照算法:snapshot-at-the-beginning (SATB)。

(5)Copy/Clean up,多線程清除失活對象,會有STW。G1將回收區域的存活對象拷貝到新區域,清除Remember Sets,併發清空回收區域並把它返回到空閒區域鏈表中。

(6)複製/清除過程後。回收區域的活性對象已經被集中回收到深藍色和深綠色區域。

4、finalize()方法詳解

1. finalize的做用

(1)finalize()是Object的protected方法,子類能夠覆蓋該方法以實現資源清理工做,GC在回收對象以前調用該方法。

(2)finalize()與C++中的析構函數不是對應的。C++中的析構函數調用的時機是肯定的(對象離開做用域或delete掉),但Java中的finalize的調用具備不肯定性

(3)不建議用finalize方法完成「非內存資源」的清理工做,但建議用於:① 清理本地對象(經過JNI建立的對象);② 做爲確保某些非內存資源(如Socket、文件等)釋放的一個補充:在finalize方法中顯式調用其餘資源釋放方法。其緣由可見下文[finalize的問題]

2. finalize的問題

(1)一些與finalize相關的方法,因爲一些致命的缺陷,已經被廢棄了,如System.runFinalizersOnExit()方法、Runtime.runFinalizersOnExit()方法

(2)System.gc()與System.runFinalization()方法增長了finalize方法執行的機會,但不可盲目依賴它們

(3)Java語言規範並不保證finalize方法會被及時地執行、並且根本不會保證它們會被執行

(4)finalize方法可能會帶來性能問題。由於JVM一般在單獨的低優先級線程中完成finalize的執行

(5)對象再生問題:finalize方法中,可將待回收對象賦值給GC Roots可達的對象引用,從而達到對象再生的目的

(6)finalize方法至多由GC執行一次(用戶固然能夠手動調用對象的finalize方法,但並不影響GC對finalize的行爲)

3. finalize的執行過程(生命週期)

(1) 首先,大體描述一下finalize流程:當對象變成(GC Roots)不可達時,GC會判斷該對象是否覆蓋了finalize方法,若未覆蓋,則直接將其回收。不然,若對象未執行過finalize方法,將其放入F-Queue隊列,由一低優先級線程執行該隊列中對象的finalize方法。執行finalize方法完畢後,GC會再次判斷該對象是否可達,若不可達,則進行回收,不然,對象「復活」。

(2) 具體的finalize流程:

對象可由兩種狀態,涉及到兩類狀態空間,一是終結狀態空間 F = {unfinalized, finalizable, finalized};二是可達狀態空間 R = {reachable, finalizer-reachable, unreachable}。各狀態含義以下:

  • unfinalized: 新建對象會先進入此狀態,GC並未準備執行其finalize方法,由於該對象是可達的
  • finalizable: 表示GC可對該對象執行finalize方法,GC已檢測到該對象不可達。正如前面所述,GC經過F-Queue隊列和一專用線程完成finalize的執行
  • finalized: 表示GC已經對該對象執行過finalize方法
  • reachable: 表示GC Roots引用可達
  • finalizer-reachable(f-reachable):表示不是reachable,但可經過某個finalizable對象可達
  • unreachable:對象不可經過上面兩種途徑可達

狀態變遷圖:

變遷說明:

(1)新建對象首先處於[reachable, unfinalized]狀態(A)

(2)隨着程序的運行,一些引用關係會消失,致使狀態變遷,從reachable狀態變遷到f-reachable(B, C, D)或unreachable(E, F)狀態

(3)若JVM檢測處處於unfinalized狀態的對象變成f-reachable或unreachable,JVM會將其標記爲finalizable狀態(G,H)。若對象原處於[unreachable, unfinalized]狀態,則同時將其標記爲f-reachable(H)。

(4)在某個時刻,JVM取出某個finalizable對象,將其標記爲finalized並在某個線程中執行其finalize方法。因爲是在活動線程中引用了該對象,該對象將變遷到(reachable, finalized)狀態(K或J)。該動做將影響某些其餘對象從f-reachable狀態從新回到reachable狀態(L, M, N)

(5)處於finalizable狀態的對象不能同時是unreahable的,由第4點可知,將對象finalizable對象標記爲finalized時會由某個線程執行該對象的finalize方法,導致其變成reachable。這也是圖中只有八個狀態點的緣由

(6)程序員手動調用finalize方法並不會影響到上述內部標記的變化,所以JVM只會至多調用finalize一次,即便該對象「復活」也是如此。程序員手動調用多少次不影響JVM的行爲

(7)若JVM檢測到finalized狀態的對象變成unreachable,回收其內存(I)

(8)若對象並未覆蓋finalize方法,JVM會進行優化,直接回收對象(O)

(9)注:System.runFinalizersOnExit()等方法可使對象即便處於reachable狀態,JVM仍對其執行finalize方法

5、總結

在此我向你們推薦一個架構學習交流羣。交流學習羣號:821169538  裏面會分享一些資深架構師錄製的視頻錄像:有Spring,MyBatis,Netty源碼分析,高併發、高性能、分佈式、微服務架構的原理,JVM性能優化、分佈式架構等這些成爲架構師必備的知識體系。還能領取免費的學習資源,目前受益良多。
根據GC的工做原理,咱們能夠經過一些技巧和方式,讓GC運行更加有效率,更加符合應用程序的要求。一些關於程序設計的幾點建議:

1.最基本的建議就是儘早釋放無用對象的引用。大多數程序員在使用臨時變量的時候,都是讓引用變量在退出活動域(scope)後,自動設置爲 null.咱們在使用這種方式時候,必須特別注意一些複雜的對象圖,例如數組,隊列,樹,圖等,這些對象之間有相互引用關係較爲複雜。對於這類對象,GC 回收它們通常效率較低。若是程序容許,儘早將不用的引用對象賦爲null.這樣能夠加速GC的工做。

2.儘可能少用finalize函數。finalize函數是Java提供給程序員一個釋放對象或資源的機會。可是,它會加大GC的工做量,所以儘可能少採用finalize方式回收資源。 

3.若是須要使用常用的圖片,可使用soft應用類型。它能夠儘量將圖片保存在內存中,供程序調用,而不引發OutOfMemory. 

4.注意集合數據類型,包括數組,樹,圖,鏈表等數據結構,這些數據結構對GC來講,回收更爲複雜。另外,注意一些全局的變量,以及一些靜態變量。這些變量每每容易引發懸掛對象(dangling reference),形成內存浪費。 

5.當程序有必定的等待時間,程序員能夠手動執行System.gc(),通知GC運行,可是Java語言規範並不保證GC必定會執行。使用增量式GC能夠縮短Java程序的暫停時間。

出處:http://www.cnblogs.com/jobbible/p/

相關文章
相關標籤/搜索