Java虛擬機垃圾回收

一 、Java虛擬機垃圾回收基礎

一、 什麼是Java虛擬機垃圾回收

垃圾回收,或稱垃圾收集(Garbage Collection,GC)是指自動管理回收再也不被引用的內存數據。

在1960年誕生於MIT的Lisp語言首次使用了動態內存分配和垃圾收集技術,能夠實現垃圾回收的一個基本要求是語言是類型安全的,如今使用的包括Java、Perl、ML等。java

1-一、爲何須要了解垃圾回收

目前內存的動態分配與內存回收技術已經至關成熟,但爲何還須要去了解內存分配與GC呢?

一、當須要排查各類內存溢出、內存泄漏問題時;
程序員

二、當垃圾收集成爲系統達到更高併發量的瓶頸時;算法

咱們就須要對這些"自動化"技術實話必要的監控和調節;
數組

1-二、垃圾回收須要瞭解什麼

一、哪些內存須要回收?即如何判斷對象已經死亡;緩存

二、何時回收?即GC發生在何時?須要瞭解GC策略,與垃圾回收器實現有關;安全

三、如何回收?即須要瞭解垃圾回收算法,及算法的實現--垃圾回收器bash

二、判斷對象能夠回收

垃圾收集器對堆進行回收前,首先要肯定堆中的對象哪些還"存活",哪些已經"死去";

下面先來了解兩種判斷對象再也不被引用的算法,再來談談對象的引用,最後來看如何真正宣告一個對象死亡。服務器

2-一、引用計數算法(Recference Counting)

一、算法基本思路
給對象添加一個引用計數器,每當有一個地方引用它,計數器加1;
當引用失效,計數器值減1;
任什麼時候刻計數器值爲0,則認爲對象是再也不被使用的;
二、優勢
實現簡單,斷定高效,能夠很好解決大部分場景的問題,也有一些著名的應用案例;
三、缺點

(A)、很難解決對象之間相互循環引用的問題數據結構

ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;複製代碼

當兩個對象再也不被訪問時,由於相互引用對方,致使引用計數不爲0;
多線程

更復雜的循環數據結構,如圖:

(B)、而且開銷較大,頻繁且大量的引用變化,帶來大量的額外運算;

主流的JVM都沒有選用引用計數算法來管理內存;

2-二、可達性分析算法(Reachability Analysis)

