白話說java gc垃圾回收
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垃圾回收