由淺入深瞭解GC原理

GCGarbage Collection)很大程度上幫助Java程序員解決了內存釋放的問題,有了GC,就不須要再手動的去控制內存的釋放。程序員

在閱讀以前須要瞭解的相關概念:算法

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

1、什麼是GC(Garbage Collection)

GC垃圾收集,Java提供的GC能夠自動監測對象是否超過做用域從而達到自動回收內存的目的。segmentfault

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

須要GC的內存區域數據結構

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

注意:
對於 Java8HotSpots 取消了永久代,那麼是否是也就沒有方法區了呢?固然不是,方法區是一個規範,規範沒變,它就一直在。那麼取代永久代的就是元空間。它可永久代有什麼不一樣的?存儲位置不一樣,永久代物理是是堆的一部分,和新生代,老年代地址是連續的,而元空間屬於本地內存;存儲內容不一樣,元空間存儲類的元信息,靜態變量和常量池等併入堆中。至關於永久代的數據被分到了堆和元空間中。

GC的對象併發

當一個對象到GC Roots不可達時,在下一個垃圾回收週期中嘗試回收該對象,若是對象重寫了finalize(),並在這個方法中成功自救(將自身賦予某個引用),那麼這個對象不會被回收。但若是這個對象沒有重寫finalize()方法或已執行過這個方法,該對象將會被回收。函數

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

  • 引用計數算法:

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

  • 可達性分析算法(Reachability Analysis):

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

在Java語言中,GC Roots包括:

  • 虛擬機棧中引用的對象;
  • 方法區中類靜態屬性實體引用的對象;
  • 方法區中常量引用的對象;
  • 本地方法棧中JNI引用的對象。

何時觸發GC

  • 程序調用System.gc時,但不是必然執行
  • 系統自身來決定GC觸發的時機(根據Eden區和From Space區的內存大小來決定。當內存大小不足時,則會啓動GC線程並中止應用線程)

GC又分爲 Minor GCFull GC (也稱爲 Major GC)
Minor GC觸發條件:當Eden區滿時,觸發Minor GC
Full GC觸發條件:

  • 調用System.gc時,系統建議執行Full GC,可是沒必要然執行
  • 老年代空間不足
  • 方法去空間不足
  • 經過Minor GC後進入老年代的平均大小大於老年代的可用內存
  • Eden區、From Space區向To Space區複製時,對象大小大於To Space可用內存,則把該對象轉存到老年代,且老年代的可用內存小於該對象大小

GC作了什麼事

主要作了清理對象,整理內存的工做。Java堆分爲新生代和老年代,採用了不一樣的回收方式。

GC經常使用算法

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

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

標記-清除算法(Mark-Sweep)

首先標記出全部須要回收的對象,標記完成後回收全部被標記的對象。不足主要體如今效率和空間,從效率的角度講,標記和清除效率都不高;從空間的角度講,標記清除後會產生大量不連續的內存碎片, 內存碎片太多可能會致使須要分配較大對象時,沒法找到足夠的連續內存而提早觸發一次垃圾收集動做。

從堆棧和靜態存儲區出發,遍歷全部的引用,進而找出全部存活的對象,若是活着,就標記。只有所有標記完畢的時候,清理動做纔開始。在清理的時候,沒有標記的對象將會被釋放,不會發生任何動做。可是剩下的堆空間是不連續的,垃圾回收器要是但願獲得連續空間的話,就得從新整理剩下的對象。

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

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

標記-壓縮算法(標記-整理)(Mark-Compact)

過程與標記-清除算法同樣,不過不是直接對可回收對象進行清理,而是讓全部存活對象都向一端移動,而後直接清理掉邊界之外的內存。在標記階段,該算法也將全部對象標記爲存活和死亡兩種狀態;不一樣的是,在第二個階段,該算法並無直接對死亡的對象進行清理,而是將全部存活的對象整理一下,放到另外一處空間,而後把剩下的全部對象所有清除。這樣就達到了標記-整理的目的。

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

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

複製(Copying)算法

將可用內存分爲兩塊,每次只用其中一塊,當一塊內存用完了,就將還存活的對象複製到另一塊上,而後再把已經使用過的內存空間一次性清理掉,循環下去。這樣每次只需對整個半區進行內存回收,內存分配時也不須要考慮內存碎片等複雜狀況,只須要移動指針,按照順序分配便可。

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

缺點:內存縮小爲原來的一半,代價過高

如今商用虛擬機都採用這種算法來回收新生代,不過 1:1的比例很是不科學,所以新生代的內存被劃分爲一塊較大的 Eden空間和兩塊較小的 Survivor空間,每次使用 Eden和其中一塊 Survivor。每次回收時,將 EdenSurvivor中還存活着的對象一次性複製到另一塊 Survivor空間上,最後清理掉 Eden和剛纔用過的 Survivor空間。 HotSpot虛擬機默認 Eden區和 Survivor區的比例爲 8:1,意思是每次新生代中可用內存空間爲整個新生代容量的 90%。固然,咱們沒法保證每次回收都少於 10%的對象存活,當 Survivor空間不夠用時,須要依賴老年代進行分配擔保( Handle Promotion)。
image.png