也稱爲傳遞跟蹤算法;
主流的調用程序語言(Java、C#等)在主流的實現中,都是經過可達性分析來斷定對象是否存活的。
一、算法基本思路
經過一系列"GC Roots"對象做爲起始點,開始向下搜索;
搜索所走過和路徑稱爲引用鏈(Reference Chain);

當一個對象到GC Roots沒有任何引用鏈相連時(從GC Roots到這個對象不可達),則證實該對象是不可用的;

二、GC Roots對象
Java中,GC Roots對象包括:

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

(3)方法區中常量引用的對象;

(4)本地方法棧中JNI(Native方法)引用的對象;

主要在執行上下文中和全局性的引用;

三、優勢
更加精確和嚴謹,能夠分析出循環數據結構相互引用的狀況;
四、缺點
實現比較複雜;
須要分析大量數據,消耗大量時間;
分析過程須要GC停頓(引用關係不能發生變化),即 停頓全部Java執行線程 (稱爲 "Stop The World",是垃圾回收重點關注的問題 );

後面會針對HotSpot虛擬機實現的可達性分析算法進行介紹,看看是它如何解決這些缺點的

2-三、對象引用

java程序經過reference類型數據操做堆上的具體對象;
一、JVM層面的引用
reference類型是引用類型(Reference Types)的一種;

JVM規範規定reference類型來表示對某個對象的引用,能夠想象成相似於一個指向對象的指針;

對象的操做、傳遞和檢查都經過引用它的reference類型的數據進行操做;
二、Java語言層面的引用
(i)、JDK1.2前的引用定義

若是reference類型的數據中存儲的數值表明的是另一塊內存的起始地址,就稱這塊內存表明着一個引用;這種定義太過狹隘,沒法描述更多信息;

(ii)、JDK1.2後,對引用概念進行了擴充,將引用分爲:

(1)強引用(Strong Reference)

程序代碼廣泛存在的,相似"Object obj=new Object()";

只要強引用還存在,GC永遠不會回收被引用的對象;

(2)軟引用(Soft Reference)

用來描述還有用但並不是必需的對象;

直到內存空間不夠時(拋出OutOfMemoryError以前),纔會被垃圾回收;最經常使用於實現對內存敏感的緩存;SoftReference類實現;

(3)弱引用(Weak Reference)

用來描述非必需對象;只能生存到下一次垃圾回收以前,不管內存是否足夠;WeakReference類實現;

(4)虛引用(Phantom Reference)

也稱爲幽靈引用或幻影引用;徹底不會對其生存時間構成影響;惟一目的就是能在這個對象被回收時收到一個系統通知;PhantomRenference類實現;

2-四、判斷對象生存仍是死亡

要真正宣告一個對象死亡,至少要經歷兩次標記過程。
一、第一次標記
在可達性分析後發現到GC Roots沒有任何引用鏈相連時,被第一次標記;
而且進行一次篩選:此對象是否必要執行finalize()方法;

(A)沒有必要執行

沒有必要執行的狀況:

(1) 對象沒有覆蓋finalize()方法;

(2) finalize()方法已經被JVM調用過;

這兩種狀況就能夠認爲對象已死,能夠回收;

(B) 有必要執行

對有必要執行finalize()方法的對象,被放入F-Queue隊列中;

稍後在JVM自動創建、低優先級的Finalizer線程(可能多個線程)中觸發這個方法;

二、第二次標記

GC將對F-Queue隊列中的對象進行第二次小規模標記;

finalize()方法是對象逃脫死亡的最後一次機會:

(A)、若是對象在其finalize()方法中從新與引用鏈上任何一個對象創建關聯,第二次標記時會將其移出"即將回收"的集合;

(B)、若是對象沒有,也能夠認爲對象已死,能夠回收了;

一個對象的finalize()方法只會被系統自動調用一次,通過finalize()方法逃脫死亡的對象,第二次不會再調用;

2-五、finalize()方法

上面已經說到finalize()方法與垃圾回收第二次標記相關,下面瞭解下 在Java語言層面有哪些須要注意的

finalize()是Object類的一個方法,是Java剛誕生時爲了使C/C++程序員容易接受它所作出的一個妥協,但不要看成相似C/C++的析構函數;

由於它執行的時間不肯定,甚至是否被執行也不肯定(Java程序的不正常退出),並且運行代價高昂,沒法保證各個對象的調用順序(甚至有不一樣線程中調用);
若是須要"釋放資源",能夠定義顯式的終止方法,並在"try-catch-finally"的finally{}塊中保證及時調用,如File相關類的close()方法;
此外,finalize()方法主要有兩種用途:

一、充當"安全網"

當顯式的終止方法沒有調用時,在finalize()方法中發現後發出警告;

但要考慮是否值得付出這樣的代價;如FileInputStream、FileOutputStream、Timer和Connection類中都有這種應用;

二、與對象的本地對等體有關

本地對等體:普通對象調用本地方法(JNI)委託的本地對象;本地對等體不會被GC回收;

若是本地對等體不擁有關鍵資源,finalize()方法裏能夠回收它(如C/C++中malloc(),須要調用free());

若是有關鍵資源,必須顯式的終止方法;

通常狀況下,應儘可能避免使用它,甚至能夠忘掉它。

三、HotSpot虛擬機中對象可達性分析的實現

前面對可達性分析算法進行介紹,並看到了它在判斷對象存活與死亡的做用,下面看看是HotSpot虛擬機是如何實現可達性分析算法,如何解決相關缺點的。

3-一、可達性分析的問題

一、消耗大量時間

從前面可達性分析知道,GC Roots主要在全局性的引用(常量或靜態屬性)和執行上下文中(棧幀中的本地變量表);
要在這些大量的數據中,逐個檢查引用,會消耗不少時間;
二、GC停頓
可達性分析期間須要保證整個執行系統的一致性,對象的引用關係不能發生變化;
致使GC進行時必須停頓全部Java執行線程 (稱爲 "Stop The World" );
(幾乎不會發生停頓的CMS收集器中,枚舉根節點時也是必需要停頓的)
Stop The World:

是JVM在後臺自動發起和自動完成的;在用戶不可見的狀況下,把用戶正常的工做線程所有停掉;

3-二、枚舉根節點

枚舉根節點也就是查找GC Roots;
目前主流JVM都是準確式GC,能夠直接得知哪些地方存放着對象引用,因此執行系統停頓下來後,並不須要所有、逐個檢查徹底局性的和執行上下文中的引用位置;
在HotSpot中,是使用一組稱爲OopMap的數據結構來達到這個目的的;

在類加載時,計算對象內什麼偏移量上是什麼類型的數據;

在JIT編譯時,也會記錄棧和寄存器中的哪些位置是引用;

這樣GC掃描時就能夠直接得知這些信息;

3-三、安全點

一、安全點是什麼,爲何須要安全點
HotSpot在OopMap的幫助下能夠快速且準確的完成GC Roots枚舉,可是這有一個問題:        

運行中,很是多的指令都會致使引用關係變化;若是爲這些指令都生成對應的OopMap,須要的空間成本過高;

問題解決:

只在特定的位置記錄OopMap引用關係,這些位置稱爲安全點(Safepoint)

即程序執行時並不是全部地方都能停頓下來開始GC;

二、安全點的選定
不能太少,不然GC等待時間太長;也不能太多,不然GC過於頻繁,增大運行時負荷;
因此,基本上是以程序"是否具備讓程序長時間執行的特徵"爲標準選定;
"長時間執行"最明顯的特徵就是指令序列複用,如:方法調用、循環跳轉、循環的末尾、異常跳轉等;

只有具備這些功能的指令纔會產生Safepoint;

三、如何在安全點上停頓
對於Safepoint,如何在GC發生時讓全部線程(不包括JNI線程)運行到其所在最近的Safepoint上再停頓下來?
主要有兩種方案可選:

(A)搶先式中斷(Preemptive Suspension)

不須要線程主動配合,實現以下:

(1)在GC發生時,首先中斷全部線程;

(2)若是發現不在Safepoint上的線程,就恢復讓其運行到Safepoint上;

如今幾乎沒有JVM實現採用這種方式;

(B)主動式中斷(Voluntary Suspension)

(1)在GC發生時,不直接操做線程中斷,而是僅簡單設置一個標誌;

(2)讓各線程執行時主動去輪詢這個標誌,發現中斷標誌爲真時就本身中斷掛起;

而輪詢標誌的地方和Safepoint是重合的;

在JIT執行方式下:test指令是HotSpot生成的輪詢指令;一條test彙編指令便完成Safepoint輪詢和觸發線程中斷;

3-四、安全區域

一、爲何須要安全區域
對於上面的Safepoint還有一個問題:

程序不執行時沒有CPU時間(Sleep或Blocked狀態),沒法運行到Safepoint上再中斷掛起;

這就須要安全區域來解決;
二、什麼是安全區域(Safe Region)
指一段代碼片斷中,引用關係不會發生變化;
在這個區域中的任意地方開始GC都是安全的;
三、如何用安全區域解決問題
安全區域解決問題的思路:

(1)線程執行進入Safe Region,首先標識本身已經進入Safe Region;

(2)線程被喚醒離開Safe Region時,其須要檢查系統是否已經完成根節點枚舉(或整個GC);

若是已經完成,就繼續執行;不然必須等待,直到收到能夠安全離開Safe Region的信號通知,這樣就不會影響標記結果;

雖然HotSpot虛擬機中採用了這些方法來解決對象可達性分析的問題,但只是大大減小了這些問題影響,並不能徹底解決,如GC停頓"Stop The World"是垃圾回收重點關注的問題,後面介紹垃圾回收器時應注意:低GC停頓是其一個關注。


2、 回收算法

下面先來了解Java虛擬機垃圾回收的幾種常見算法:標記-清除算法、複製算法、標記-整理算法、分代收集算法、火車算法,介紹它們的算法思路,有什麼優勢和缺點,以及主要應用場景。

一、標記-清除算法

標記-清除(Mark-Sweep)算法是一種基礎的收集算法。
一、算法思路
"標記-清除"算法,分爲兩個階段:

(A)標記

首先標記出全部須要回收的對象;標記過程以下

(1)第一次標記

在可達性分析後發現對象到GC Roots沒有任何引用鏈相連時,被第一次標記;

而且進行一次篩選:此對象是否必要執行finalize()方法;

對有必要執行finalize()方法的對象,被放入F-Queue隊列中;

(2)第二次標記

GC將對F-Queue隊列中的對象進行第二次小規模標記;

在其finalize()方法中從新與引用鏈上任何一個對象創建關聯,第二次標記時會將其移出"即將回收"的集合;

對第一次被標記,且第二次還被標記(若是須要,但沒有移出"即將回收"的集合),就能夠認爲對象已死,能夠進行回收

(B)清除

兩次標記後,還在"即將回收"集合的對象將被統一回收;

執行過程以下圖:

二、優勢
基於最基礎的可達性分析算法,它是最基礎的收集算法;
然後續的收集算法都是基於這種思路並對其不足進行改進獲得的;
三、缺點
主要有兩個缺點:

(A)效率問題

標記和清除兩個過程的效率都不高;

(B)空間問題

標記清除後會產生大量不連續的內存碎片;這會致使分配大內存對象時,沒法找到足夠的連續內存;從而須要提早觸發另外一次垃圾收集動做;

四、應用場景

針對老年代的CMS收集器;

二、複製算法算法

"複製"(Copying)收集算法,爲了解決標記-清除算法的效率問題;
一、算法思路
(A)把內存劃分爲大小相等的兩塊,每次只使用其中一塊;
(B)當一塊內存用完了,就將還存活的對象複製到另外一塊上(然後使用這一塊);
(C)再把已使用過的那塊內存空間一次清理掉,然後重複步驟2;

執行過程以下圖:

二、優勢
這使得每次都是隻對整個半區進行內存回收;
內存分配時也不用考慮內存碎片等問題(可以使用"指針碰撞"的方式分配內存);
實現簡單,運行高效;

三、缺點

(A)空間浪費

可用內存縮減爲原來的一半,太過浪費(解決:能夠改良,不按1:1比例劃分);

(B)效率隨對象存活率升高而變低

當對象存活率較高時,須要進行較多複製操做,效率將會變低(解決:後面的標記-整理算法);

四、應用場景
如今商業JVM都採用這種算法(經過改良缺點1)來回收新生代;
如Serial收集器、ParNew收集器、Parallel Scavenge收集器、、G1(從局部看);
五、HotSpot虛擬機的改良算法

(A)弱代理論

分代垃圾收集基於弱代理論(weak generational hypothesis),具體描述以下:

(1)大多數分配了內存的對象並不會存活太長時間,在處於年輕代時就會死掉;

(2)不多有對象會從老年代變成年輕代;

其中IBM研究代表:新生代中98%的對象都是"朝生夕死";

因此並不須要按1:1比例來劃份內存(解決了缺點1);

(B)HotSpot虛擬機新生代內存佈局及算法

(1)將新生代內存分爲一塊較大的Eden空間和兩塊較小的Survivor空間;
(2)每次使用Eden和其中一塊Survivor;
(3)當回收時,將Eden和使用中的Survivor中還存活的對象一次性複製到另一塊Survivor;
(4)然後清理掉Eden和使用過的Survivor空間;
(5)後面就使用Eden和複製到的那一塊Survivor空間,重複步驟3;

默認Eden:Survivor=8:1,即每次可使用90%的空間,只有一塊Survivor的空間被浪費;

(C)分配擔保
若是另外一塊Survivor空間沒有足夠空間存放上一次新生代收集下來的存活對象時,這些對象將直接經過分配擔保機制(Handle Promotion)進入老年代;
分配擔保在之後講解垃圾收集器執行規則時再詳解;

三、標記-整理算法

"標記-整理"(Mark-Compact)算法是根據老年代的特色提出的。
1 算法思路

(1)標記

標記過程與"標記-清除"算法同樣;

(2)整理

但後續不是直接對可回收對象進行清理,而是讓全部存活的對象都向一端移動;

而後直接清理掉端邊界之外的內存;

執行過程以下圖:

2 優勢

(A)不會像複製算法,效率隨對象存活率升高而變低

老年代特色:

對象存活率高,沒有額外的空間能夠分配擔保;

因此老年代通常不能直接選用複製算法算法;

而選用標記-整理算法;

(B)不會像標記-清除算法,產生內存碎片

由於清除前,進行了整理,存活對象都集中到空間一側;

3 缺點
主要是效率問題:除像標記-清除算法的標記過程外,還多了須要整理的過程,效率更低;
4 應用場景
不少垃圾收集器採用這種算法來回收老年代;

如Serial Old收集器、G1(從總體看);

四、分代收集算法

"分代收集"(Generational Collection)算法結合不一樣的收集算法處理不一樣區域。
一、算法思路
基於前面說的弱代理論,其實並無什麼新的思想;
只是根據對象存活週期的不一樣將內存劃分爲幾塊;
這樣就能夠根據各個年代的特色採用最適當的收集算法;
通常把Java堆分爲新生代和老年代;

(A)新生代

每次垃圾收集都有大批對象死去,只有少許存活;因此可採用複製算法;

(B)老年代

對象存活率高,沒有額外的空間能夠分配擔保;使用"標記-清理"或"標記-整理"算法;

結合上面對新生代的內存劃分介紹和上篇文章對Java堆的介紹,能夠得出HotSpot虛擬機通常的年代內存劃分,以下圖:

二、優勢

能夠根據各個年代的特色採用最適當的收集算法;

三、缺點

仍然不能控制每次垃圾收集的時間;

四、應用場景
目前幾乎全部商業虛擬機的垃圾收集器都採用分代收集算法;

如HotSpot虛擬機中所有垃圾收集器:Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1(也保留);

五、火車算法

火車算法也稱列車算法,是一種更完全的分區域處理收集算法,是對分代收集算法的一個有力補充。
一、算法思路
在火車算法中,內存被分爲塊,多個塊組成一個集合。爲了形象化,一節車箱表明一個塊,一列火車表明一個集合,以下圖;
火車與車廂都按建立順序標號,每一個車箱大小相等,但每一個火車包含的車箱數不必定相等;
每節車廂有一個被記憶集合,而每輛火車的記憶集合是它全部車箱記憶集合的總和;
記憶集合由指向車廂中對象的引用組成,這些引用來自同一輛火車中序號較高的車廂中的對象,以及序號較高中的對象;
垃圾收集以車箱爲單位,總體算法流程以下:

(1)選擇標號最小的火車;

(2)若是火車的記憶集合是空的, 釋放整列火車並終止, 不然進行第三步操做;

(3)選擇火車中標號最小的車箱;

(4)對於車箱記憶集合的每一個元素:

若是它是一個被根引用引用的對象, 那麼, 將拷貝到一列新的火車中去;

若是是一個被其它火車的對象指向的對象, 那麼, 將它拷貝到這個指向它的火車中去.;

假設有一些對象已經被保留下來了, 那麼經過這些對象能夠觸及到的對象將會被拷貝到同一列火車中去;

若是一個對象被來自多個火車的對象引用, 那麼它能夠被拷貝到任意一個火車去;

這個步驟中, 有必要對受影響的引用集合進行相應地更新;

(5)、釋放車箱而且終止;

收集過程會刪除一些空車廂和空車,當須要的時候也會建立一些車廂和火車。

執行過程以下圖:

二、優勢
能夠在成熟對象空間提供限定時間的漸近收集;
而不須要每次都進行一個大區域的垃圾回收過程;
便可以控制垃圾回收的時間,在指定時間內進行一些小區域的回收;
三、缺點
實現較爲複雜,如採用相似的算法的G1收集器在JDK7才實現;
一些場景下可能性價比不高;
四、應用場景
JDK7後HotSpot虛擬機 G1收集器 採用 相似的算法 ,能創建可預測的停頓時間模型;


3、垃圾收集器

垃圾收集器是垃圾回收算法(標記-清除算法、複製算法、標記-整理算法、火車算法)的具體實現,不一樣商家、不一樣版本的JVM所提供的垃圾收集器可能會有很在差異,下面主要介紹HotSpot虛擬機中的垃圾收集器。

一、垃圾收集器概述

1-一、垃圾收集器組合

JDK7/8後,HotSpot虛擬機全部收集器及組合(連線),以下圖:

新生代收集器 :Serial、ParNew、Parallel Scavenge;
老年代收集器 :Serial Old、Parallel Old、CMS;
整堆收集器 :G1;

二、Serial收集器

Serial(串行)垃圾收集器是最基本、發展歷史最悠久的收集器;
JDK1.3.1前是HotSpot新生代收集的惟一選擇;
一、特色
針對新生代;
採用複製算法;
單線程收集;
進行垃圾收集時,必須暫停全部工做線程,直到完成;
即會"Stop The World";

Serial/Serial Old組合收集器運行示意圖以下:

三、ParNew收集器

ParNew垃圾收集器是 Serial收集器的多線程版本
一、特色
除了多線程外,其他的行爲、特色和Serial收集器同樣;
如Serial收集器可用控制參數、收集算法、Stop The World、內存分配規則、回收策略等;
兩個收集器共用了很多代碼;

ParNew/Serial Old組合收集器運行示意圖以下:

四、Parallel Scavenge收集器

Parallel Scavenge垃圾收集器由於與吞吐量關係密切,也稱爲 吞吐量收集器(Throughput Collector)
一、特色

(A)有一些特色與ParNew收集器類似

新生代收集器;

採用複製算法;

多線程收集;

(B)主要特色是:它的關注點與其餘收集器不一樣

CMS等收集器的關注點是儘量地縮短垃圾收集時用戶線程的停頓時間;

而Parallel Scavenge收集器的目標則是達一個可控制的吞吐量(Throughput)

關於吞吐量與收集器關注點說明詳見本節後面;

上面介紹的都是新生代收集器,接下來開始介紹老年代收集器;

五、Serial Old收集器

Serial Old是 Serial收集器的老年代版本

一、特色

針對老年代;

採用"標記-整理"算法(還有壓縮,Mark-Sweep-Compact);

單線程收集;

Serial/Serial Old收集器運行示意圖以下:

六、Parallel Old收集器

Parallel Old垃圾收集器是Parallel Scavenge收集器的老年代版本;
JDK1.6中才開始提供;
一、特色
針對老年代;
採用"標記-整理"算法;
多線程收集;

Parallel Scavenge/Parallel Old收集器運行示意圖以下:

七、CMS收集器

併發標記清理(Concurrent Mark Sweep,CMS)收集器也稱爲併發低停頓收集器(Concurrent Low Pause Collector)或低延遲(low-latency)垃圾收集器;
在前面ParNew收集器曾簡單介紹過其特色;
一、特色
針對老年代;
基於"標記-清除"算法(不進行壓縮操做,產生內存碎片);
以獲取最短回收停頓時間爲目標;
併發收集、低停頓;
須要更多的內存(看後面的缺點);
是HotSpot在JDK1.5推出的第一款真正意義上的併發(Concurrent)收集器;
第一次實現了讓垃圾收集線程與用戶線程(基本上)同時工做;

整個過程當中耗時最長的併發標記和併發清除均可以與用戶線程一塊兒工做;

因此整體上說,CMS收集器的內存回收過程與用戶線程一塊兒併發執行;

CMS收集器運行示意圖以下:

CMS收集器3個明顯的缺點

(A)對CPU資源很是敏感

(B)沒法處理浮動垃圾,可能出現"Concurrent Mode Failure"失敗

(C)產生大量內存碎片

八、G1收集器

G1( Garbage-First )是 JDK7-u4 才推出商用的收集器;

一、特色

(A)並行與併發

能充分利用多CPU、多核環境下的硬件優點;
能夠並行來縮短"Stop The World"停頓時間;
也能夠併發讓垃圾收集與用戶程序同時進行;
(B)分代收集,收集範圍包括新生代和老年代
能獨立管理整個GC堆(新生代和老年代),而不須要與其餘收集器搭配;
可以採用不一樣方式處理不一樣時期的對象;
雖然保留分代概念,但Java堆的內存佈局有很大差異;
將整個堆劃分爲多個大小相等的獨立區域(Region);
新生代和老年代再也不是物理隔離,它們都是一部分Region(不須要連續)的集合;

(C)結合多種垃圾收集算法,空間整合,不產生碎片

從總體看,是基於標記-整理算法;

從局部(兩個Region間)看,是基於複製算法;

這是一種相似火車算法的實現;都不會產生內存碎片,有利於長時間運行;

(D)可預測的停頓:低停頓的同時實現高吞吐量

G1除了追求低停頓處,還能創建可預測的停頓時間模型;

能夠明確指定M毫秒時間片內,垃圾收集消耗的時間不超過N毫秒;


G1收集器運做過程

不計算維護Remembered Set的操做,能夠分爲4個步驟(與CMS較爲類似)。

(A)初始標記(Initial Marking)

僅標記一下GC Roots能直接關聯到的對象;

且修改TAMS(Next Top at Mark Start),讓下一階段併發運行時,用戶程序能在正確可用的Region中建立新對象;須要"Stop The World",但速度很快;

(B)併發標記(Concurrent Marking)

進行GC Roots Tracing的過程;剛纔產生的集合中標記出存活對象;耗時較長,但應用程序也在運行;並不能保證能夠標記出全部的存活對象;

(C)最終標記(Final Marking)

爲了修正併發標記期間因用戶程序繼續運做而致使標記變更的那一部分對象的標記記錄;

上一階段對象的變化記錄在線程的Remembered Set Log;

這裏把Remembered Set Log合併到Remembered Set中;

須要"Stop The World",且停頓時間比初始標記稍長,但遠比並發標記短;

採用多線程並行執行來提高效率;

(D)篩選回收(Live Data Counting and Evacuation)

首先排序各個Region的回收價值和成本;

而後根據用戶指望的GC停頓時間來制定回收計劃;

最後按計劃回收一些價值高的Region中垃圾對象;

回收時採用"複製"算法,從一個或多個Region複製存活對象到堆上的另外一個空的Region,而且在此過程當中壓縮和釋放內存;

能夠併發進行,下降停頓時間,並增長吞吐量;


4、內存分配與回收策略 方法區垃圾回收 以及 JVM垃圾回收的調優方法


一、內存分配

在堆上分配(JIT編譯優化後可能在棧上分配),主要在新生代的Eden區中分配;
若是啓用了本地線程分配緩衝,將線程優先在TLAB上分配;

少數狀況下,可能直接分配在老年代中。

分配的細節取決於當前使用哪一種垃圾收集器組合,以及JVM中內存相關參數設置。

接下來將會講解幾條最廣泛的內存分配規則。


1-一、對象優先在Eden分配

默認Eden:Survivor=8:1,即每次可使用90%的空間,只有一塊Survivor的空間被浪費;

大多數狀況下,對象在新生代Eden區中分配;

當Eden區沒有足夠空間進行分配時,JVM將發起一次Minor GC(新生代GC);

Minor GC時,若是發現存活的對象沒法所有放入Survivor空間,只好經過分配擔保機制提早轉移到老年代。

1-二、大對象直接進入老年代

大對象指須要大量連續內存空間的Java對象,如,很長的字符串、數組;

常常出現大對象容易致使內存還有很多空間就提早觸發GC,以獲取足夠的連續空間來存放它們,因此應該儘可能避免使用建立大對象;

1-三、長期存活的對象將進入老年代

JVM給每一個對象定義一個對象年齡計數器,其計算流程以下:

在Eden中分配的對象,經Minor GC後還存活,就複製移動到Survivor區,年齡爲1;

然後每經一次Minor GC後還存活,在Survivor區複製移動一次,年齡就增長1歲;

若是年齡達到必定程度,就晉升到老年代中;

1-四、動態對象年齡斷定

若是在Survivor空間中相同年齡的全部對象大小總和大於Survivor空間的一半,大於或等於該年齡的對象就能夠直接進入老年代

1-五、空間分配擔保


二、回收方法區

2-一、方法區(永久代)的主要回收對象

一、廢棄常量
與回收Java堆中對象很是相似;
二、無用的類
同時知足下面3個條件才能算"無用的類":

(1)該類全部實例都已經被回收(即Java椎中不存在該類的任何實例);

(2)加載該類的ClassLoader已經被回收,也即經過引導程序加載器加載的類不能被回收;

(3)該類對應的java.lang.Class對象沒有在任何地方被引用,沒法在任何地方經過反射訪問該類的方法;

2-二、須要注意方法區回收的應用

大量使用反射、動態代理、常常動態生成大量類的應用 ,要注意類的回收;
如運行時動態生成類的應用:

一、CGLib在Spring、Hibernate等框架中對類進行加強時會使用;

二、VM的動態語言也會動態建立類來實現語言的動態性;

三、另外,JSP(第一次使用編譯爲Java類)、基於OSGi頻繁自定義ClassLoader的應用(同一個類文件,不一樣加載器加載視爲不一樣類)等;

2-三、HotSpot虛擬機的相關調整

一、在JDK7中
使用永久代(Permanent Generation)實現方法區,這樣就能夠不用專門實現方法區的內存管理,但這容易引發內存溢出問題;
有規劃放棄永久代而改用Native Memory來實現方法區;
再也不在Java堆的永久代中生成中分配字符串常量池,而是在Java堆其餘的主要部分(年輕代和老年代)中分配;
二、在JDK8中
永久代已被刪除,類元數據(Class Metadata)存儲空間在本地內存中分配,並用顯式管理元數據的空間:

從OS請求空間,而後分紅塊;

類加載器從它的塊中分配元數據的空間(一個塊被綁定到一個特定的類加載器);

當爲類加載器卸載類時,它的塊被回收再使用或返回到操做系統;

元數據使用由mmap分配的空間,而不是由malloc分配的空間;

三、相關參數
"-XX:MaxMetaspaceSize" (JDK8):指定類元數據區的最大內存大小;
"-XX:MetaspaceSize" (JDK8):指定類元數據區的內存閾值--超過將觸發垃圾回收; 
"-Xnolassgc" :控制是否對類進行回收;
"-verbose:class"、"-XX:TraceClassLoading"、"-XX:TraceClassUnloading":查看類加載和卸載信息;


三、JVM垃圾回收的調優方法

內存回收與垃圾收集器是影響系統性能、併發能力的主要因素之一,通常都須要進行一些手動的測試、調整優化;

下面介紹的是一些思路,並不是是具體的參數設置。

3-一、明確指望的目標(關注點)

首先應該明確咱們的應用程序調整垃圾回收指望的目標(關注點)是什麼?
在前文曾介紹過一般有這些關注點:

(1)停頓時間

GC停頓時間越短就適合須要與用戶交互的程序,良好的響應速度能提高用戶體驗;

與用戶交互較多的場景,以給用戶帶來較好的體驗;

如常見WEB、B/S系統的服務器上的應用;

(2)吞吐量

吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間);

