垃圾標記階段
- 對象存活判斷:在堆裏存放着幾乎全部的Java對象實例,在GC執行垃圾回收以前,首先須要區分出內存中哪些是存活對象,哪些是已經死亡的對象。只有被標記爲己經死亡的對象,GC纔會在執行垃圾回收時,釋放掉其所佔用的內存空間,所以這個過程咱們能夠稱爲垃圾標記階段。
- 那麼在JVM中到底是如何標記一個死亡對象呢?簡單來講,當一個對象已經再也不被任何的存活對象繼續引用時,就能夠宣判爲已經死亡。
- 判斷對象存活通常有兩種方式:引用計數算法和可達性分析算法。
引用計數法 (java沒有采用)
- 引用計數算法(Reference Counting)比較簡單,對每一個對象保存一個整型 的引用計數器屬性。用於記錄對象被引用的狀況。
- 對於一個對象A,只要有任何一個對象引用了A,則A的引用計數器就加1;當引用失效時,引用計數器就減1。只要對象A的引用計數器的值爲0,即表示對象A不可能再被使用,可進行回收。
- 優勢:實現簡單,垃圾對象便於辨識;斷定效率高,回收沒有延遲性。
- 缺點:
- ➢它須要單獨的字段存儲計數器,這樣的作法增長了存儲空間的開銷。
- ➢每次賦值都須要更新計數器,伴隨着加法和減法操做,這增長了時間開銷。
- ➢引用計數器有一個嚴重的問題,即沒法處理循環引用的狀況。這是一 條致命缺陷,致使在Java的垃圾回收器中沒有使用這類算法。
循環引用致使的內存泄漏狀況:java
![](http://static.javashuo.com/static/loading.gif)
/** * -XX:+PrintGCDetails * 證實:java使用的不是引用計數算法 */ public class RefCountGC { //這個成員屬性惟一的做用就是佔用一點內存 private byte[] bigSize = new byte[5 * 1024 * 1024];//5MB,僅僅意味着只要建立我這個RefCountGC對象實例,就會佔用5M的堆空間 Object reference = null; public static void main(String[] args) { RefCountGC obj1 = new RefCountGC(); RefCountGC obj2 = new RefCountGC(); obj1.reference = obj2; obj2.reference = obj1; obj1 = null; obj2 = null; //顯式的執行垃圾回收行爲 //這裏發生GC,obj1和obj2可否被回收?能,證實Java沒有采用引用計數算法 System.gc(); try { Thread.sleep(1000000); } catch (InterruptedException e) { e.printStackTrace(); } } }
引用計數法的弊端:即便把obj1 一reference和obj2 一reference(棧引用)置null。 則在Java堆當中的兩塊內存依然保持着互相引用,沒法回收。算法
可是把obj1 一reference和obj2 一reference置爲null以後,若是顯示的去調用System.gc();這裏將會發生GC,回收堆中bj1和obj2實體。從而證實Jlava沒有采用引用計數算法 .數據庫
![](http://static.javashuo.com/static/loading.gif)
可達性分析算法
也叫根搜索算法或追蹤性垃圾收集緩存
- 相對於引用計數算法而言,可達性分析算法不只一樣具有實現簡單和執行高 效等特色,更重要的是該算法能夠有效地解決在引用計數算法中循環引用的問題,防止內存泄漏的發生。
- 相較於引用計數算法,這裏的可達性分析就是Java、C#選擇的。這種類型的垃圾收集一般也叫做追蹤性垃圾收集(Tracing GarbageCollection)。
- 所謂"GC Roots"根集合就是一組必須活躍的引用。
- ➢可達性分析算法是以根對象集合(GCRoots)爲起始點,按照從上至下的方式搜索被根對象集合所鏈接的目標對象是否可達。
- ➢使用可達性分析算法後,內存中的存活對象都會被根對象集合直接或間接鏈接着,搜索所走過的路徑稱爲引用鏈(Reference Chain)
- ➢若是目標對象沒有任何引用鏈相連,則是不可達的,就意味着該對象己經死亡,能夠標記爲垃圾對象。
![](http://static.javashuo.com/static/loading.gif)
GC Rootseclipse
在Java語言中,GC Roots對象包括如下幾類元素:jvm
1.虛擬機棧中引用的對象:➢好比:各個線程當中被調用的方法中使用到的參數、局部變量等。ide
2.本地方法棧內JNI(一般說的本地方法)引用的對象函數
3.方法區中類靜態屬性引用的對象➢好比:Java類的引用類型靜態變量工具
Class Dog { private static Object tail; }
4.方法區中常量引用的對象:➢好比:字符串常量池(string Table) 裏的引用性能
Class Dog { private final Object tail; }
5.全部被同步鎖synchroni zed持有的對象
6.Java虛擬機內部的引用:➢基本數據類型對應的Class對象,一些常駐的異常對象(如: NullPointerException、OutOfMemoryError) ,系統類加載器。
7.反映java虛擬機內部狀況的JMXBean、JVMTI中註冊的回調、本地代碼緩存等.
8.除了這些固定的GCRoots集合之外,根據用戶所選用的垃圾收集器以及當 前回收的內存區域不一樣,還能夠有其餘對象「臨時性」地加入,共同構成完整GC Roots集合。好比:分代收集和局部回收(Partial GC)。➢若是隻針對Java堆中的某一塊區域進行垃圾回收(好比:典型的只針 對新生代),必須考慮到內存區域是虛擬機本身的實現細節,更不是孤立封閉的,這個區域的對象徹底有可能被其餘區域的對象所引用,這時候就須要一.並將關聯的區域對象也加入GC Roots集合中去考慮,才能保證可達性分析的準確性。
9.小技巧:因爲Root採用棧方式存放變量和指針,因此若是一個指針,它保存了堆內存裏面的對象,可是本身又不存放在堆內存裏面,那它就是一個Root
![](http://static.javashuo.com/static/loading.gif)
注意
- 若是要使用可達性分析算法來判斷內存是否可回收,那麼分析工做必須在 一個能保障一致性的快照中進行。這點不知足的話分析結果的準確性就沒法保證。
- ➢即便是號稱(幾乎)不會發生停頓的CMS收集器中,枚舉根節點時也是必需要停頓的。這點也是致使GC進行時必須「StopTheWorld"的一個重要緣由。
對象的finalization機制
- Java語言提供了對象終止(finalization)機制來容許開發人員提供對象被銷燬以前的自定義處理邏輯。
- 當垃圾回收器發現沒有引用指向一個對象,即:垃圾回收此對象以前,總會先調用這個對象的finalize()方法。
- finalize()方法容許在子類中被重寫,用於在對象被回收時進行資源釋放。一般在這個方法中進行一些資源釋放和清理的工做,好比關閉文件、套接字和數據庫鏈接等。
- 應該交給垃圾回收機制調用,永遠不要主動調用某個對象的finalize ()方法。理由包括下面三點:
- 意思是若是你重寫了finalize(),finalize()會在一個低優先級的線程中等待自動執行(前提是發生了GC),不須要人爲主動的去調用。
- ➢在finalize() 時可能會致使對象復活。
- ➢finalize()方法的執行時間是沒有保障的,它徹底由Gc線程決定,極端狀況下,若不發生GC,則finalize() 方法將沒有執行機會。
- ➢一個糟糕的finalize ()會嚴重影響GC的性能。
- 從功能上來講,finalize()方法與C++ 中的析構函數比較類似,可是Java採用的是基於垃圾回收器的自動內存管理機制,因此finalize()方法在本質,上不一樣於C++ 中的析構函數。
對象是否"死亡"
- 因爲finalize ()方法的存在,虛擬機中的對象通常處於三種可能的狀 若是從全部的根節點都沒法訪問到某個對象,說明對象己經再也不使用了。通常來講,此對象須要被回收。但事實上,也並不是是「非死不可」的,這時候它們暫時處於「緩刑」階段。一個沒法觸及的對象有可能在某一個條件下「復活」本身,若是這樣,那麼對它的回收就是不合理的,爲此,定義虛擬機中的對象可能的三種狀態。以下:
- ➢可觸及的:從根節點開始,能夠到達這個對象。
- ➢可復活的:對象的全部引用都被釋放,可是對象有可能在finalize()中復活。
- ➢不可觸及的:對象的finalize()被調用,而且沒有復活,那麼就會進入不可觸及狀態。不可觸及的對象不可能被複活,由於finalize() 只會被調用1次。
- 以上3種狀態中,是因爲finalize()方法的存在,進行的區分。只有在對象不可觸及時才能夠被回收。 斷定是否能夠回收具體過程 斷定一個對象objA是否可回收,至少要經歷兩次標記過程:
過程1.若是對象objA到GC Roots沒有引用鏈,則進行第一 次標記。
過程2.進行篩選,判斷此對象是否有必要執行finalize()方法
- ①若是對 象objA沒有重寫finalize()方法,或者finalize ()方法已經被虛擬機調用過,則虛擬機視爲「沒有必要執行」,objA被斷定爲不可觸及的。
- ②若是對象objA重寫了finalize()方法,且還未執行過,那麼objA會被插入到F一Queue隊列中,由一個虛擬機自動建立的、低優先級的Finalizer線程觸發其finalize()方法執行。
- ③finalize()方法是對象逃脫死亡的最後機會,稍後Gc會對F一Queue隊列中的對象進行第二次標記。若是objA在finalize()方法中與引用鏈上的任何一個對象創建了聯繫,那麼在第二次標記時,objA會被移出「即將回收」集合。以後,對象會再次出現沒有引用存在的狀況。在這個狀況下,finalize方法不會被再次調用,對象會直接變成不可觸及的狀態,也就是說,一個對象的finalize方法只會被調用一次。
代碼測試對象可復活:
/** * 測試Object類中finalize()方法,即對象的finalization機制。 * */ public class CanReliveObj { public static CanReliveObj obj;//類變量,屬於 GC Root //此方法只能被調用一次 @Override protected void finalize() throws Throwable { super.finalize(); System.out.println("調用當前類重寫的finalize()方法"); obj = this;//當前待回收的對象在finalize()方法中與引用鏈上的一個對象obj創建了聯繫 } public static void main(String[] args) { try { obj = new CanReliveObj(); // 對象第一次成功拯救本身 obj = null; System.gc();//調用垃圾回收器 System.out.println("第1次 gc"); // 由於Finalizer線程優先級很低,暫停2秒,以等待它 Thread.sleep(2000); if (obj == null) { System.out.println("obj is dead"); } else { System.out.println("obj is still alive"); } System.out.println("第2次 gc"); // 下面這段代碼與上面的徹底相同,可是此次自救卻失敗了 obj = null; System.gc(); // 由於Finalizer線程優先級很低,暫停2秒,以等待它 Thread.sleep(2000); if (obj == null) { System.out.println("obj is dead"); } else { System.out.println("obj is still alive"); } } catch (InterruptedException e) { e.printStackTrace(); } } }
控制檯輸出:
第1次 gc 調用當前類重寫的finalize()方法 obj is still alive 第2次 gc obj is dead
使用(MAT與JProfiler)工具分析GCRoots
MAT是基於Eclipse開發的,是一款免費的性能分析工具。
能夠在http://www.eclipse org/mat/下載並使用MAT。
獲取dump文件
方式1: 命令行使用jmap
- jps
- jmap -dump:format=b,live,file=test1.bin {進程id}
方式2:使用JVisualVM導出
- 捕獲的heap dump文件是一個臨時文件,關閉JVisua1VM後自動刪除,若要保留,須要將其另存爲文件。
- 可經過如下方法捕獲heap dump:
- ➢在左側「Application」(應用程序)子窗口中右擊相應的應用程序,選擇Heap Dump(堆Dump)。
- ➢在Monitor (監視)子標籤頁中點擊Heap Dump (堆Dump)按鈕。
- 本地應用程序的Heap dumps做爲應用程序標籤頁的一個子標籤頁打開。同時, heap dump在左側的Application (應用程序)欄中對應一個含有時間戳的節點。右擊這個節點選擇save as (另存爲)便可將heap dump保存到本地。
![](http://static.javashuo.com/static/loading.gif)
GC Roots分析
public class GCRootsTest { public static void main(String[] args) { List<Object> numList = new ArrayList<>(); Date birth = new Date(); for (int i = 0; i < 100; i++) { numList.add(String.valueOf(i)); try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("數據添加完畢,請操做:"); new Scanner(System.in).next(); numList = null; birth = null; System.out.println("numList、birth已置空,請操做:"); new Scanner(System.in).next(); System.out.println("結束"); } }
使用MAT查看GC Roots:
![](http://static.javashuo.com/static/loading.gif)
使用jProfiler進行GC溯源:
![](http://static.javashuo.com/static/loading.gif)
使用Jprofiler分析OOM:
/** * -Xms8m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError * */ public class HeapOOM { byte[] buffer = new byte[1 * 1024 * 1024];//1MB public static void main(String[] args) { ArrayList<HeapOOM> list = new ArrayList<>(); int count = 0; try{ while(true){ list.add(new HeapOOM()); count++; } }catch (Throwable e){ System.out.println("count = " + count); e.printStackTrace(); } } }
控制檯輸出:
java.lang.OutOfMemoryError: Java heap space Dumping heap to java_pid45386.hprof ... Heap dump file created [7390812 bytes in 0.019 secs] count = 6 java.lang.OutOfMemoryError: Java heap space at com.dsh.jvm.gc.algorithm.HeapOOM.<init>(HeapOOM.java:12) at com.dsh.jvm.gc.algorithm.HeapOOM.main(HeapOOM.java:20)
對應count=6
![](http://static.javashuo.com/static/loading.gif)
出現OOM的代碼
![](http://static.javashuo.com/static/loading.gif)
垃圾清除階段
當成功區分出內存中存活對象和死亡對象後,GC接下來的任務就是執行垃圾回收,釋放掉無用對象所佔用的內存空間,以便有足夠的可用內存空間爲新對象分配內存。
目前在JVM中比較常見的三種垃圾收集算法是標記一清除算法( Mark一Sweep)、複製算法(Copying)、標記一壓縮算法(Mark一Compact)。
標記-清除算法
標記一清除算法(Mark一Sweep)是一種很是基礎和常見的垃圾收集算法,該算法被J . McCarthy等人在1960年提出並並應用於Lisp語言。
當堆中的有效內存空間(available memory) 被耗盡的時候,就會中止整個程序(也被稱爲stop the world),而後進行兩項工做,第一項則是標記,第二項則是清除。
- 標記: Collector從引用根節點開始遍歷,標記全部被引用的對象。通常是在對象的Header中記錄爲可達對象。
- 清除: Collector對堆 內存從頭至尾進行線性的遍歷,若是發現某個對象在其Header中沒有標記爲可達對象,則將其回收。
![](http://static.javashuo.com/static/loading.gif)
標記清除算法的缺點
- ➢效率不算高
- ➢在進行Gc的時候,須要中止整個應用程序,致使用戶體驗差。
- ➢這種方式清理出來的空閒內存是不連續的,產生內存碎片。須要維護一個空閒列表。
注意:何爲清除?
- 這裏所謂的清除並非真的置空,而是把須要清除的對象地址保存在空閒 的地址列表裏。下次有新對象須要加載時,判斷垃圾的位置空間是否夠,若是夠,就存放。
複製算法
爲了解決標記一清除算法在垃圾收集效率方面的缺陷,M.L.Minsky於1963年發表了著名的論文,「 使用雙存儲區的Li sp語言垃圾收集器CALISP Garbage Collector Algorithm Using SerialSecondary Storage )」。M.L. Minsky在該論文中描述的算法被人們稱爲複製(Copying)算法,它也被M. L.Minsky本人成功地引入到了Lisp語言的一個實現版本中。
![](http://static.javashuo.com/static/loading.gif)
複製算法優勢:
- 沒有標記和清除過程,實現簡單,運行高效
- 複製過去之後保證空間的連續性,不會出現「碎片」問題。
缺點:
- 此算法的缺點也是很明顯的,就是須要兩倍的內存空間。
- 對於G1這種分拆成爲大量region的GC,複製而不是移動,意味着GC須要維護region之間對象引用關係,無論是內存佔用或者時間開銷也不小。
特別的 若是系統中的垃圾對象不少,複製算法不會很理想,複製算法須要複製的存活對象數量並不會太大,或者說很是低才行。
應用場景:
在新生代,對常規應用的垃圾回收,一次一般能夠回收708一 99的內存空間。回收性價比很高。因此如今的商業虛擬機都是用這種收集算法回收新生代。
![](http://static.javashuo.com/static/loading.gif)
標記-壓縮(整理,Mark-Compact)算法
複製算法的高效性是創建在存活對象少、垃圾對象多的前提下的。這種狀況在新生代常常發生。
可是在老年代,更常見的狀況是大部分對象都是存活對象。若是依然使用複製算法,因爲存活對象較多,複製的成本也將很高。
所以,基於老年代垃圾回收的特性,須要使用其餘的算法。標記一清除算法的確能夠應用在老年代中,可是該算法不只執行效率低下,並且在執行完內存回收後還會產生內存碎片,因此JVM的設計者須要在此基礎之上進行改進。標記一壓縮(Mark一Compact) 算法由此誕生。1970年先後,G. L. Steele 、C. J. Chene和D.S. Wise 等研究者發佈標記一壓縮算法。在許多現代的垃圾收集器中,人們都使用了標記一壓縮算法或其改進版本。
標記壓縮算法執行過程:
-
第一階段和標記一清除算法同樣,從根節點開始標記全部被引用對象.
-
第二階段將全部的存活對象壓縮到內存的一端,按順序排放。
-
以後,清理邊界外全部的空間
![](http://static.javashuo.com/static/loading.gif)
標記一壓縮算法的最終效果等同於標記一清除算法執行完成後,再進行一次內存碎片整理,所以,也能夠把它稱爲標記一清除一壓縮(Mark一 Sweep一Compact)算法。
兩者的本質差別在於標記一清除算法是一種非移動式的回收算法,標記一壓.縮是移動式的。是否移動回收後的存活對象是一項優缺點並存的風險決策。
能夠看到,標記的存活對象將會被整理,按照內存地址依次排列,而未被標記的內存會被清理掉。
如此一來,當咱們須要給新對象分配內存時,JVM只須要持有一個內存的起始地址便可,這比維護一個空閒列表顯然少了許多開銷。
指針碰撞(Bump the Pointer )
若是內存空間以規整和有序的方式分佈,即已用和未用的內存都各自一邊,彼此之間維繫着一個記錄下一次分配起始點的標記指針,當爲新對象分配內存時,只須要經過修改指針的偏移量將新對象分配在第一個空閒內存位置上,這種分配方式就叫作指針碰撞(Bump the Pointer) 。
標記壓縮算法優勢
- 消除了標記一清除算法當中,內存區域分散的缺點。
- 咱們須要給新對象分配內存時,JVM只 須要持有一個內存的起始地址便可。
- 消除了複製算法當中,內存減半的高額代價。
標記壓縮算法缺點
- 從效率.上來講,標記一整理算法要低於複製算法。
- 移動對象的同時,若是對象被其餘對象引用,則還須要調整引用的地址。· 移動過程當中,須要全程暫停用戶應用程序。即: STW
以上三種垃圾回收算法對比
- 速度上來講,複製算法是當之無愧的老大,可是卻浪費了太多內存。
- 而爲了儘可能兼顧上面提到的三個指標,標記一整理算法相對來講更平滑一些,可是效率/速度上不盡如人意,它比複製算法多了一個標記的階段,比標記一清除多了一個整理內存的階段。
分代收集算法
難道就沒有一種最優的算法麼?沒有最好的算法,只有更合適的算法。
前面全部這些算法中,並無一種算法能夠徹底替代其餘算法,它們都具備本身獨特的優點和特色。分代收集算法應運而生。、
分代收集算法,是基於這樣一個事實:不一樣的對象的生命週期是不同的。所以,不一樣生命週期的對象能夠採起不一樣的收集方式,以便提升回收效率。通常是把Java堆分爲新生代和老年代,這樣就能夠根據各個年代的特色使用不一樣的回收算法,以提升垃圾回收的效率。
在Java程序運行的過程當中,會產生大量的對象,其中有些對象是與業務信息相關,好比Http請求中的Session對象、線程、Socket鏈接, 這類對象跟業務直接掛鉤,所以生命週期比較長。
可是還有一些對象,主要是程序運行過程當中生成的臨時變量,這些對象生命週期會比較短,好比: String對象, 因爲其不變類的特性,系統會產生大量的這些對象,有些對象甚至只用一次便可回收。
目前幾乎全部的GC都是採用分代收集(Generational Collecting) 算法執行垃圾回收的。在HotSpot中,基於分代的概念,GC所使用的內存回收算法必須結合年輕代和老年代各自的特色。
-
年輕代(Young Gen)
- 年輕代特色:區域相對老年代較小,對象生命週期短、存活率低,回收頻繁。
- 這種狀況複製算法的回收整理,速度是最快的。複製算法的效率只和當前存活對象大小有關,所以很適用於年輕代的回收。而複製算法內存利用率不高的問題,經過hotspot中的兩個survivor的設計獲得緩解(伊甸園:倖存者=8:1)。·
-
老年代(Tenured Gen)
- 老年代特色:區域較大,對象生命週期長、存活率高,回收不及年輕代頻繁。
- 這種狀況存在大量存活率高的對象,複製算法明顯變得不合適。通常是由標記一清除或者是標記一清除與標記一整理的混合實現。
- ➢Mark階段的開銷與存活對象的數量成正比。
- ➢Sweep階段的開銷與所管理區域的大小成正相關。
- ➢Compact階段的開銷與存活對象的數據成正比。
-
以HotSpot中的CMS回收器爲例,CMS是基於Mark一 Sweep實現的,對於對象的回收效率很高。而對於碎片問題,CMS採用基於Mark一Compact算法的Serial 0ld回收器做爲補償措施:當內存回收不佳(碎片致使的Concurrent Mode Failure時),將採用Serial 0ld執行Full GC以達到對老年代內存的整理。
- 分代的思想被現有的虛擬機普遍使用。幾乎全部的垃圾回收器都區分新生代和老年代。
增量收集算法
上述現有的算法,在垃圾回收過程當中,應用軟件將處於一種stop the World的狀態。在Stop the World狀態下,應用程序全部的線程都會掛起,暫停一切正常的工做,等待垃圾回收的完成。若是垃圾回收時間過長,應用程序會被掛起好久,將嚴重影響用戶體驗或者系統的穩定性。爲了解決這個問題,即對實時垃圾收集算法的研究直接致使了增量收集(Incremental Collecting) 算法的誕生。
增量收集算法基本思想
若是一次性將全部的垃圾進行處理,須要形成系統長時間的停頓,那麼就可讓垃圾收集線程和應用程序線程交替執行。每次,垃圾收集線程只收集一小片區域的內存空間,接着切換到應用程序線程。依次反覆,直到垃圾收集完成。
總的來講,增量收集算法的基礎還是傳統的標記一清除和複製算法。增量收集算法經過對線程間衝突的妥善處理,容許垃圾收集線程以分階段的方式完成標記、清理或複製工做。
增量收集算法優缺點:
使用這種方式,因爲在垃圾回收過程當中,間斷性地還執行了應用程序代碼,因此能減小系統的停頓時間。
可是,由於線程切換和上下文轉換的消耗,會使得垃圾回收的整體成本上升,形成系統吞吐量的降低。
分區算法G1回收器
通常來講,在相同條件下,堆空間越大,一次GC時所須要的時間就越長,有關GC產生的停頓也越長。
爲了更好地控制GC產生的停頓時間,將一塊大的內存區域分割成多個小塊,根據目標的停頓時間,每次合理地回收若干個小區間,而不是整個堆空間,從而減小一次GC所產生的停頓。
分代算法將按照對象的生命週期長短劃分紅兩個部分,而分區算法將整個堆空間劃分紅連續的不一樣小區間。
每個小區間都獨立使用,獨立回收。這種算法的好處是能夠控制一次回收多少個小區間。
![](http://static.javashuo.com/static/loading.gif)