垃圾收集 Garbage Collection 一般被稱爲「GC」,本文詳細講述Java垃圾回收機制。html
導讀:java
一、什麼是GC程序員
二、GC經常使用算法算法
三、垃圾收集器數組
四、finalize()方法詳解性能優化
五、總結--根據GC原理來優化代碼數據結構
正式閱讀以前須要瞭解相關概念:多線程
Java 堆內存分爲新生代和老年代,新生代中又分爲1個 Eden 區域 和 2個 Survivor 區域。架構
每一個程序員都遇到過內存溢出的狀況,程序運行時,內存空間是有限的,那麼如何及時的把再也不使用的對象清除將內存釋放出來,這就是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。
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、垃圾收集器
若是說收集算法是內存回收的方法論,垃圾收集器就是內存回收的具體實現
串行收集器是最古老,最穩定以及效率高的收集器
可能會產生較長的停頓,只使用一個線程去回收
-XX:+UseSerialGC
-XX:+UseParNewGC(new表明新生代,因此適用於新生代)
Serial收集器新生代的並行版本
在新生代回收時使用複製算法
多線程,須要多核支持
-XX:ParallelGCThreads 限制線程數量
相似ParNew
新生代複製算法
老年代標記-壓縮
更加關注吞吐量
-XX:+UseParallelGC
-XX:+UseParallelOldGC
2.3 其餘GC參數
-XX:MaxGCPauseMills
-XX:GCTimeRatio
這兩個參數是矛盾的。由於停頓時間和吞吐量不可能同時調優
CMS運行過程比較複雜,着重實現了標記的過程,可分爲
1. 初始標記(會產生全局停頓)
2. 併發標記(和用戶線程一塊兒)
3. 從新標記 (會產生全局停頓)
4. 併發清除(和用戶線程一塊兒)
這裏就能很明顯的看出,爲何CMS要使用標記清除而不是標記壓縮,若是使用標記壓縮,須要多對象的內存位置進行改變,這樣程序就很難繼續執行。可是標記清除會產生大量內存碎片,不利於內存分配。
CMS收集器特色:
儘量下降停頓
會影響系統總體吞吐量和性能
清理不完全
由於和用戶線程一塊兒運行,不能在空間快滿時再清理(由於也許在併發GC的期間,用戶線程又申請了大量內存,致使內存不夠)
一旦 concurrent mode failure產生,將使用串行收集器做爲後備。
CMS也提供了整理碎片的參數:
-XX:+ UseCMSCompactAtFullCollection Full GC後,進行一次整理
-XX:+CMSFullGCsBeforeCompaction
-XX:ParallelCMSThreads
CMS的提出是想改善GC的停頓時間,在GC過程當中的確作到了減小GC時間,可是一樣致使產生大量內存碎片,又須要消耗大量時間去整理碎片,從本質上並無改善時間。
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}。各狀態含義以下:
狀態變遷圖:
變遷說明:
(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方法
在此我向你們推薦一個架構學習交流羣。交流學習羣號: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程序的暫停時間。