高吞吐量能夠高效率地利用CPU時間,儘快完成運算的任務,主要適合在後臺計算而不須要太多交互的任務;

應用程序運行在具備多個CPU上,對暫停時間沒有特別高的要求;

程序主要在後臺進行計算,而不須要與用戶進行太多交互;

例如,那些執行批量處理、訂單處理、工資支付、科學計算的應用程序;

(3)覆蓋區(Footprint)

在達到前面兩個目標的狀況下,儘可能減小堆的內存空間,以得到更好的空間局部性;

能夠減小到不知足前兩個目標爲止,而後再解決未知足的目標;

若是是動態收縮的堆設置,堆的大小將隨着垃圾收集器試圖知足競爭目標而振盪;

總結就是:低停頓、高吞吐量、少用內存資源;

通常這些目標都相互影響的,增大堆內存得到高吞吐量但會增加停頓時間,反之亦然,有時需折中處理。

3-二、JVM自適應調整(Ergonomics)

JVM有自適應選擇、調整相關設置的功能;

通常都會先根據平臺性能來選擇好垃圾收集器,以及設置好其參數;

在運行中,一些收集器還會收集監控信息來自動地、動態的調整垃圾回收策略;

因此當咱們不知道何如選擇收集器和調整時,應該首先讓JVM自適應調整;
而後經過輸出GC日誌進行分析,看能不能知足明確指望的目標(第一步);