分代收集(Generational Collection)算法

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

新生代(Young)分爲Eden區,From區與To區:
image.png

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

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

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

通過若干次YoungGC後,有些對象在FromTo之間來回遊蕩,這時候From區與To區亮出了底線(閾值),這些傢伙要是尚未被回收,就會被複制到老年代:
image.png

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

垃圾收集器

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

Serial收集器

串行收集器是最古老,最穩定以及效率高的收集器,可是可能會產生較長的停頓,只使用一個線程去回收。
啓用命令:-XX:+UseSerialGC

Parallel收集器

並行GC的好處是提高垃圾回收的性能,減小串行回收帶來的問題,也有停頓,但能夠並行回收,一邊標記對象一邊執行線程,總體上提高了回收的性能。
啓用命令:
-XX:+UseParallelGC

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

-XX:+UseParallelOldGC

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

image.png

CMS收集器

CMS收集器是以獲取最短回收停頓時間爲目標的收集器,基於」標記-清除」(Mark-Sweep)算法實現,整個過程分爲四個步驟:

  • 初始標記 (Stop the World事件CPU停頓很短) ,僅標記GC Roots能直接關聯到的對象,速度快;
  • 併發標記 (收集垃圾跟用戶線程一塊兒執行) ,初始標記和從新標記仍須要 Stop the World,併發標記過程就是進行 GC Roots Tracing的過程;
  • 從新標記 (Stop the World事件CPU停頓,比初始標記稍長,遠比並發標記短),修正併發標記期因用戶程序繼續運做而致使標記產生變更的那部分對象的標記記錄,這個階段停頓時間比初始標記階段稍長些,比並發標記時間短;
  • 併發清理-清除算法。

整個過程當中最耗時的併發標記和併發清除過程,收集器線程均可與用戶線程一塊兒工做,整體上來講,CMS收集器的內存回收過程是與用戶線程一塊兒併發執行的。

CMS收集器優勢:併發收集,低停頓

CMS收集器缺點:

  • CMS收集器對CPU資源很是敏感
  • CMS處理器沒法處理浮動垃圾
  • CMS基於「標記--清除」算法實現,會產生大量空間碎片,會在大對象分配時提早觸發Full GC。爲解決這個問題,CMS提供了一個開關參數,用於在CMS要進行Full GC時開啓內存碎片的合併整理過程,內存整理的過程沒法併發,停頓時間變長;

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

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

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

-XX:+CMSFullGCsBeforeCompaction

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

-XX:ParallelCMSThreads

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

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

G1(Garbage First)收集器

G1是一款面向服務端應用的垃圾收集器。與CMS收集器相比G1收集器有如下特色:

  • 空間整合:G1收集器採用標記整理算法,不會產生內存空間碎片。分配大對象時不會由於沒法找到連續空間而提早觸發下一次GC
  • 可預測停頓:這是G1的另外一大優點,下降停頓時間是G1CMS的共同關注點,但G1除了追求低停頓外,還能創建可預測的停頓時間模型,能讓使用者明確指定在一個長度爲N毫秒的時間片斷內,消耗在垃圾收集上的時間不得超過N毫秒,這幾乎已是實時JavaRTSJ)的垃圾收集器的特徵了。
  • 並行於併發:充分使用多個CPU來縮短Stop the World停頓時間。
  • 分代收集:採用不一樣方式處理新建立的對象和已存活一段時間,熬過屢次GC的舊對象,以獲取更好的收集效果。

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

G1運做步驟:

  • 初始標記(Initial-Mark)(Stop the World事件CPU停頓只處理垃圾);

這個階段是停頓的(Stop the World Event),而且會觸發一次普通Mintor GC
對應GC log:GC pause (young) (inital-mark)

  • Root Region Scanning;

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

  • 併發標記(Concurrent Marking)(與用戶線程併發執行);

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

  • 最終標記(Stop the World事件CPU停頓處理垃圾);

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

  • 篩選回收(Stop the World事件根據用戶指望的GC停頓時間回收);

多線程清除失活對象,會有Stop the World事件。G1將回收區域的存活對象拷貝到新區域,清除Remember Sets,併發清空回收區域並把它返回到空閒區域鏈表中。

finalize()方法

finalize的做用

  • finalize()Objectprotected方法,子類能夠覆蓋該方法以實現資源清理工做,GC在回收對象以前調用該方法;
  • finalize()C++中的析構函數不是對應的。C++中的析構函數調用的時機是肯定的(對象離開做用域或delete掉),但Java中的finalize的調用具備不肯定性;
  • 不建議用finalize方法完成「非內存資源」的清理工做,但建議用於:

