上一次整理了一下深刻理解jvm虛擬機內存,本章來整理一下gc垃圾收集。html
虛擬機內存分爲線程隔離內存部分和線程共享內存部分,其中線程隔離部分包括程序計數器,虛擬機棧和本地方法棧;線程共享部分分爲堆和方法區。線程隔離內存中的數據隨線程而生,隨線程而滅,不須要gc來管理,但堆和方法區內存的分配和回收是動態的,這就須要用到gc來及回收機制來管理。java
垃圾回收須要知道三個部分,如何判斷對象死亡? 何時進行回收? 如何回收? 接下來就針對這三個問題進行分析。算法
一開始大多數都是用引用計數算法,當增長一個對這個對象的引用時,引用計數器就+1;不然引用計數器-1,知道引用計數器的值爲0的時候纔會斷定該對象能夠被回收,每個對象都有一個對應的引用計數器。可是引用計數算法中對象之間有可能會出現互相引用的狀況,如對象A和對象B:A a = new A(); B b = new B(); a.instance = b; b.instance = a; 這樣就造成了互相引用而致使計數器永遠都不可能爲0,從而不會對該對象進行回收。所以這種斷定對象死亡的算法逐漸被拋棄。數組
後面提出了一種可達性分析算法,它是以GC roots 對象做爲起始對象,經過起始對象爲節點尋找對象的引用,想下進行搜索,走過的節點路徑稱之爲引用鏈。若是對象沒有直接或間接的關聯到GC Roots(即對象引用鏈不可達GC roots時),則稱此對象可被回收了。下圖展現GCRoots引用鏈示意圖:多線程
(圖片來源於:http://www.importnew.com/23035.html)併發
那哪些對象能夠做爲GCRoots呢? 下面來列舉一下:jvm
1.虛擬機棧(棧幀的本地變量表)中引用的對象this
2.方法區中類靜態屬性引用的對象線程
3.方法區中常量引用的對象指針
4.本地方法棧中JNI(通常說的是Native方法)中引用的對象
對象在通過可達性分析算法確認不可達以後,並非立刻就會進行回收的。首先會對不可達對象進行標記,而後會進一步進行篩選來確認對象是否有必要執行finalize()方法,若是對象以前執行過一次finalize或對象沒有覆蓋finalize方法,虛擬機都會認爲finalize方法不必執行。 當虛擬機認爲有必要執行finalize方法的時候,會將該對象放入F-Queue隊列中。虛擬機會自動建立一個低優先級的Finalizer線程執行這個隊列。finalize方法是對象自我挽救的最後一根救命稻草,若是對象想要自我挽救,能夠再覆蓋的finalize方法中將對象從新根據引用鏈關聯上GC Roots便可(如將對象賦值給this類或類成員變量)。這樣進行二次標記的時候若是發現對象是可達的,就會將其踢出F-Queue隊列;若是二次標記仍是不可達的話,那就真的要被回收了。
以上講的都是對堆中的對象進行回收狀況,那麼方法區呢? 方法區通常稱之它爲永久代,它回收的數據是廢棄常量以及無用的類。String存儲數據通常都存儲到常量池中,若是常量池中數據沒有對應的引用的話就會將其進行置爲廢棄常量從而進行回收。而無用的類回收條件就比回收廢棄常量苛刻多了,回收無用類的條件有三個:
1.類沒有任何對應的實例對象,即全部對應的實例對象都已被回收
2.類對應的classLoader被卸載
3.該類對應的java.lang.class實例沒有任何地方被引用,沒法在任何地方經過反射訪問該類方法
聊完了哪些對象須要被回收後,接下來講說回收都有哪些算法。
1.標記-清理算法(mark-sweep)
標記-清理算法是先將對象進行標記,標記就是不須要回收的對象,標記完後回收清理全部沒有被標記的對象。但這種算法的缺點也比較明顯,首先標記和清理過程效率較低;其次這種算法會產生大量的內存碎片,這樣當爲大對象分配內存時,對致使沒有連續的存儲空間而提早觸發一次垃圾回收。
(圖片來源於:http://www.importnew.com/23035.html)
2.複製算法
複製算法是將堆內存按照容量平分爲2份,其中一份用於存儲對象,另外一份空置。當觸發垃圾回收時,會先回收對象,而後將剩餘存活的對象複製到另外一塊空置對象中。雖然這種方法簡單高效,可是內存利用率減半,並且成活對象過多時,複製的對象會較多,因此這種算法通常用於新生代。下圖是複製算法模型圖:
(圖片來源於:http://www.importnew.com/16173.html)
3.標記-整理算法(mark-compact)
標記整理算法和標記清除算法相似,只不過多了一步整理。它是先將對象用可達性分析算法進行標記,而後統一將未被標記的對象進行清除,對於產生的內存碎片虛擬機會經過改變對象與對象之間的指針進行整理,以免爲大對象分配內存是沒有足夠大的連續內存空間。
(圖片來源於:http://www.importnew.com/23035.html)
講完了一些基本的內存回收算法,那如何利用這些算法來回收垃圾呢? 通常對內存會按照對象的存活時間來進行分代,通常分爲新生代,老年代,永久代(它通常是方法區,有些虛擬機會將方法區也看做是堆的一部分), 接下來介紹一些新生代和老年代垃圾收集器:
1.serial收集器
它是一種新生代收集器,採用上面所述的複製算法。它是一種單線程收集器,只有有一條線程去執行GC回收,並且gc線程執行時,其餘用戶線程所有中止(俗稱stop the world),執行完以後纔會釋放用戶線程。可是這有一個缺點,就是用戶線程在用戶不知情的狀況下被迫停止,這很是的不友好若是GC線程致使的停頓時間較長則會嚴重影響用戶體驗。全部就有了parNew收集器。
(圖片來源於:-氣宗】深刻理解Java虛擬機:JVM高級特性與最佳實踐(最新第二版).pdf)
2.parNew收集器
它也是一種新生代收集器,採用的也是複製算法,但它是一種多線程收集器,也就是說當執行GC回收的時候也能夠同時執行用戶線程,它是第一款實現併發的收集器。但若是是單核CPU的狀況下,它的效果是不如serial收集器的。但用戶線程併發執行的時候也會產生一些浮動垃圾。
(圖片來源於:-氣宗】深刻理解Java虛擬機:JVM高級特性與最佳實踐(最新第二版).pdf)
3.parallel scavenge收集器
它也是新生代收集器,採用的依舊是複製算法,是一種多線程收集器支持併發。但它主要的不一樣是他關注的是吞吐量,而前面兩種關注的是GC回收停頓時間,吞吐量的定義是 用戶線程執行時間/(用戶線程執行時間+GC回收執行時間),GC回收停頓時間越短並不表明吞吐量越高,這二者仍是有區別的,好比原本要回收500M內存,經過設置回收300M,雖然一次停頓時間縮短了,但這樣回收頻率天然也會增長。可是吞吐量仍是仍是沒有改變。停頓時間越短表明響應用戶速度越快;而高吞吐量則是高效的利用CPU時間
4.serial old收集器
serial收集器是一種老年代收集器,採用的標記-整理算法,採用的是單線程收集。它和serial收集器相似,只不過它是針對老年代。
(圖片來源於:-氣宗】深刻理解Java虛擬機:JVM高級特性與最佳實踐(最新第二版).pdf)
5.parallel old收集器
parallel scavenge收集器也是一種老年代收集器,採用的是標記-整理算法,採用多線程併發收集。它和parallel scavenge收集器對應,都是吞吐量優先。
(圖片來源於:-氣宗】深刻理解Java虛擬機:JVM高級特性與最佳實踐(最新第二版).pdf)
6.CMS(conccurent mark sweep)收集器
顧名思義,它是併發標記清理收集器,是一種老年代收集器,採用的是標記-整理算法,它是以獲取最短停頓時間爲目標的收集器。他會經歷以下幾個階段:
初始標記:此時會對可達性分析中與GC Roots直接關聯的對象進行標記
併發標記:此時會對GC Roots Tracing跟蹤引用鏈下全部的對象進行標記,此線程會與用戶線程併發執行,這個階段停頓時間會比較長
從新標記:此時會對併發標記過程當中用戶線程對象引用鏈產生的改變進行從新標記
併發清理:此時會對未被標記的對象進行清除,這個線程是與用戶線程併發執行的
CMS雖然有併發收集,低停頓的優勢,但缺點也比較明顯。首先CMS對 CPU資源比較敏感。因爲它在併發階段佔用必定CPU資源,因此會致使應用程序變慢,總吞吐量會下降。其次,在併發清理階段會產生一些浮動垃圾,這是因爲用戶線程在這個階段也會產生一些須要回收的對象,但本次收集不能回收,只能等待下次垃圾回收。最後,CMS採用的是標記-清除算法,這種算法在前面就說明了它的標記和清除效率是比較低的,並且會產生大量的內存碎片,從而致使分配大對象(通常是很長的字符串或長度較大的數組)內存時,沒有足夠的連續內存空間。
(圖片來源於:-氣宗】深刻理解Java虛擬機:JVM高級特性與最佳實踐(最新第二版).pdf)
7.g1收集器
g1收集器它是一種分區收集器,是將堆內存分爲若干個大小相等region,但仍是保留了新生代和老年代,只不過不一樣代會有多個region區。G1相對於其餘的收集器具備以下優勢:
併發與並行:G1能充分利用多CPU,多核環境下的硬件優點,可經過冰法的方式讓程序繼續執行。
分代收集:分代概念在G1中依然得以保留。它可以採用不一樣的方式去處理新建立的對象和已經存活了一段時間,熬過屢次GC的舊對象以獲取更好的收集效果。
空間整合:採用了標記-整理算法,前面已經說了這種算法相對於標記-清楚算法的優點。
可預測的停頓:G1除了追求低停頓以外還能創建可預測的停頓時間模型
Region之間的對象引用以及其餘收集器中的新生代和老年代之間的對象引用,虛擬機都是使用Remembered Set,虛擬機發現Reference類型的數據進行寫操做時,會產生一個Write Barrier暫時中斷寫操做,檢查Reference對象是否處於不一樣的Region中,若是是,經過CardTable把相關引用信息記錄到引用對象所述的Region的Remembered Set之中。當進行內存時,Rememmbered便可保證不對全堆掃描也不會有遺漏。
引入Remembered Set以後,g1收集器會通過以下幾個步驟:
初始標記:初始標記階段會對直接與GC Roots對象相關聯的對象進行標記。
併發標記:併發標記階段會會標記根據可達性分析GC Roots節點引用鏈向下搜索對象進行標記,此過程可與用戶線程併發執行
終極標記:終極標記階段會將併發標記的時候用戶線程執行過程當中對象引用變化部分進行從新標記,且生成Remembered Set Logs,並將其加入到Remembered Set中,加入過程當中可能會產生停頓,但能夠併發執行
篩選回收:篩選回收階段會將Region區域按回收價值和成本進行優先排序,並按照用戶指定的回收時間進行制定回收計劃
(圖片來源於:-氣宗】深刻理解Java虛擬機:JVM高級特性與最佳實踐(最新第二版).pdf)
說完垃圾收集器以後,再來講說垃圾回收策略。
當虛擬機爲對象分配內存空間時,會先將對象分配到新生代Eden區域,若是Eden內存不夠的時候,會進行一次minor GC,將須要回收的對象複製到survivor區中。新生代區域分爲一個Eden和2個survivor區,其中Eden與survivor區域大小比值爲8:1。若是survivor區中的已滿,則會觸發一次full GC,將survivor區中的對象放到老年代中。虛擬機爲每個對象定義了一個年齡計數器,Eden對象出生並通過第一次minor GC的時候,就會將Eden區域存活的對象複製到survivor中,並將對象的年齡計數器設爲1,之後對象每通過一次minor GC ,survivor區中的對象年齡計數器就會+1,當對象熬到必定程度的時候(通常稱爲閾值,默認15),就會將對象放到老年代中。但也不是必定要等年齡到閾值才能放到老年代,當survivor區中年齡相同的對象佔空間的一半以上,則年齡大於或等於此對象的對象就能夠進入老年代了。每次進行mimor GC以前都會斷定一下Eden區域對象的大小總和是否比老年代剩餘空間大小要小,若是是,則可確保不會有問題;若是不是,可能回榆中比較極端的狀況,即當Eden區域中有大量對象存活的狀況下,survivor區不能放下複製過來的對象,這樣就直接晉升爲老年代,但若是老年代空間大小不夠就只能觸發一次full gc了。因此在minor GC以前,須要有空間分配擔保,即老年代的可用空間是否足夠容納Eden區全部存貨的對象或歷次晉升至老年代的平均大小。
本文僅僅是我的對深刻理解虛擬機的筆記整理,若有不當之處歡迎點評,轉載請註明:http://www.cnblogs.com/qven/p/8797349.html
(參考文獻:-氣宗】深刻理解Java虛擬機:JVM高級特性與最佳實踐(最新第二版).pdf)