5、垃圾回收
爲何要垃圾回收?
計算機系統,包括內存最小的尋址單元是字節;說白了,虛擬機理論上最大內存就是硬件內存,硬件內存是有限的,你佔用了,我就用不了了;因此對象不用的時候,回收其佔用內存空間,以提升虛擬機資源利用率!讓虛擬機有更高的產出!html
垃圾回收做用的區域?
程序計數器,棧區,本地方法棧區的生命週期都是和線程綁定的;線程消失,其佔用的內存也就釋放;java
因此,垃圾回收做用的區域是 堆內存(對象),方法區(常量);python
基本類型在棧區,自動回收!git
方法區如何回收常量?
String str=」abc」;在方法區常量池中會添加「abc」,後期若是有其餘字符串值爲」abc」,都會指向常量池中惟一的「abc」;github
當沒有String指向常量池中的「abc」時,就回收它了!算法
方法區回收性價比不高!編程
方法區如何回收無用的類?
類須要同時知足下面3個條件才能算是「無用的類」:數組
1)該類全部的實例都已經被回收,也就是Java堆中不存在該類的任何實例。瀏覽器
2)加載該類的ClassLoader已經被回收。緩存
3)該類對應的java.lang.Class 對象沒有在任何地方被引用,沒法在任何地方經過反射訪問該類的方法
虛擬機能夠對知足上述3個條件的無用類進行回收,這裏說的僅僅是「能夠」,而並非和對象同樣,不使用了就必然會回收。
在大量使用反射、動態代理、CGLib等ByteCode框架、動態生成JSP以及OSGi這類頻繁自定義ClassLoader的場景都須要虛擬機具有類卸載的功能,以保證永久代不會溢出。
如何判斷對象已死?
引用記數法
在對象中,好比對象頭中添加引用計數器,有一個地方引用它就+1;不然-1;計算引用計算器的值來肯定對象是否被引用。
缺點:不能解決循環引用問題
Java JVM是否實現:否,python中實現了。
可達性算法分析
什麼是可達性算法?
從垃圾回收的根對象GC Root觸發,搜索根對象持有的全部成員變量對象Objs;再從全部成員變量對象Objs出發,搜索Objs持有的全部成員變量對象Objs2;搜索不可到達的對象就是能夠垃圾回收的對象!
算法特色Stop-the-world
Stop-the-world,是說GC停頓,在執行算法時要求整個程序暫停,目的是對象引用鏈不能改變。
由於任何垃圾回收算法判斷對象是否存活都使用可達性算法來分析,因此,任何垃圾回收算法,標記階段都必須Stop-the-world。
哪些對象能夠做爲GC Root對象?
1)虛擬機棧中引用的對象,(棧幀中本地變量表中引用的對象)
就是對象A的普通成員變量是對象B;A和B都在棧幀中,均可作GC Root對象
2)方法區中類靜態屬性引用的對象;
就是對象A的靜態成員是對象static B,而 static B在方法區中,B中引用了對象的成員變量,那麼B也做爲GC Root對象
3)方法區中常量引用的對象
就是對象A的成員對象是final B,final修飾的B默認就是final static B;B中引用了對象的成員變量,那麼B也做爲GC Root對象
1) 本地方法棧中JNI(native方法中)引用的對象
不可達對象的逃脫
http://blog.csdn.net/mark2when/article/details/59162810
http://blog.csdn.net/w605283073/article/details/72757684
1)從GC Roots搜索全部不可到達對象
2)準備執行不可到達對象的finalize方法
若是finalize方法執行過,直接垃圾回收
若是finalize方法每執行過,執行它,而後垃圾回收
若是執行finalize方法的過程當中,該對象被其餘可到達對象引用了,則該對象逃脫!
/**
* 此代碼演示了兩點:
* 1.對象能夠在被GC時自我拯救。
* 2.這種自救的機會只有一次,由於一個對象的finalize()方法最多隻會被系統自動調用一次
* @author zzm
*/
public class FinalizeEscapeGC {
public static FinalizeEscapeGC SAVE_HOOK = null;
public void isAlive() {
System.out.println("yes, i am still alive :)");
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize mehtod executed!");
FinalizeEscapeGC.SAVE_HOOK = this;
}
public static void main(String[] args) throws Throwable {
SAVE_HOOK = new FinalizeEscapeGC();
//對象第一次成功拯救本身
SAVE_HOOK = null;
System.gc();
// 由於Finalizer方法優先級很低,暫停0.5秒,以等待它
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("no, i am dead :(");
}
// 下面這段代碼與上面的徹底相同,可是此次自救卻失敗了
SAVE_HOOK = null;
System.gc();
// 由於Finalizer方法優先級很低,暫停0.5秒,以等待它
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("no, i am dead :(");
}
}
}
運行結果:
finalize mehtod executed!
yes, i am still alive :)
no, i am dead :(
SAVE_HOOK對象的finalize()方法確實被GC收集器觸發過,而且在被收集前成功逃脫了。
另一個值得注意的地方是,代碼中有兩段徹底同樣的代碼片斷,執行結果倒是一次逃脫成功,一次失敗,這是由於任何一個對象的finalize()方法都只會被系統自動調用一次,若是對象面臨下一次回收,它的finalize()方法不會被再次執行,所以第二段代碼的自救行動失敗了。
三色標記算法
併發標記中,就是用戶程序和標記算法同時執行的過程,使用三色標記算法!
它是描述追蹤式回收器的一種有用的方法,利用它能夠推演回收器的正確性。 首先,咱們將對象分紅三種類型的。
黑色:根對象,或者該對象與它的子對象都被掃描
灰色:對象自己被掃描,但還沒掃描完該對象中的子對象
白色:未被掃描對象,掃描完成全部對象以後,最終爲白色的爲不可達對象,即垃圾對象
當GC開始掃描對象時,按照以下圖步驟進行對象的掃描:
根對象被置爲黑色,子對象被置爲灰色,沒有引用的對象被置爲白色
繼續由灰色遍歷,將已掃描了子對象的對象置爲黑色。
遍歷了全部可達的對象後,全部可達的對象都變成了黑色。不可達的對象即爲白色,須要被清理
問題來了:併發標記時用戶程序更改對象引用關係了怎麼辦?
如何解決併發標記時用戶更改對象引用問題?
程序代碼更改對象引用有2種方式:
1)增長對象引用,建立對象Object o=new Object(); 或者o1=new Object()
2)刪除對象引用,o=null;
因此併發標記保證應用程序在運行的時候,GC標記的對象不丟失,有以下2中可行的方式:
1)在新增對象時,記錄對象的reference關係到可到達的對象結構中;
2)在刪除的時候,從可到達對象結構中,刪除對象的reference
恰好這對應CMS和G1的2種不一樣實現方式:
1)在CMS中,記錄新增,不記錄刪除,採用的是增量更新(Incremental update),只要在寫屏障(write barrier)裏發現要有一個白對象的引用被賦值到一個黑對象 的字段裏,那就把這個白對象變成灰色的。即插入的時候記錄下來。
2)在G1中,記錄刪除,不記錄新增,使用的是STAB(snapshot-at-the-beginning)的方式,刪除的時候記錄全部的對象,它有3個步驟:
第1,在開始標記的時候生成一個快照圖標記存活對象
第2,在併發標記的時候全部被改變的對象入隊(在write barrier裏把全部舊的引用所指向的對象都變成非白的)
第3,可能存在遊離的垃圾,將在下次被收集
Write Barrier
https://stackoverflow.com/questions/19154607/how-actually-card-table-and-writer-barrier-works
Write Barrier是併發標記時,對用戶程序更改對象引用關係的一種監聽機制,會把用戶程序對對象-引用關係的更改記錄到remember set log中,並在後期處理remember set log時告知Garbage Collector;方便垃圾回收器GC。
不一樣的是,你記錄新增仍是刪除。CMS就記錄新增,G1就記錄刪除!
write barrier - a piece of code executed whenever a member variable (of a reference type) is assigned/written to. If the new reference points to a young object and it's stored in an old object, the write barrier records that fact for the garbage collect. The difference lies in how it's recorded.
具體能夠看我以前的回答整理
https://www.zhihu.com/question/37028283
GC的本質
GC的本質就是,從GC Roots觸發搜索全部不可到達對象,而後執行這些對象的finalize()方法,而後再垃圾回收這些對象!
因此執行finalize()時,若是要回收的對象被可到達對象引用,則該對象逃脫GC。
引用類型
Reference引用存儲的是堆內存的地址
引用有哪些類型?
1)強引用Strong reference
2)軟引用Soft reference
什麼是軟引用類型?
軟引用類型就是有用但非必須的對象
3)弱引用Weak reference
4)虛引用Phantom reference
爲何有引用類型?
引入引用類型的級別,是爲了根據是否能夠回收內存,把對象進行分類;哪些能夠回收,哪些不能回收,這樣讓JVM的垃圾回收的目的更具體,更高效!
強引用類型
什麼是強引用類型?
強引用類型就是這種,Object obj=new Object()
如何回收強引用類型對象?
當內存空間不足,拋出OutOfMemoryError錯誤
除非obj=null;不然只要強引用還在,垃圾回收器就不會回收引用的對象!
分2種狀況
1)方法中使用強引用類型
public void test(){
Object o=new Object();
// 省略其餘操做
}
此時,執行test()方法時,test方法進入棧幀,對象o的reference引用在線程棧區建立,而reference指向的對象在堆區;test()方法執行完畢,test方法退出棧幀,對象o的引用自動隨之清除,堆內存的Object對象也會被垃圾回收!
結論:不用管,自動清除!
2)成員變量使用強引用類型
成員變量Object obj=new Object();
在成員變量使命達成後,將來再也不使用,則手動設置obj=null,則對象將被垃圾回收!
如何使用強引用類型?釋放數組元素空間
一句話:除非你把我置爲null,不然JVM是不會回收強引用類型的!(方法內除外)。
你能夠保留個人位置,這樣就不用從新分配內存了!
強引用在實際中有很是重要的用處,舉個ArrayList的實現源代碼:
private transient Object[] elementData;
public void clear() {
modCount++;
// Let gc do its work
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
注意ArrayList私有成員變量elementData,在clear()執行時,把elementData數組中每一個成員置爲null;而不是把elementData置爲null;這樣數組中的對象都將被垃圾回收;而elementData數組由於是強引用,它的棧reference,在棧內存,數組對應(數組對象)空間在堆內存不變,避免後續調用add()或其餘方法時 還得從新分配數組對象空間!
軟引用類型
什麼是軟引用類型?
軟引用類型修飾的對象是 有用但非必須的對象!什麼意思?缺了軟引用對象,程序也照常運行!
JVM內存不足時,會清除軟引用類型修飾的對象!
生命週期:建立到JVM內存不足,內存充足的時候也可能進行垃圾回收!
如何回收軟引用類型對象?
String str=new String("abc"); // 強引用
SoftReference<String> softRef=new SoftReference<String>(str); // 軟引用
虛擬機會在內存溢出以前,回收掉軟引用類型的對象str!內存不足時軟引用類型對象自動被垃圾回收!
至關於,內存不足,JVM 自動把str置爲null,而後等到垃圾回收str。
If(JVM.內存不足()) {
str = null; // 轉換爲軟引用
System.gc(); // 垃圾回收器進行回收
}
軟引用類型能夠用來作什麼?
能夠用來實現內存敏感的高速緩存!
如何使用軟引用類型?瀏覽器緩存
一句話:有內存,我存在,沒內存,我奉獻個人內存!你能夠在你須要時使用我,不須要我時清除我!
虛引用在實際中有重要的應用,例如瀏覽器的後退按鈕。按後退時,這個後退時顯示的網頁內容是從新進行請求仍是從緩存中取出呢?這就要看具體的實現策略了。
(1)若是一個網頁在瀏覽結束時就進行內容的回收,則按後退查看前面瀏覽過的頁面時,須要從新請求;
(2)若是將瀏覽過的網頁存儲到內存中會形成內存的大量浪費,甚至會形成內存溢出
這時候就可使用軟引用
Browser prev = new Browser(); // 獲取頁面進行瀏覽
SoftReference sr = new SoftReference(prev); // 瀏覽完畢後置爲軟引用
if(sr.get()!=null){
rev = (Browser) sr.get(); // 尚未被回收器回收,直接獲取
}else{
prev = new Browser(); // 因爲內存吃緊,因此對軟引用的對象回收了
sr = new SoftReference(prev); // 從新構建
}
軟引用能夠和一個引用隊列(ReferenceQueue)聯合使用,若是軟引用所引用的對象被垃圾回收器回收,Java虛擬機就會把這個軟引用加入到與之關聯的引用隊列中。?
弱引用類型
什麼是弱引用類型?
弱引用類型的對象是更沒必要須的對象!
軟引用類型對象會在jvm內存不足時被垃圾回收!
弱引用類型對象會在垃圾回收線程發現它時就被回收,不論jvm內存是否不足!
不過,因爲垃圾回收器是一個優先級很低的線程,所以不必定會很快發現那些只具備弱引用的對象。
生命週期:建立到第一次垃圾回收!
如何回收弱引用類型?
自動回收str,不論內存是否不足
String str=new String("abc");
WeakReference<String> abcWeakRef = new WeakReference<String>(str);
弱引用類型轉化成強引用類型
String abc = abcWeakRef.get(); //一句代碼就把str轉成強引用類型了
何時使用弱引用類型?
1)偶爾使用,隨用隨取的對象
若是這個對象是偶爾的使用,而且但願在使用時隨時就能獲取到,但又不想影響此對象的垃圾收集,那麼你應該用 Weak Reference 來記住此對象。
2)引用一個對象又不想改變它的生命週期時,使用弱引用類型
弱引用類型舉例
public class ReferenceTest {
private static ReferenceQueue<VeryBig> rq = new ReferenceQueue<VeryBig>();
public static void checkQueue() {
Reference<? extends VeryBig> ref = null;
while ((ref = rq.poll()) != null) {
if (ref != null) {
System.out.println("In queue: " + ((VeryBigWeakReference) (ref)).id);
}
}
}
public static void main(String args[]) {
int size = 3;
LinkedList<WeakReference<VeryBig>> weakList = new LinkedList<WeakReference<VeryBig>>();
for (int i = 0; i < size; i++) {
weakList.add(new VeryBigWeakReference(new VeryBig("Weak " + i), rq));
System.out.println("Just created weak: " + weakList.getLast());
}
System.gc();
try { // 下面休息幾分鐘,讓上面的垃圾回收線程運行完成
Thread.currentThread().sleep(6000);
} catch (InterruptedException e) {
e.printStackTrace();
}
checkQueue();
}
}
class VeryBig {
public String id;
// 佔用空間,讓線程進行回收
byte[] b = new byte[2 * 1024];
public VeryBig(String id) {
this.id = id;
}
protected void finalize() {
System.out.println("Finalizing VeryBig " + id);
}
}
class VeryBigWeakReference extends WeakReference<VeryBig> {
public String id;
public VeryBigWeakReference(VeryBig big, ReferenceQueue<VeryBig> rq) {
super(big, rq);
this.id = big.id;
}
protected void finalize() {
System.out.println("Finalizing VeryBigWeakReference " + id);
}
}
最後的輸出結果爲:
Just created weak: com.javabase.reference.VeryBigWeakReference@1641c0
Just created weak: com.javabase.reference.VeryBigWeakReference@136ab79
Just created weak: com.javabase.reference.VeryBigWeakReference@33c1aa
Finalizing VeryBig Weak 2
Finalizing VeryBig Weak 1
Finalizing VeryBig Weak 0
In queue: Weak 1
In queue: Weak 2
In queue: Weak 0
虛引用類型
什麼是虛引用類型?
與軟引用,弱引用不一樣,虛引用指向的對象十分脆弱,咱們不能夠經過get方法來獲得其指向的對象。
虛引用類型必須和引用隊列一塊兒使用!PhantomReference類實現虛引用!
虛引用類型有什麼做用?
惟一做用就是當其指向的對象被回收以後,本身被加入到引用隊列,用做記錄該引用指向的對象已被銷燬。
虛引用類型的使用場景有哪些?
1)虛引用類型可讓你知道它指向的對象何時從內存中移除
設置了虛引用的對象在被垃圾回收時會接到系統發送的通知!
而實際上這是Java中惟一的方式。這一點尤爲表如今處理相似圖片的大文件的狀況。當你肯定一個圖片數據對象應該被回收,你能夠利用虛引用來判斷這個對象回收以後在繼續加載下一張圖片。這樣能夠儘量地避免可怕的內存溢出錯誤。
2)避免析構問題
參考https://droidyue.com/blog/2014/10/12/understanding-weakreference-in-java/
總結與參考
引用類型 |
被垃圾回收時間 |
用途 |
生存時間 |
強引用 |
歷來不會 |
對象的通常狀態 |
JVM中止運行時終止 |
軟引用 |
在內存不足時 |
對象緩存 |
內存不足時終止 |
弱引用 |
在垃圾回收時 |
對象緩存 |
gc運行後終止 |
虛引用 |
Unknown |
Unknown |
Unknown |
Java 7之基礎 - 強引用、弱引用、軟引用、虛引用
http://blog.csdn.net/mazhimazh/article/details/19752475
譯文:理解Java中的弱引用- 技術小黑屋
https://droidyue.com/blog/2014/10/12/understanding-weakreference-in-java/
垃圾回收算法
標記-清除法
最基礎的算法,其餘算法都是它的改進版!
算法過程
1)標記全部沒有引用的對象
2)統一回收全部被標記的對象
適用區域
無,理論算法
缺點是什麼?
1)標記和清除的效率都不高,爲何?
2)產生大量的內存碎片,重點
複製回收法
算法過程
標記-複製;
1) 將內存分爲等大小2塊區域A和B,
2) 只使用一塊區域A,
3) 等A區域滿了,對A進行標記,把全部存活對象集中複製到B區域
4) 清理A區域
適用區域
年輕代 ,對象存活率較低,複製的開銷低!
若是使用在年老代,對象存活率高,複製的開銷也高!
優缺點
優勢:解決內存縫隙問題
缺點:內存使用率低
商業虛擬機大部分使用這種算法,Hotspot虛擬機年輕代Eden和2個Suivivor比例爲
8:1:1就是爲了不內存碎片問題
標記-整理法
適用區域
老年代,對象存活率較高,不用複製算法
算法過程
1) 遍歷GC Roots,對全部存活對象標記
2) 把全部存活對象,集中複製到內存某一端(最前或者最後)連續區域A
3) 清理除了A外全部區域
優缺點
優勢:連續空間
缺點:效率不高,標記全部存活對象,並記錄全部對象引用地址
JVM垃圾回收策略-分代收集
通過大量實際觀察得知,在面向對象編程語言中,絕大多數對象的生命週期都很是短。分代收集的基本思想是,將堆劃分爲兩個或多個稱爲 代(generation) 的空間。新建立的對象存放在稱爲 新生代(young generation) 中(通常來講,新生代的大小會比 老年代 小不少),隨着垃圾回收的重複執行,生命週期較長的對象會被 提高(promotion) 到老年代中。所以,新生代垃圾回收和老年代垃圾回收兩種不一樣的垃圾回收方式應運而生,分別用於對各自空間中的對象執行垃圾回收。新生代垃圾回收的速度很是快,比老年代快幾個數量級,即便新生代垃圾回收的頻率更高,執行效率也仍然比老年代垃圾回收強,這是由於大多數對象的生命週期都很短,根本無需提高到老年代。
把Java堆分爲年輕代(Eden:Survior1:Survivor2=8:1:1),老年代,再加上方法區的永久代;一共3個區域;
按照區域的特色,分別有適合垃圾回收算法!
對象存活率低,複製算法;
對象存活率高,標記清理,或者標記整理
垃圾回收過程
查找GC Root過程
GC Roots主要存在於常量池全局變量,線程棧本地變量表的引用;但如今應用方法區有上百兆,如何高效查找到全部GC Root?
GC停頓後,並不是遍歷全部線程棧和方法區查找GC Roots;而是在類加載的過程當中虛擬機就把對象什麼偏移量上是什麼類型變量計算出來,放在OopMap數據結構中,JIT編譯過程也會記錄棧和寄存器中哪些位置是引用,這樣JVM遍歷OopMap就知道哪些是GC Roots了。
什麼是OopMap?
https://stackoverflow.com/questions/26029764/what-does-oop-maps-means-in-hotspot-vm-exactly
OopMap是記錄對象索引在Java棧的哪些位置的數據結構,OopMap的首要做用是查找GC Roots;當對象從堆中刪除時,其索引也被刪除了,因此OopMap要在須要的時候更新reference紀律!
問題來了:何時更新OopMap?
安全點GC safepoint
http://blog.ragozin.info/2012/10/safepoints-in-hotspot-jvm.html
查找OopMap就知道GC Roots了,問題是程序一直在運行,能夠致使OopMap變化的指令不少,何時記錄(更新)OopMap?
你不知道何時應該GC,若是時刻記錄OopMap,那麼內存成本很高!
因此這個問題能夠轉化成-JVM應該何時GC?
由於GC的時候纔要查找GC Roots,查找GC Roots才須要OopMap!
那何時才應該GC呢?
答案:safepoint的時候!
http://www.sczyh30.com/posts/Java/jvm-gc-safepoint-condition/
什麼是GC safepoint?
https://stackoverflow.com/questions/19201332/java-gc-safepoint
safepoint的實現機制取決於JVM的實現機制!
HotSpot中safepoint指的是: JVM在GC以前暫停程序來進行垃圾回收的機制!
Safepoint期間,全部java線程中止,native若是不和JVM交互則不用中止,全部GC Roots是肯定的,引用和對象的關係鏈也是肯定的,在這個期間垃圾回收程序回收java堆上無引用的對象!
何時是safepoint呢?
若是要觸發一次GC,那麼JVM中全部Java線程都必須到達GC Safepoint。
JVM只會在特定位置放置safepoint,好比:
1)內存分配的地方(allocation,即new一個新對象的時候)
2)長時間執行區塊結束的時刻(如方法調用,循環跳轉,異常跳轉等)
Safepoint的時刻就是GC的時刻!GC的時刻就是Stop-the-world時刻!
因此safepoint時刻就是STW時刻!
這樣,JVM只在程序運行到1),2)狀況時才記錄/更新OopMap;
問題來了,如何讓全部線程都到達本身最近Safepoint呢?
如何讓全部線程同時到達本身最近Safepoint?
兩種方式,搶佔式和主動式是從線程的角度說的!
1)搶佔式中斷-理論方法沒有JVM實現
GC發生時,首先把全部線程所有中斷,遍歷檢查全部線程,若是發現有線程不在safepoint,則恢復該線程,知道它跑到安全點上!直到全部線程到safepoint上,進行垃圾回收。
2)主動式中斷-大部分JVM實現
GC發生時,JVM不中斷全部線程;全部線程檢查本身是否在safepoint上,若是在則在本身線程上作一個標誌,而且線程自動暫停;全部線程依自身狀況前後主動暫停本身;
而後進行垃圾回收!
Safepoint有什麼用呢?
Garbage collection pauses垃圾回收暫停
Code deoptimization代碼優化
Flushing code cache刷新代碼緩存
Class redefinition (e.g. hot swap or instrumentation)類別從新定義(例如熱插拔或儀器)
Biased lock revocation有偏見的鎖定撤銷
Various debug operation (e.g. deadlock check or stacktrace dump)
各類debug操做,如死鎖檢查,堆棧dump
GC safepoint機制有何漏洞?
主動式中斷線程的方式Safepoint被大部分JVM實現,主動式中斷要求一點:線程是清醒的執行的狀態,若是線程處於阻塞狀態或者等待狀態怎麼辦?
JVM中程序不等人啊,程序要求一致運行!怎麼辦?
用Safe Region安全區域解決!
安全區域GC Safe region
什麼是安全區域safe region?
Safe region指一段代碼區域,這個區域中不會更改引用-對象的關係,因此在這個區域中任意位置均可以開始GC。
線程阻塞或者等待狀態就不會更改引用對象的關係!
Safe region如何結合safe point工做?
1)在GC前,全部線程標註本身是否進入safepoint狀態,是否進入safe region狀態!
2)JVM不會去管進入safe region狀態的線程;只需等待其餘線程進入safepoint狀態便可開始GC回收;
3)若是進入safe region狀態線程要離開safe region狀態,JVM先查看是否完成GC任務,完成則能夠離開,不然不能離開!
垃圾收集器
圖片來自https://crowhawk.github.io/2017/08/15/jvm_3/
http://zqhxuyuan.github.io/2016/07/26/JVM/ 多圖
GC的一些概念
在垃圾回收中;
並行,指垃圾回收器並行多個線程同時執行;但垃圾回收器工做期間,程序STW;
併發,指用戶程序與垃圾回收器併發執行;不必定是並行的,可能交替執行!
用戶程序與垃圾回收器能並行同時執行嗎?
不能全程同時執行;某些時刻t,須要用戶程序STW,
吞吐量
吞吐量就是CPU用於運行用戶代碼的時間與CPU總消耗時間的比值!
吞吐量=cpu運行用戶代碼時間/(cpu運行代碼時間+cpu垃圾回收時間)
虛擬機運行100分鐘,垃圾回收1分鐘,吞吐量就是99%
GC的類型
https://www.zhihu.com/question/41922036
https://plumbr.io/blog/garbage-collection/minor-gc-vs-major-gc-vs-full-gc
針對HotSpot的GC,分爲兩大類:
1)Partial GC:並不收集整個堆的模式
Young GC
只收集young代的GC
Old GC
只收集old代的GC,只有CMS是這個模式,其餘說收集old的都會帶着收集young
Mixed GC
收集整個young代+部分old代的GC,只有G1是這個模式
2)Full GC,收集整個堆,young代,old代,perm代(jdk8把perm移到了堆區)
通常Major GC指Full GC,也有人認爲Major GC指Old GC,因此要問清楚他時候的究竟是哪一個!
Young GC又稱Minor GC
什麼是Minor GC?
清理年輕代(Eden,2Survivor)的Young GC又叫作Minor GC
什麼時候觸發Minor GC?
當Eden區滿了,不能分配內存給new的對象時;
Minor GC的特色
1.Minor GC頻繁發生
Eden常常滿,對象常常死亡,因此Minor GC頻繁發生
2.Minor GC後沒有內存碎片
年輕代Minor GC採用複製算法,通過標記後,Eden區的存活對象被複制到2個(0,1)Survivor區的1個;
對於Eden區,內存指針能夠從0開始,沒有內存碎片;
對於Survivor區,2個區域是沒有前後順序的,一個使用,另一個就用來複制;因此Survivor的1區域內存指針也老是從0開始的,1個Survivor區沒有內存碎片;
3.Minor GC清理年輕代,老年代也不會被清理。
Minor GC採用複製算法,標記-複製的標記階段中,從年老代指向年輕代的引用被認爲是有效的引用,而從年輕代指向年老代的引用則認爲是無效的引用!
4.Minor GC的 STW時間和Eden中垃圾對象的多少有關
Full GC
Major GC和Full GC是非官方的說法。
Major GC說的就是Full GC
Full GC是清理young+old+perm(若是屬於java 堆)的GC;
什麼時候觸發Full GC
https://stackoverflow.com/questions/24766118/when-is-a-full-gc-triggered
1)準備觸發Minor GC時,發現Old區剩餘的空間不夠,若是不使用CMS,則觸發Full GC。
由於只有CMS是單獨收集Old區的!其餘收集Old去的都會收集整個堆!
2)若是堆中有Perm代(jdk8),當Perm區不夠時也會觸發Full GC
3)System.gc( )觸發Full GC
4)Heap dump帶的GC也是Full GC
把Heap中數據生成dump文件來分析JVM故障。
5)調節young,old區域size時也觸發Full GC;
Parallel Scavenge(-XX:+UseParallelGC)框架下,默認是在要觸發full GC前先執行一次young GC,而且兩次GC之間能讓應用程序稍微運行一小下,以期下降full GC的暫停時間(由於young GC會盡可能清理了young gen的死對象,減小了full GC的工做量)。控制這個行爲的VM參數是-XX:+ScavengeBeforeFullGC。
回收策略
年輕代serial,parNew,Parallel Scavenge使用複製算法;
老年代Serial old,Parallel old使用標記-整理算法,CMS使用標記-刪除算法;
垃圾收集器組合使用
不一樣廠商,不一樣JVM,實現的垃圾收集器也不一樣!
用戶也會根據應用特色組合各年代所使用的的垃圾收集器!
有連線,說明能夠組合使用!
不然,不能組合! Tenured gen老年代
注重吞吐量以及CPU資源敏感的場合,能夠優先考慮Parallel Scavenge+Parallel Old收集器
Serial回收器
什麼是serial垃圾回收器?
Serial是一個使用複製算法的單線程垃圾回收器,只使用單cpu,單線程執行GC,並且執行GC的過程必須暫停程序。
Serial垃圾回收適用情景?
1)JVM client模式下默認垃圾回收器!新生代收集器!
何時使用Serial垃圾回收期?
2)小應用,佔用java堆內存少200M之內,停頓時間能夠控制在100毫秒之內;這樣不影響客戶端體驗
3)單核cpu或2核cpu;serial收集器沒有線程交互的開銷,專心作垃圾收集對於少核cpu來講效率較高!
4)與CMS配合使用
Serial垃圾回收器如何工做?
1)垃圾回收前STW,單線程對新生代Eden使用複製算法收集;
2)垃圾回收前STW,單線程對老年代Old使用標記整理算法收集;
ParNew 收集器
什麼是ParNew垃圾回收器?
ParNew就是Serial收集器的多線程版本!暫停程序!也是複製算法
Serial垃圾回收適用情景?
1)新生代收集器
何時使用ParNew垃圾回收器?
JVM Server模式下默認垃圾回收器!
2)與CMS配合使用
CMS老年代收集器只能和Serial,ParNew收集器使用!
爲何使用ParNew回收器?
ParNew垃圾回收器如何工做?
1)垃圾回收前STW,多線程對新生代Eden使用複製算法收集;
2)垃圾回收前STW,單線程對老年代Old使用標記整理算法收集;
它默認開啓的收集線程數與CPU的數量相同,在CPU很是多的狀況下可以使用-XX:ParallerGCThreads參數設置
如何使用ParNew回收器?
1)使用-XX:+UseConcMarkSweepGC選項設置使用CMS進行老年代收集後,新生代默認就使用ParNew回收器!
2)強制指定使用ParNew回收器
-XX:+UseParNewGC選項
Parallel Scavenge收集器
Scavenge是清除的意思
什麼是Parallel Scavenge垃圾回收器?
Parallel是一個使用複製算法的新生代垃圾回收器;並行多線程收集;
關注點
吞吐量優先
Parallel Scavenge收集器的目標是可控的吞吐量!
吞吐量就是CPU用於運行用戶代碼的時間與CPU總消耗時間的比值!
吞吐量=cpu運行用戶代碼時間/(cpu運行代碼時間+cpu垃圾回收時間)
虛擬機運行100分鐘,垃圾回收1分鐘,吞吐量就是99%
停頓時間越短,越適合須要與用戶交互的程序!須要高吞吐量!!
Parallel Scavenge的應用情景?
新生代收集器
何時使用Parallel Scavenge垃圾回收器?
爲何使用Parallel Scavenge垃圾回收器?
Parallel Scavenge垃圾回收期如何工做?
如何使用Parallel Scavenge回收器?
Parallel Scavenge提供了2個參數,用於控制吞吐量,分別是:
1)最大垃圾收集停頓時間:
-XX:MaxGCPauseMillis 數值>0
收集器儘可能保證回收時間不超過該值,具體停頓時間由實際狀況而定!
正常狀況下,垃圾回收的停頓時間是不會改變的,除非你調小新生代內存的大小!如收集500M和收集300M相比,確定後者時間少,可是後者確定收集的更頻繁!原來10S收集一次,每次停頓100毫秒;如今每5秒收集一次,每次停頓70毫秒;停頓時間將下來,但總體吞吐量可能也降下來了!
2)吞吐量大小,直接設置吞吐量
-XX:GCTimeRatio 數值大於0小於100,默認是99
吞吐量=代碼時間 /(代碼時間+停頓時間)
3)Parallel Scavenge有自適應調節策略GC Ergonomics
-XX:+UseAdaptiveSizePolicy 是一個開關參數
設置後,虛擬機按照實際狀況自動調節新生代Eden與Survivor的比例,自動調節此生老年代對象的年齡等參數!
動態調整這些參數來得到最大的吞吐量!
Parallel Scavenge收集器的特色
具備自適應調節策略!
Serial Old收集器
Serial收集器的老年代版本!使用複製-整理算法!
適用場景
1)Client模式的 老年代回收器!
2)Server模式下,做爲CMS的備案。當CMS發生concurrent mode Failure使用!
Parallel Old收集器
Parallel Scavenge收集器的老年代版本,使用標記-整理算法
關注點
吞吐量優先
應用情景
注重吞吐量以及CPU資源敏感的場合,均可以優先考慮Parallel Scavenge+Parallel Old收集器組合!
如何使用Parallel Old收集器?
CMS收集器
什麼是CMS收集器?
https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/cms.html
Concurrent Mark Sweep清掃,CMS使用併發的標記-清除算法
CMS的關注點
最小化GC停頓時間。
如何作到的?--這是尋找信息時給別人的回答
http://www.cnblogs.com/littleLord/p/5380624.html
首先,CMS垃圾回收器的設計目標 是 最小化 STW的時間,也就是最小化用戶程序的暫停時間!
而後,要讓程序運行,又要垃圾回收,想一個這種的辦法就是讓程序和垃圾回收同時運行!
可是,垃圾回收的可達性分析階段,對不可到達對象的標記時必需要STW(由於對動態的reference-對象關係標記是沒有用的);
問題來了:如何減小STW呢?
CMS的作法:
第一,初始標記,初始標記不是遞歸標記全部不可到達對象;而是從GC Roots出發,只標記第一級關聯對象;須要STW,可是時間很短,由於第一級對象對於全部對象來講數量不多
第二,併發標記,讓CMS垃圾回收程序和用戶程序同時運行;因此沒有STW;此次標記確定是從第一級關聯對象出發;搜索全部不能到達的對象;
這是一個很消耗時間的過程,可是由於用戶程序在運行,因此沒影響;
第三,從新標記,由於第二階段用戶程序在運行,可能產生新的對象;這一階段須要把這些新產生的對象找出來;因此須要STW;
若是是我設計這個階段的搜索,我必定會記錄第二階段什麼時間點,用戶新增了對象;這樣查找新增的對象時間也不會好久;
第四,併發清理;直接刪除第三階段肯定的全部不可到達對象;產生了內存碎片;
CMS如何工做?
工做流程分爲4步:
1)初始標記CMS initial mark -STW
標記GC Roots能直接關聯到的全部一級關聯對象,會STW,時間短,由於GC Roots數量和總體對象數量相比仍是小!
暫停,時間短
2)併發標記CMS concurrent mark
從GC Roots的一級關聯對象出發,遞歸標記全部能標記的對象;
時間長,不暫停
3)從新併發標記CMS remark -STW
從新標記可到達對象;
爲何要從新標記?爲何還要STW從新標記?
由於2)中併發標記時程序運行,可能改變reference-對象的關係;因此若是想要一個肯定的reference-對象的關係,必須STW。
時間長,暫停
4)併發清除CMS concurrent sweep
併發清除全部垃圾對象
如何使用CMS?
1)使用CMS收集Old
-XX:+UseConcMarkSweepGC選項,則老年代使用CMS,而後默認年輕代使用ParNew收集器。
2)設置Old內存佔滿多少觸發CMS垃圾回收
能夠用-XX:CMSInitiatingOccupancyFraction值來設定老年代內存達到多少百分比來觸發CMS垃圾回收!
可是,該值設置多少合適呢?
該值設置過低,CMS垃圾回收太頻繁;
設置過高,預留給程序內存不足,CMS回收失敗,觸發Serial Old回收,停頓時間更長
3)解決CMS碎片問題
使用-XX:+UseCMSCompactAtFullCollection(默認開啓);用於CMS頂不住要進行FullGC時開啓內存碎片的合併整理過程,空間碎片沒有了,可是STW停頓時間變長了。
4)設置執行多少次不壓縮的FullGC後壓縮
使用-XX:CMSFullFCsBeforeCompaction,默認值爲0,每次進入Full GC都碎片整理。
CMS啓動多少線程進行併發回收?
CMS默認啓動回收線程數 tnum=( Cpu數量+3 ) / 4
也就是說,即使不+3, CMS也要佔用1/4的Cpu資源。
CM優缺點
優勢:
1)用戶程序STW停頓時間少;
缺點:
提高CMS的吞吐量(提高程序代碼運行時間)是以犧牲程序的性能爲前提的
是因爲GC和程序同時運行致使。
1)CMS收集器對Cpu資源敏感,併發收集階段會發生於用戶程序搶Cpu資源狀況。
併發收集垃圾過程通常佔用1/4 cpu資源!
2)CMS失敗Concurrent Mode Failuer用戶程序也暫停,可能致使另外一次Full GC的產生!
Why CMS失敗致使Full GC?
3)CMS使用併發標記-清除算法,產生大量內存碎片。
爲何CMS失敗會觸發Full GC?
1)併發收集階段,因爲用戶程序也要運行,因此,須要給程序預留足夠內存空間;
2)所以,CMS收集器不能等老年代填滿再收集,由於老年代也須要預留空間給程序。
JDK1.5默認設置下,當老年代使用68%空間則CMS就會被激活;68%的設置能夠用
-XX:CMSInitiatingOccupancyFraction值來設定。
JDK1.6,默認92%老年代使用激活CMS回收;這時若是CMS運行期間程序所需老年代內存>8%,則會發生Concurrent Mode Failure失敗,
這時,JVM啓動備案,臨時啓用Serial Old收集器來從新進行老年代垃圾回收!
什麼是浮動垃圾Floating Garbage?
CMS併發清理回收垃圾的階段,程序是運行的;運行就可能產生新的reference-對象;
因此,浮動垃圾,就是CMS併發清理過程,由用戶程序新生成的對象!
CMS沒法處理浮動垃圾Floating Garbage,只能下次CMS時回收!
增量式併發收集器I-CMS
在併發標記,清理的時候讓GC線程,用戶程序交替運行,儘可能減小GC線程獨佔資源的時間!
後果:整個GC過程會更長,可是對用戶程序影響就顯得少一些,速度降低沒有那麼快!
使用效果也不太好!
官方設置爲deprecated,再也不提倡用戶使用!
G1收集器-通用收集器
什麼是G1收集器?
Garbage First Collector,簡稱G1 Collector;是Hotspot1.7之後面向大內存(Heap區n~10G)、多核系統的收集器。
那到底什麼是G1收集器?
G1是一種能夠操做全堆的,併發、並行、部分Stop The World、使用Copying算法收集region的分代的增量式收集器!
G1在JDK1.9中成爲默認收集器!
Region?啥是Region?
Region是區域的意思,就是內存的一段地址區間;G1算法將Heap區域分紅獨立等大的Region,G1兼收代的概念,每一個Region屬於一個代(可能不一樣部分),如E表明Eden的region,O表明Old代的region等等。
操做全堆?
能收集young和old,而不是像CMS只能收集old。
併發?
未
並行?
部分STW?
使用Copying算法?
G1將全堆內存分割成等大小獨立的Region,在GC階段,A的region存活對象被Copying到B Region。
問題來了?
Region是內存,JVM如何記錄對象內存地址的變化的?
分代?
G1中兼收Young代(Eden,Survior),Old代的概念;不過G1分割過的堆,代的地址空間不是連續的了!極可能一個region是Eden的,右邊相鄰的region是Old代的!
增量收集?
G1的設計目標/關注點是最小化STW;爲了實現這一目標,G1把Heap堆切分爲不少Region,而後根據Region回收的價值(如最多垃圾對象最早回收)回收相應Region來解決內存不足的問題,而不是以往老套算法整代區域都要GC。
爲何叫G1收集器?怎麼不叫G2呢?
Garbage First,First是啥意思?
這裏的First是指回收價值最高;
那怎麼叫回收價值最高呢?
確定是垃圾對象佔比越高的Region回收價值越高;
每次G1收集時會判斷各Region的活性(存活對象的佔比),垃圾對象佔比越多回收價值越高,而後G1會參考按照以前回收某些Region消耗的時間,來估算此次回收哪些Region。用最小時間獲取最大收益!因此叫Garbage First!
以什麼爲單位回收什麼呢?
Region;
合起來就是:對回收價值最高的Region進行回收!
爲何要分region呢?
這個思想來源是分治思想,仍是要回歸到G1的設計目標:最小化STW!
先不說標記,光說回收!收集一整代時間長?仍是收集一整代的某一部分時間長?
確定是後者時間短,因此就分region了!
這種將Heap區劃分紅多塊的理念源於:當併發後臺線程尋找可回收的對象時、有些區塊包含可回收的對象要比其餘區塊多不少。雖然在清理這些區塊時G1仍然須要暫停應用線程、但能夠用相對較少的時間優先回收包含垃圾最多區塊。這也是爲何G1命名爲Garbage First的緣由:第一時間處理垃圾最多的區塊。
爲何須要G1收集器?GC三個性能指標
Hotspot以前已經攜帶了Serial, Paralel, CMS等收集器,爲何還須要研發一個新的G1呢?垃圾收集的三個性能指標: footprint, max pause time, throughput彷佛像CAP同樣不能同時知足。
在服務端更注重的是短停頓時間,也就是stop-the-world的時間,另一段時間內的總停頓時間也是一個衡量指標。
Mark-Sweep, Mark-Compact均須要和清理區域大小成比例的工做量,而Copying算法則須要通常是一半的空間用於存放每次copy的活對象。CMS的Initial Marking和Remarking兩個STW階段在Heap區愈來愈大的狀況下須要的時間越長,而且因爲內存碎片,須要壓縮的話也會形成較長停頓時間。因此須要一種高吞吐量的短暫停時間的收集器,而無論堆內存多大。
標記:從某一時刻t的對象圖快照開始標記;
而後標記程序和用戶程序同時執行;須要記錄t時刻用戶程序新增了哪些對象,併發標記過程當中這些對象都被認爲是存活對象,不會對它們進行標記
G1的實現方式
Region
避免長暫停時間,能夠考慮將堆分紅多個部分,一次收集其中一部分,這樣的方式又叫作增量收集(incremental collection), 分代收集也能夠當作一種特殊的增量收集。
G1收集器將堆內存劃分爲一系列大小相等的Region區域,Region大小在1MB到32MB在啓動時肯定,G1一樣也使用分代收集策略,將堆分爲Eden, Survivior, Old等,只不過是按照邏輯劃分的,每一個Region邏輯上屬於一個分代區域,而且在物理上不連續,當一個Old的Region收集完成後會變成新可用Region並可能成爲下一個Eden Region。當申請的對象大於Region大小的一半時,會被認爲是巨型對象,巨型對象默認會被放在Old區,但若是巨型對象知識短時間存在,則會被放入一個Humongous Region(巨型區域)中。當一個Region中是空的時,稱爲可用Region或新Region。
爲何要把堆切分爲region?
JVM指標:
1)JVM的吞吐量,
2)GC暫停時間,
3)垃圾對象回收時間;
4)程序佔用內存;
G1須要在這4個JVM指標中取捨!
爲何須要G1時說明白一點,須要一種無論堆內存多大狀況下都可以高吞吐量,短暫停時間的垃圾回收器!
因此G1選擇了吞吐量,暫停時間;捨棄了垃圾對象回收時間;程序佔用內存;
高吞吐量怎麼得到?提高年輕代大小,從程序開始運行到垃圾回收的的時間就變長了;
年輕代變大了,回收年輕代時間也變久了,STW時間也變大了!怎麼辦?
分治,把堆內存分爲等大小的內存區域Region;而後按照Region垃圾對象佔比多少選擇回收價值最高的region來進行GC;對於一次GC,不會回收整代內存(不連續也不必,由於涉及目標是縮短暫停時間),而是隻回收某些Region,回收的內存足以支撐程序運行便可;
這樣以回收某些region,回收屢次的方式,這樣維持了一個較高JVM的吞吐量,但減少了單次GC暫停時間;
如何找到全部的GC Roots對象?
大部分GC Roots對象都是Old對象嗎?
是
是否要掃描Old代的所有region?
老年代的全部對象都是根麼?這樣掃描下來會耗費大量的時間
Remember Set
G1引進了RSet的概念。它的全稱是Remembered Set,做用是跟蹤指向某個區域內的對象引用。這樣就能夠用RS以Region爲單位GC,而不用掃描整個堆GC。
通常狀況下,這個RSet實際上是一個Hash Table,Key是別的Region的起始地址,Value是一個集合,裏面的元素是Card Table的Index。
HashTable結構:
RememberSet數據結構:region起始地址(0~11)-region的CartTable
由於1個region對應1個CartTable,因此hashTable中每一個key只有1個item,item是CartTable,CartTable是字節數組,字節數組記錄了被外部region引用的Cart
RS主要存放:1)old到young的引用;2)old到old的引用
CMS中的RSet-Point Out
在CMS中,也有RSet的概念,在老年代中有一塊區域用來記錄指向新生代的引用。這是一種point-out,在進行Young GC時,掃描根時,僅僅須要掃描這一塊區域,而不須要掃描整個老年代。
G1中的RSet-Point In
其餘的region中對象引用我本身region中的對象,本身region中對象屬於哪一個卡表,記錄哪些卡表的索引
從region角度,有2種信息,分別是:我引用了誰?point-out,誰引用了我point-in?
從回收region的角度,確定是誰引用了我更有價值!!
因此在G1中,並無使用point-out,這是因爲G1分區過小,分區數量太多,若是是用point-out的話,會形成大量的掃描浪費,有些根本不須要GC的分區引用也掃描了。因而G1中使用point-in來解決。point-in的意思是哪些分區引用了當前分區中的對象。這樣,僅僅將這些對象當作根來掃描就避免了無效的掃描。因爲新生代有多個,那麼咱們須要在新生代之間記錄引用嗎?這是沒必要要的,緣由在於每次GC時,全部新生代都會被掃描,因此只須要記錄老年代到新生代之間的引用便可。
RSet for Refuib2中每一個紅格結構: region開始地址-該region的cardTable
Card Table卡表
須要注意的是,若是引用的對象不少,賦值器須要對每一個引用作處理,賦值器開銷會很大,爲了解決賦值器開銷這個問題,在G1 中又引入了另一個概念,卡表(Card Table)。一個Card Table將一個分區在邏輯上劃分爲固定大小的連續區域,每一個區域稱之爲卡。卡一般較小,介於128到512字節之間,用來存放對象?應該是。Card Table一般爲字節數組,由Card的索引(即數組下標)來標識每一個分區的空間地址。默認狀況下,每一個卡都未被引用。當一個地址空間被引用時,這個地址空間對應的數組索引的值被標記爲」0″,即標記爲髒被引用,此外RSet也將這個數組下標記錄下來。通常狀況下,這個RSet實際上是一個Hash Table,Key是別的Region的起始地址,Value是一個集合,裏面的元素是Card Table的Index。
經過Index(0~某數字)應該不能直接找到Card的內存地址,應該還有其餘映射關係。
GC Writebarrier
GC和用戶程序併發執行時,用戶線程何時修改跨region的引用??
維護remembered set須要mutator(用戶程序)線程在可能修改跨Region的引用的時候通知collector, 這種方式一般叫作write barrier(和GC中的Memory Barrier不一樣), 每一個線程都會有本身的remembered set log,至關於各自的修改的card的緩衝buffer,除此以外還有全局的buffer, mutator本身的remember set buffer滿了以後會放入到全局buffer中,而後建立一個新的buffer。
SATB的標記算法
Snaphot-At-The-Beginning簡稱SATB
對象的分配策略
對象的分配策略。它分爲3個階段:
1)TLAB(Thread Local Allocation Buffer)線程本地分配緩衝區
2)Eden區中分配
3)Humongous區分配
TLAB爲線程本地分配緩衝區,它的目的爲了使對象儘量快的分配出來。若是對象在一個共享的空間中分配,咱們須要採用一些同步機制來管理這些空間內的空閒空間指針。在Eden空間中,每個線程都有一個固定的分區用於分配對象,即一個TLAB。分配對象時,線程之間再也不須要進行任何的同步。
對TLAB空間中沒法分配的對象,JVM會嘗試在Eden空間中進行分配。若是Eden空間沒法容納該對象,就只能在老年代中進行分配空間。
G1的工做流程
Marking階段
階段目標
G1收集器的標記階段負責標記處存活的對象、而且計算各個Region的活躍度等。
標記算法
G1使用了一種Snaphot-At-The-Beginning簡稱SATB的標記算法, 記錄標記開始時的對象圖的快照,以後併發收集過程當中的新申請的對象都認爲是存活對象。
快照標記,和CMS同樣沒法解決浮動垃圾問題!
什麼時候標記
當堆使用比例超過InitiatingHeapOccupancyPercent後開始marking階段,使用SATB記錄marking開始階段的對象圖快照。
如何標記
G1使用bitmap標記哪些位置已經完成標記了,一個bitmap的bit表示8bytes, 咱們使用兩個marking bitmap,一個previous、一個next,
previous marking bitmap表示已經完成標記的部分,標記完成後會交換previous和next
標記階段分爲一下幾個步驟:
Initial Marking Phase初始標記
標記週期的最開始是清除next marking bitmap,是併發執行的。而後開始initial marking phase, 會暫停全部線程,標記出全部能夠直接從GC roots能夠直接到達的對象,這是在Young GC的暫停收集階段順帶進行的。
Root Region Scan Phase【add】
找出全部的GC Roots的Region, 而後從這些Region開始標記可到達的對象,是一個併發階段。
Concurrent Marking Phase
這個階段G1經過tracing找出整個堆全部的可到達的對象。這個階段是併發執行的;
用戶程序若是修改對象引用關係,則記錄修改到Remember set log中Rslog;
Final mark Phase
Final mark是一個STW階段,G1將全部的SATB buffer處理完成。就是處理Rslog,併到更新可到達對象關係Remember Set中;
Cleanup Phase
marking的最後一個階段,G1統計各個Region的活躍性,徹底沒有存活對象的Region直接放入空閒可用Region列表中,而後會找出mixed GC的Region候選列表。
G1的工做方式
和通常的分代式收集不一樣,G1中除了普通的Young GC,還有Mixed GC。
它仍然屬於分代收集器。
新生代的垃圾收集依然採用暫停全部應用線程的方式,將存活對象拷貝到老年代或者Survivor空間。
老年代也分紅不少區域,G1收集器經過將對象從一個區域複製到另一個區域,完成了清理工做。
這就意味着,在正常的處理過程當中,G1完成了堆的壓縮(至少是部分堆的壓縮),這樣也就不會有cms內存碎片問題的存在了。
Young GC
收集目標:young gen的Eden區
什麼時候觸發:
當Eden區域沒法申請新的對象時(滿了),就會進行Young GC,
收集過程:
Young GC將Eden和Survivor區域的Region(稱爲Collection Set, CSet)中的活對象Copy到一些新Region中(即新的Survivor),當對象的GC年齡達到閾值後會Copy到Old Region中。因爲採起的是Copying算法,因此就避免了內存碎片的問題,再也不須要單獨的壓縮。
應用程序是否暫停
Young GC過程當中,應用程序暫停。
最終Eden空間的數據爲空,GC中止工做,應用線程繼續執行。
Young GC 階段:
階段1:根掃描
靜態和本地對象被掃描
階段2:更新RS
處理dirty card隊列更新RS,point-in的引用更新
階段3:處理RS
檢測從年輕代指向年老代的對象
階段4:對象拷貝
拷貝存活的對象到survivor/old區域
階段5:處理引用隊列
軟引用,弱引用,虛引用處理
如何只GC young代呢?
Mixed GC
GC對象:young+old
什麼時候觸發
當old區Heap的對象佔總Heap的比例超過InitiatingHeapOccupancyPercent以後,就會開始ConcurentMarking, 完成了Concurrent Marking後,G1會從Young GC切換到Mixed GC,
GC步驟
全局併發標記(global concurrent marking)
拷貝存活對象(evacuation)
global concurrent marking的執行過程
在G1 GC中,它主要是爲Mixed GC提供標記服務的,並非一次GC過程的一個必須環節。global concurrent marking的執行過程分爲五個步驟:
1)初始標記(initial mark,STW)
在此階段,G1 GC 對根進行標記。該階段與常規的 (STW) 年輕代垃圾回收密切相關。
根區域掃描(root region scan)
G1 GC 在初始標記的存活區掃描對老年代的引用,並標記被引用的對象。該階段與應用程序(非 STW)同時運行,而且只有完成該階段後,才能開始下一次 STW 年輕代垃圾回收。
2)併發標記(Concurrent Marking)
G1 GC 在整個堆中查找可訪問的(存活的)對象。該階段與應用程序同時運行,能夠被 STW 年輕代垃圾回收中斷
3)最終標記(Remark,STW)
該階段是 STW 回收,幫助完成標記週期。G1 GC 清空 SATB 緩衝區,跟蹤未被訪問的存活對象,並執行引用處理。
4)清除垃圾(Cleanup,STW)
在這個最後階段,G1 GC 執行統計和 RSet 淨化的 STW 操做。在統計期間,G1 GC 會識別徹底空閒的區域和可供進行混合垃圾回收的區域。清理階段在將空白區域重置並返回到空閒列表時爲部分併發。
Full GC
和CMS同樣,G1的一些收集過程是和應用程序併發執行的,因此可能尚未回收完成,是因爲申請內存的速度比回收速度快,新的對象就佔滿了全部空間,在CMS中叫作Concurrent Mode Failure, 在G1中稱爲Allocation Failure,也會降級爲一個STW的fullgc。
Floating Garbage
G1使用一種Snapshot-At-The-Begining的方式記錄活對象,也就是那一時刻(整個堆concurrent marking開始的時候)的內存的Object graph, 可是在以後這裏面的對象可能會變成Garbage, 叫作floating garbage 只能等到下一次收集回收掉。
應用情景
1.在JDK7及之後,大內存(n~10G),多核系統
我的認爲更換GC或者進行調優只能算是系統的錦上添花,並不能做爲主要解決系統性能問題的關鍵,出現內存問題時,應當以修改應用代碼爲主、編寫清晰的GC友好的代碼,選擇與應用場景合適的收集器能夠提升系統的性能。
如今推薦從CMS更換到G1的一些狀況以下:
2.Java堆的50%以上都是活對象
3.對象的分配速率變化很大
4.因爲old gc或壓縮致使不可忍受的長時間的暫停
關注點
最小化STW時間
特色
1)可以實現軟停頓目標收集??軟停頓?,而且具備高吞吐量,具備可預測停頓時間
1)最小化STW時間,GC程序能夠和用戶程序併發執行;
2)分代收集,總體上看是標記-整理算法,局部(region)上看是複製算法;
3)沒有內存碎片
4)STW停頓可預測
CMS:設置了最小GC時間,實際GC時間依據JVM狀況而定
G1:能讓使用者指定M毫秒的時間片斷內,消耗在GC上的時間不超過N毫秒,很厲害!
爲何G1能夠預測停頓時間?
G1設計就是爲了不Full GC,因此G1跟蹤region裏垃圾狀況(回收所得到的空間大小以及回收所需時間的經驗值),在後臺維護一個優先列表,每次根據容許的收集時間,優先回收價值最大的Region,這也就是Garbage-First的由來!
參考文檔
https://liuzhengyang.github.io/2017/06/07/garbage-first-collector/
GC完整過程詳解
http://blog.jobbole.com/109170/ 深刻理解java G1收集器 【到】