1. 程式人生 > >白話說java gc垃圾回收

白話說java gc垃圾回收

管理 無法 靜態 掃描 列表 充足 計劃 tracing 最短

  gc是java區別於其他好幾門語言(c/c++)的一個代表功能(當然也有很多可以自動管理內存的語言,如所有的腳本語言,你根本不知道內存管理這回事)!

  當然,之所以要把c/c++和java相比,是因為java出現的初衷即是對標c++的缺點的。不管怎麽樣,gc讓程序員gg們不用痛苦地管理內存,這是好事!

  回歸正題,gc是什麽?小白:Garbage Collect 垃圾回收(內存),是一種自動管理內存的一種機制!

  下面,我們分幾個問題來討論gc的實現及原理!

一條主線(如果是你會怎麽做?):

  1. 什麽內存可以回收?(回收對象判定)
  2. 什麽時候回收?(回收時機)
  3. 怎麽回收?(回收算法)

基本上,我們主要來回答完這幾個問題,gc的事情基本就定了!

我們也可以先用簡單的三句話來回答上面的問題:

  1. 沒有用的內存就可以回收了;
  2. 在保證回收準確的前提下,隨時可以回收;
  3. 用高效算法進行回收,保證最小影響業務代碼運行;

所以,其實大體思路還是簡單的,但是具體做下來就不那麽簡單了,gc功能經過幾十年的發展依然還在完善中,是最好的證明!

下面我們來細細解答這幾個思路!

1. 什麽樣的內存可以回收?什麽樣的內存是沒有用的?可以回收的內存,一般來說肯定是沒用的內存(有用內存將其刪除是高危動作)!

  所以,判定什麽樣的內存是無用內存,就是問題的關鍵!通常的簡單的,使用引用計數器法推斷:給對象添加引用計數器,當一個地方引用時,將計數加1,當引用失效時,將計數器減1;計數器為0,則表示對象不會再被使用了,即是無用內存。這樣單說,其實是沒有問題的,因為就是有的語言就是這麽幹的,如AS3.0, python等等!但是,java卻是沒有采用這種判斷方法,判定對象是否無用的。因為這種算法對java而言,存在一個循環引用問題,解決不了。java中是使用可達性分析算法來判定一個對象是否有用的。算法過程為:從 gc roots 作為起始點,所超走過的路徑為引用鏈,當一個對象到gc roots不可達時,則證明對象不可達,即對象無引用,可回收。所以,我們只要找出家些不可達gc roots的對象,將其回收即可。

  可達性分析剩下兩個問題:

    1. gc roots 在哪裏?
    2. 分析的起點是 gc roots嗎?還是其他對象?
    3. 需要掃描所有路徑嗎?數量怎麽樣?效率怎麽樣?

  java中規定以下幾種對象可作為gc roots:

    1. 虛擬機棧中引用的對象(棧幀中的本地變量表中引用的對象);
    2. 方法區中靜態屬性引用的對象;
    3. 方法區中常量引用的對象;
    4. 本地方法棧中jni引用的對象;

  即以以上幾種gc roots作為根開始掃描,沒有引用的對象可以清除;

  為全路徑掃描,找不到對象為需要刪除的對象;(請查看c++源碼掃描解釋)

2. 什麽時候回收?任何安全準確的時間點進行回收?

  在確定了哪些對象可以清除後,找個時間點就可以清除了。其實,在可達性分析後不可達的對象,也可以繼續存在:

    1. 對象可以finalize()方法中拯救自己一次!(逃逸)
    2. 當然,gc不是實時運行的,它的觸發時機為:當新生代空間不夠將觸發一次minor gc,此時幸存下來的對象的年齡則加1;當老年代空間也不夠放對象時,將觸發一次full gc,一般fg都伴隨著一次minor gc。
    3. 執行內存清理時,需要暫停所有線程,否則會存在一致性問題。暫停所有的線程方式有兩種: 1. 搶占式中斷,2. 主動式中斷;對於睡眠線程,則將其設置為安全區域。在此安全占或安全區域(safepoint)內才可以進行回收!

3. 怎樣回收?

  怎樣高效回收內存!都有些什麽算法?

  1. 標記清除算法;優點是簡單;缺點:1. 兩個算法效率都不高;2. 回收後會產生內存碎片;
  2. 改進1,復制算法;實現方法務,將內存分為兩塊,將其中一塊用於存儲,當其中一塊好的復制到另一塊上後,直接清除原來的內存;優點:實現簡單,運行高效;缺點是需要使用一半的內存來做備用,浪費空間了。這裏還涉及到擔保問題。
  3. 改進2,標記整理算法;其實現方式為,找出可清除的區塊,讓其沿頭移動,從而得到歸整的內存區域;
  4. 分代收集算法,這裏是組合多個基礎算法的優點而來的算法,也是當下的調用虛擬機的算法。比如年輕代使用復制算法,老年代使用標記整理算法,物盡其用!

  把三個問題解答完後,我們把gc外圍的東西搞定了,現在讓我們看看具體的收集器吧。