① 清理本地對象(經過JNI建立的對象);
② 做爲確保某些非內存資源(如Socket、文件等)釋放的一個補充:在finalize方法中顯式調用其餘資源釋放方法。

finalize的問題

  • 一些與finalize相關的方法,因爲一些致命的缺陷,已經被廢棄了,如System.runFinalizersOnExit()方法、Runtime.runFinalizersOnExit()方法;
  • System.gc()System.runFinalization()方法增長了finalize方法執行的機會,但不可盲目依賴它們;
  • Java語言規範並不保證finalize方法會被及時地執行、並且根本不會保證它們會被執行;
  • finalize方法可能會帶來性能問題。由於JVM一般在單獨的低優先級線程中完成finalize的執行;
  • 對象再生問題:finalize方法中,可將待回收對象賦值給GC Roots可達的對象引用,從而達到對象再生的目的;
  • finalize方法至多由GC執行一次(用戶固然能夠手動調用對象的finalize方法,但並不影響GCfinalize的行爲)。

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

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

具體的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:對象不可經過上面兩種途徑可達。

狀態變遷圖:
image.png

狀態變遷說明:

  • 新建對象首先處於[reachable, unfinalized]狀態(A);
  • 隨着程序的運行,一些引用關係會消失,致使狀態變遷,從reachable狀態變遷到f-reachable(B, C, D)或unreachable(E, F)狀態;
  • JVM檢測處處於unfinalized狀態的對象變成f-reachableunreachableJVM會將其標記爲finalizable狀態(G,H)。若對象原處於[unreachable, unfinalized]狀態,則同時將其標記爲f-reachable(H);
  • 在某個時刻,JVM取出某個finalizable對象,將其標記爲finalized並在某個線程中執行其finalize方法。因爲是在活動線程中引用了該對象,該對象將變遷到(reachable, finalized)狀態(KJ)。該動做將影響某些其餘對象從f-reachable狀態從新回到reachable狀態(L, M, N);
  • 處於finalizable狀態的對象不能同時是unreahable的,由上一點可知,將對象finalizable對象標記爲finalized時會由某個線程執行該對象的finalize方法,導致其變成reachable。這也是圖中只有八個狀態點的緣由;
  • 程序員手動調用finalize方法並不會影響到上述內部標記的變化,所以JVM只會至多調用finalize一次,即便該對象「復活」也是如此。程序員手動調用多少次不影響JVM的行爲;
  • JVM檢測到finalized狀態的對象變成unreachable,回收其內存(I);
  • 若對象並未覆蓋finalize方法,JVM會進行優化,直接回收對象(O)。

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

總結

GC垃圾收集,Java提供的GC能夠自動監測對象是否超過做用域從而達到自動回收內存的目的。

判斷一個對象是否存活經常使用的有兩種辦法:引用計數算法和可達性分析算法。

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

無論選擇哪一種GC算法,Stop the World都是不可避免的。Stop the World意味着從應用中停下來並進入到GC執行過程當中去。一旦Stop the World發生,除了GC所需的線程外,其餘線程都將中止工做,中斷了的線程直到GC任務結束才繼續它們的任務。GC調優一般就是爲了改善Stop the World的時間。

關於程序設計的幾點建議:

  • 儘早釋放無用對象的引用。大多數程序員在使用臨時變量的時候,都是讓引用變量在退出活動域(scope)後,自動設置爲 null.咱們在使用這種方式時候,必須特別注意一些複雜的對象圖,例如數組,隊列,樹,圖等,這些對象之間有相互引用關係較爲複雜。對於這類對象,GC 回收它們通常效率較低。若是程序容許,儘早將不用的引用對象賦爲 null,這樣能夠加速GC的工做。
  • 儘可能少用finalize函數。finalize函數是Java提供給程序員一個釋放對象或資源的機會。可是,它會加大GC的工做量,所以儘可能少採用finalize方式回收資源。
  • 若是須要使用常用的圖片,可使用soft應用類型。它能夠儘量將圖片保存在內存中,供程序調用,而不引發OutOfMemoryException
  • 注意集合數據類型,包括數組,樹,圖,鏈表等數據結構,這些數據結構對GC來講,回收更爲複雜。另外,注意一些全局的變量,以及一些靜態變量。這些變量每每容易引發懸掛對象(dangling reference),形成內存浪費。
  • 當程序有必定的等待時間,程序員能夠手動執行System.gc(),通知GC運行,可是Java語言規範並不保證GC必定會執行。使用增量式GC能夠縮短Java程序的暫停時間。
本文由博客一文多發平臺 OpenWrite 發佈!
更多內容請點擊個人博客 沐晨
相關文章
相關標籤/搜索