若是不能知足,或者經過打印設置的參數信息,發現能夠有更好的調優時,能夠進行手動指定參數進行設置,並測試;

3-三、實踐調優:選擇垃圾收集器,並進行相關設置

須要明確一個觀點:

沒有最好的收集器,更沒有萬能的收集;

選擇的只能是對具體應用最適合的收集器;

咱們知道HotSpot有這些組合能夠搭配使用:

Serial/Serial Old、Serial/CMS、ParNew/Serial Old、ParNew/CMS、Parallel Scavenge/Serial Old、Parallel Scavenge/Parallel Old、G1;

到實踐調優階段,那必需要了解每一個具體收集器的行爲特色、優點和劣勢、調節參數等(請參考前面的文章內容);

而後根據明確指望的目標,選擇具體應用最適合的收集器;

當選擇使用某種並行垃圾收集器時,應該指按期望的具體目標而不是指定堆的大小;

讓垃圾收集器自動地、動態的調整堆的大小來知足指望的行爲;

即堆的大小將隨着垃圾收集器試圖知足競爭目標而振盪;

固然有時發現問題,堆的大小、劃分也是須要進行一些調整的,通常規則:

除非應用程序沒法接受長時間的暫停,不然能夠將堆調的儘量大一些;

除非發現問題的緣由在於老年代的垃圾收集或應用程序暫停次數過多,不然你應該將堆的較大部分分給年輕代;

等等… 

例如,使用Parallel Scavenge/Parallel Old組合,這是一種值得推薦的方式:

一、只需設置好內存數據大小(如"-Xmx"設置最大堆);

二、而後使用"-XX:MaxGCPauseMillis"或"-XX:GCTimeRatio"給JVM設置一個優化目標;

三、那些具體細節參數的調節就由JVM自適應完成;

設置調整後,應該經過在產生環境下進行不斷測試,來分析是否達到咱們的目標;


引用:

一、blog.csdn.net/tjiyu/artic…

二、blog.csdn.net/tjiyu/artic…

三、blog.csdn.net/tjiyu/artic…

四、blog.csdn.net/tjiyu/artic…

相關文章
相關標籤/搜索