畢竟,原理只是原理,只有具體的收集器對我們才更實用呢!

4. 都有些什麽垃圾收集器呢?

  Serial 是歷史悠久的串行收集器;
  Serial old 是serial的老年代收集器,采用標記整理算法收集;
  ParNew 是serial的多線程版本收集器;
  Parallel Scavenge 是專註於吞吐量的並行收集器;
  Parallel old 是Parallel Scavenge的老年代版本,使用多線程和標記整理算法進行收集;
  CMS Concurrent Mark Sweep, 是一款以獲取最短停頓時間為目標的收集器;
  G1 Garbage First, 是一款最新的性能最好的垃圾收集器;

  如上面幾種垃圾收集器,一般都是以組合的形式進行工作的,而不是單個收集器做完所有事情。(當然越往後就越融合為一個收集器做完了)總之,其目標都是一致的,即以不同的方式收集不同類型的內存, 從而達到最佳收集效果!

  其中,serial, serial old, parnew, ps, ps old 基本上就如同前面的一句話描述,雖然其實現可能很復雜,但是呈現出來的還是比較簡單的。

我們主要看下 CMS 和 G1 兩個收集器!

CMS, 是第一款真正的並發收集器。

G1, 籌備了10年才推出第一個正式版本,可見其難度一斑!

cms收集器是一款追求獲得盡量短的停頓時間為目標的收集器,它是基於標記清除算法操作的,它的運作主要分為4個步驟:

    1. 初始標記;(標記gc roots能直接關聯到的對象)
    2. 並發標記;(對gc roots進行tracing,耗時長)
    3. 重新標記;(修正並發標記期間因用戶程序運作而改變的對象的標記)
    4. 並發清除;(清除標記好的對象空間,耗時長)
  這些步驟對於前面幾種收集器來說,往往就兩個步驟,它是復雜化了的。

  它的整體動作過程圖示如下:

技術分享圖片

  可以看出,初始標記過程是單線程的,而後續幾個動作都是多線程的。其中並發標記和並發清除是和都是可以和用戶線程一起工作的,而且這兩個過程又是比較耗時的,因此雖然gc一直在工作,但是並沒有導致用戶長時間的停頓。

  有個疑問:並發標記的tracing是什麽意思?

  cms雖然看起來很好,但是它也有它的缺點,主要體現在:

    1. 因為是與用戶線程並發,雖不會導致用戶線程停頓,但是會搶占cpu資源。所以在cpu資源緊缺的場景則肯定不適合cms了;
    2. cms收集器無法處理浮動垃圾,可能會因此導致另一次full gc。因為cms在清理期間用戶線程一直在產生垃圾,所以肯定會留下些cms沒有收集到的內存,這必須等到下一次gc時才可能回收;而且,由於cms是與用戶線程一起工作的,所以,在做清理的同時必須要預留下空間給用戶線程使用,所以會收集得更頻繁些,比如超過68%的占用時就觸發gc;如果在收集期間用戶線程的內存不夠用了,就會出現“Concurrent Mode Failure”,虛擬機會啟用後備預案來進行gc以獲得足夠空間(serial old),從而導致停頓時間很長問題出現;
    3. 並發清除算法會導致內存碎片產生,這在遇到大對象分配時,將無法滿足從而會提前觸發(可能總體空間還很充足)full gc;當然cms有個開關來解決這問題,-XX:+UseCMSCompactAtFullCollection, 它會在要進行full gc時開戶碎片整理過程,當然它的代價是導致停頓時間變長;

  綜上,我們可以看出cms是個好的收集器,但是它也有自己的短板,如果不顧使用場合地隨便應用cms,則可能帶來相反的效果;

最後,我們再來看看G1收集器;

  G1收集器是個最新的收集器,其研發n的周期也預示了它的難度;粗略地說它是從jdk1.7(7u4)開始面向用戶的。

  它有如下優勢:

    1. 並行與並發;與用戶線程共存;
    2. 分代收集;
    3. 空間整合;使用 標記整理算法和復制算法,避免了空間碎片問題;
    4. 可預測的停頓;用戶可以指定時間,g1會使停頓時間小於設定值;

  G1的堆內存總局與其他收集器不同,它是將整個堆分為n個大小相等的region的布局!在回收垃圾時,g1會跟蹤各個region裏的價值大小,在後臺維護一個優先級列表,每次根據允許的收集時間,優先回收價值最大的region。

  g1運作大致分為以下幾個步驟:

    1. 初始標記;(僅標記gc roots能關聯到的對象)
    2. 並發標記;(可達性分析)
    3. 最終標記;(修正並發標記期間的變化,變化被記錄在log中)
    4. 篩選回收;(將region回收價值排序,根據用戶期望進行選擇回收計劃)

其運行過程與cms大致相似(補圖中)

G1收集器在jdk1.7中正式亮像,在jdk1.8中做了很多的完善,相信會是越來越多同學的選擇的!

如果你想調優gc配置,請另查資料!

參考: 《深入理解java虛擬機》

白話說java gc垃圾回收