簡單來講,垃圾回收 分紅兩步, 第一步找出垃圾,第二步進行回收,而標記階段使用的算法,就是 爲了找出誰是垃圾html
優勢:java
缺點:算法
由於第三點的嚴重性,JAVA 垃圾回收器中沒有使用這類算法。數據庫
什麼是 循環引用緩存
上面的圖中 , 對象一引用了對象二 , 對象二引用了對象三, 而對象三又從新指向了對象一,併發
而對象一是被外部引用的,因此它的計數器是2,eclipse
可是當外部的引用斷掉時, 計數器減1,仍然是1, 不會被清除,致使這三個對象 沒法清除,形成內存泄漏jvm
使用代碼證實JAVA 中沒有使用引用計數算法ide
public class RefCountGC { //這個成員屬性惟一的做用就是佔用一點內存 private byte[] bigSize = new byte[5 * 1024 * 1024];//5MB 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可否被回收? // System.gc(); } }複製代碼
上面代碼的內存示意圖:函數
下面運行驗證一下
jvm參數: -XX:+PrintGCDetails 打印GC日誌
不進行垃圾回收時:使用了 16798k
手動進行GC: 只剩下了655k , 說明 這兩個對象確實被回收了
引用計數小結
可達性分析算法:也能夠稱爲根搜索算法、追蹤性垃圾收集
基本思路以下:
示意圖:
GC Roots 能夠是哪些元素
列舉:
總結一句話就是,除了堆空間外的一些結構,好比:虛擬機棧、本地方法棧、方法區、字符串常量池等地方對堆空間進行引用的,均可以做爲GC Roots進行可達性分析
擴展:
除了這些固定的GC Roots集合之外,根據用戶所選用的垃圾收集器以及當前回收的內存區域不一樣,還能夠有其餘對象「臨時性」地加入,共同構成完整GC Roots集合。好比:在進行分代收集和局部回收時(PartialGC)。
若是隻針對Java堆中的某一塊區域進行垃圾回收(好比:典型的只針對新生代),必須考慮到這個區域的對象徹底有可能被其餘堆區域的對象所引用,例如老年代等,這時候就須要一併將關聯的區域對象也加入GC Roots集合中去考慮,才能保證可達性分析的準確性。
總結:
可達性分析注意事項
注意 finaliza() 方法並非一定是銷燬前調用的, 它也是肯定此對象可不能夠被銷燬的一個判斷因素,在標記階段調用
Object 類中 finalize() 源碼
// 等待被重寫 protected void finalize() throws Throwable { }複製代碼
永遠不要主動調用某個對象的finalize()方法應該交給垃圾回收機制調用。
一個糟糕的finalize()會嚴重影響GC的性能(寫個多重循環, 每一個對象在標記時調用時,均可能執行)。
從功能上來講,finalize()方法與C++中的析構函數比較類似,可是Java採用的是基於垃圾回收器的自動內存管理機制,因此finalize()方法在本質上不一樣於C++中的析構函數。
因爲finalize()方法的存在,可能會將對象復活,因此虛擬機中的對象通常處於三種可能的狀態。
若是從全部的根節點都沒法訪問到某個對象,說明對象己經再也不使用了。通常來講,此對象須要被回收。
但事實上,也並不是是「非死不可」的,這時候它們暫時處於「緩刑」階段(關入大牢)。一個沒法觸及的對象有可能在某一個條件下「復活」本身,若是這樣,那麼對它當即進行回收就是不合理的
爲此,定義虛擬機中的對象可能的三種狀態。以下:
以上3種狀態中,是因爲finalize()方法的存在,進行的區分。只有在對象兩次標記,不可觸及時才能夠被回收。
上一節已經說到, 斷定一個對象objA是否可回收,至少要經歷兩次標記過程:
使用代碼證實上述觀點
下面的代碼中, 使用類變量做爲 GC Roots ,而且在對象回收時,在finalize 方法裏自救
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 obj is dead 第2次 gc obj is dead複製代碼
說明此對象在第一次 gc時直接就回收了
打印:
調用當前類重寫的finalize()方法 第1次 gc obj is still alive 第2次 gc obj is dead複製代碼
在第一次gc時 ,調用了 finalize 方法,並又從新使類變量指向本身,復活
可是在第二次gc 時,發現finalize 方法 就沒有再執行了,直接被回收
本節將介紹使用各個工具查看 GC Roots 集合
獲取 dump 文件
jvm的一個內存快照,能夠被各個軟件分析,
下面將 演示如何將正在運行的 程序導出 dump文件
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("結束"); } }複製代碼
先將程序跑起來,將阻塞
方式一:命令行使用jmap
方式二:使用JVisualVM
第一步: 選中監視tab, 點擊堆 Dump
第二步: 右擊另存爲
第三步: 將上面程序 鍵盤輸入,繼續執行, 捕獲第二個快照
這樣咱們就獲取到了兩個 內存快照, 一個是被局部變量引用的,一個是釋放掉的
如何使用MAT 查看堆內存快照
打開 MAT ,選擇 File --> Open Heap Dump, 選擇 須要查看的Dump文件
選擇 Java Basics --> GC Roots
前後查看兩個快照, 因爲 局部變量再也不引用對象, 因此不在是GC Roots
釋 放後:
不用 dump文件, 查看實時的 運行時程序
查看當前程序中堆中最多的對象類型,並查看其GC Roots
點擊 :Live Memory --> All Object ,查看 堆中最多的對象, 並右擊 ,點擊Show Selection In Heap Walker
在顯示界面 選擇 References tab,查看堆中該類型的全部實例, 而後能夠選中某一個對象,選擇 Incoming References 選項, 再點擊 Show Paths To FC Roots 按鈕,彈出框點擊確認
而後就能夠看到 選中的對象的GC Roots , 例以下面的案例中, 字符串 "添加完畢,請操做" 對象 的 GC Roots 就是 out 對象, 由於被 System.out.println("添加完畢,請操做")打印
使用JProfiler 分析OOM
代碼:
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(); } } }複製代碼
上面的代碼 將會致使OOM, 能夠開啓jvm指令,在出現OOM 時 自動生成 dump文件
運行程序,jvm指令: -Xms8m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
輸出日誌: 出現了OOM,生成的dump文件在 工程目錄下
java.lang.OutOfMemoryError: Java heap space Dumping heap to java_pid14608.hprof ... java.lang.OutOfMemoryError: Java heap space at com.atguigu.java.HeapOOM.<init>(HeapOOM.java:12) at com.atguigu.java.HeapOOM.main(HeapOOM.java:20) Heap dump file created [7797849 bytes in 0.010 secs] count = 6複製代碼
打開JProfiler, 能夠在 超大對象 裏面找到它
也能夠 查看出現OOM的線程:
上面第一節中, 說到了如何標記垃圾,那麼下面就開始清除垃圾,關於清除垃圾,也有不一樣的算法
目前在JVM中比較常見的三種垃圾收集算法是
標記-清除算法(Mark-Sweep)是一種很是基礎和常見的垃圾收集算法,該算法被J.McCarthy等人在1960年提出並並應用於Lisp語言。
執行過程
當堆中的有效內存空間(available memory)被耗盡的時候,就會中止整個程序(也被稱爲stop the world),而後進行兩項工做,第一項則是標記,第二項則是清除
**標記:**垃圾收集器從引用根節點開始遍歷,標記全部被引用的對象。(這裏是標記不是垃圾的對象)
**清除:**垃圾收集器對堆內存從頭至尾進行線性的遍歷,若是發現某個對象在其Header中沒有標記爲可達對象,則將其回收
流程示意圖, 先從跟節點 找出全部的 可達對象, 標記爲"綠色",再遍歷整個對象列表,將沒有標記爲綠色的清除
何爲清除?
這裏所謂的清除並非真的置空,而是把須要清除的對象地址回收,保存在空閒的地址列表裏。下次有新對象須要加載時,判斷垃圾的位置空間是否夠,若是夠,就覆蓋原有的地址。 (跟電腦硬盤的刪除同樣)
關於空閒列表是在爲對象分配內存的時候提過:
若是內存規整
若是內存不規整
標記-清除算法的缺點
標記清除算法的優勢很明顯, 簡單 易理解 易於實現,可是缺點也很明顯
核心思路:
將存放對象的內存空間分爲兩塊,每次只使用其中一塊,在垃圾回收時,垃圾回收器也從跟節點開始遍歷,找到全部的可達對象,可是此時不標記, 而是直接將此對象複製到未被使用的內存塊中,以後全盤清除正在使用的內存塊中的全部對象,交換兩個內存的角色,最後完成垃圾回收
示意圖:
把可達的對象,直接複製到另一個區域中複製完成後,from區裏面的對象就沒有用了,新生代裏面就用到了複製算法
複製算法的優缺點
優勢
缺點
注意事項
若是系統中的垃圾對象不少,複製算法須要複製的存活對象數量並不會太大,效率較高,可是若是垃圾對象很是少的狀況, 每次拷貝都幾乎所有拷貝了,而後清除也就清除了個寂寞,
因此在jvm 中新生代中, 因爲垃圾回收頻率高,數量多,一次一般能夠回收70% - 99% 的內存空間 ,回收性價比很高。因此如今的商業虛擬機都是用這種收集算法回收新生代。
複製算法的高效性是創建在存活對象少、垃圾對象多的前提下的。這種狀況在新生代常常發生,可是在老年代,更常見的狀況是大部分對象都是存活對象。
若是依然使用複製算法,因爲存活對象較多,複製的成本也將很高。所以,基於老年代垃圾回收的特性,須要使用其餘的算法。
標記-清除算法的確能夠應用在老年代中,可是該算法不只執行效率低下,並且在執行完內存回收後還會產生內存碎片,因此JVM的設計者須要在此基礎之上進行改進。標記-壓縮(Mark-Compact)算法由此誕生。
1970年先後,G.L.Steele、C.J.Chene和D.s.Wise等研究者發佈標記-壓縮算法。在許多現代的垃圾收集器中,人們都使用了標記-壓縮算法或其改進版本。
執行流程:
示意圖:
標記-壓縮算法與標記-清除算法的比較
標記-壓縮算法的最終效果等同於標記-清除算法執行完成後,再進行一次內存碎片整理,所以,也能夠把它稱爲標記-清除-壓縮(Mark-Sweep-Compact)算法。
兩者的本質差別在於
並且在對象分配內存時能夠看到,若內存區域是零散的,須要訪問空閒列表(標記-清除算法回收地址到空閒列表)
可是若是使用標記-壓縮算法, 標記的存活對象將會被整理,按照內存地址依次排列,而未被標記的內存會被清理掉。如此一來,當咱們須要給新對象分配內存時,JVM只須要持有一個內存的起始地址便可,這比維護一個空閒列表顯然少了許多開銷。
是否移動回收後的存活對象是一項優缺點並存的風險決策。
標記-壓縮算法的優缺點
優勢
缺點
三種算法的橫縱對比:
標記清除
標記整理
複製
速率
中等
最慢
最快
空間開銷
少(但會堆積碎片)
少(不堆積碎片)
一般須要活對象的2倍空間(不堆積碎片)
移動對象
否
是
是
總結: 沒有最好的算法,只有最適合的算法
前面全部這些算法中,並無一種算法能夠徹底替代其餘算法,它們都具備本身獨特的優點和特色。
分代收集算法應運而生。他的目標不是替換上面的算法,而是具體問題 具體對待
分代收集算法,是基於這樣一個事實:不一樣的對象的生命週期是不同的。所以,不一樣生命週期的對象能夠採起不一樣的收集方式,以便提升回收效率。
通常是把Java堆分爲新生代和老年代,這樣就能夠根據各個年代的特色使用不一樣的回收算法,以提升垃圾回收的效率。
在Java程序運行的過程當中,會產生大量的對象,其中有些對象是與業務信息相關:
目前幾乎全部的GC都採用分代收集算法執行垃圾回收的
每一個代的各個特色和適合的回收算法
年輕代(Young Gen)
老年代(Tenured Gen)
簡單介紹CMS 回收器
上述現有的算法,在垃圾回收過程當中,應用軟件將處於一種Stop the World的狀態。在Stop the World狀態下,應用程序全部的線程都會掛起,暫停一切正常的工做,等待垃圾回收的完成。
若是垃圾回收時間過長,應用程序會被掛起好久,將嚴重影響用戶體驗或者系統的穩定性。爲了解決這個問題,即對實時垃圾收集算法的研究直接致使了增量收集(Incremental Collecting)算法的誕生。
基本思路:
使用這種方式,因爲在垃圾回收過程當中,間斷性地還執行了應用程序代碼,因此能減小系統的停頓時間。
缺點:
由於線程切換和上下文轉換的消耗,會使得垃圾回收的整體成本上升,形成系統吞吐量的降低。
注意,這些只是基本的算法思路,實際GC回收器過程要複雜的多,目前還在發展中的前沿GC都是複合算法,而且並行和併發兼備。 因此這裏以爲模糊的,到後面把各個GC 回收器的實現說明完,就清晰了.