垃圾收集機制是 Java 的招牌能力,極大的提升了開發效率
。現在,垃圾收集幾乎成爲了現代語言的標配,即便通過了如此長時間的發展,Java 的垃圾收集機制仍然在不斷的演進中,不一樣大小的設備、不一樣特徵的應用場景都對垃圾收集提出了新的挑戰,也是面試的熱門考點
。web
垃圾是指在運行程序中沒有任何指針指向的對象
,這個對象就是須要被回收的垃圾。面試
若是不及時對內存中的垃圾進行清理,那麼這些垃圾所佔的內存空間會一直保留到應用程序結束,被保留的空間沒法被其它對象所使用,甚至可能致使內存溢出
。算法
內存早晚都會被消耗完
。JVM將整理出的內存分配給的新的對象
。沒有GC就不能保證應用程序的正常進行
,常常形成 STW 的 GC 又跟不上實際的需求,因此纔會不短地嘗試對 GC 進行優化。自動內存管理,無需開發人員手動參與內存的分配與回收,下降內存泄漏和內存溢出的風險
。數據庫
自動內存管理機制,將開發人員從繁重的內存管理中釋放出來,能夠更專一與業務開發
。segmentfault
壞處數組
可能會弱化開發人員在程序中出現內存溢出時定位問題和解決問題的能力
。GC 發生的區域緩存
垃圾回收器能夠對年輕代回收,也能夠對老年代回收,甚至是整個堆和方法區的回收。安全
其中 Java堆是垃圾收集器的工做重點
。服務器
從次數上講:
垃圾標記階段:對象存活判斷
須要區分出內存中哪些是存活對象,哪些是已經死亡的對象
,只有被標記已經死亡的對象,GC 纔會執行垃圾回收時,釋放掉其所佔內存空間,所以這個過程咱們能夠稱爲垃圾標記階段。引用計數算法
和可達性分析算法
。引用計數算法
引用計數器屬性
,用於記錄對象被引用的狀況。優勢
缺點
存儲空間的開銷
。時間開銷
。沒法處理循環引用
的狀況。這個問題是致命的,因此致使在 Java 的垃圾回收器中沒有使用這類算法。
相對於引用計數算法而言,可達性分析算法不只一樣具有實現簡單和執行高效等特色,更重要的是該算法能夠有效地解決在引用計數算法中循環引用的問題,防止內存泄漏的發生
。
這樣類型的垃圾收集一般也叫作追蹤性垃圾收集(Tracing Garbage Collection)
。
所謂GC Roots
根集合就是一組必須活躍的引用。
基本思路:
搜索被根對象集合所鏈接的目標對象是否可達
。引用鏈(Reference Chain)
。
能夠做爲 GC Roots 的對象有哪些?
對象被銷燬以前的自定義處理邏輯
。用於在對象被回收時進行資源釋放
。一般在這個方法中進行一些資源釋放和清理工做,好比關閉文件、數據庫鏈接等。緣由:
因爲 finalize()方法的存在,虛擬機中的對象通常處於三種可能的狀態
。
若是從全部的根節點都沒法訪問到某對象,說明對象已經再也不使用了。通常來講,此對象須要被回收。但事實上,也並不是是"非死不可"的,這時候它們暫時處於"緩刑"階段。一個沒法觸及的對象有可能在某一個條件下"復活"本身
,若是這樣,那麼對它的回收就是不合理的,爲此,定義虛擬機中的對象可能的三種狀態。
以下:
可觸及的:
從根節點開始,能夠到達這個對象。可復活的:
對象的全部引用都被釋放,可是對象有可能在 finalize()中復活。不可觸及的:
對象的 finalize()被調用,而且沒有復活,那麼就會進入不可觸及狀態,不可觸及的對象不可能被複活,由於finalize()只會被調用一次
。以上三種狀態中,是因爲 finalize()方法的存在,進行的區分,只有在對象不可觸及時才能夠被回收。
具體過程
判斷一個對象是否能夠回收,至少要經歷兩次標記過程:
① 若是對象沒有重寫 finalize()方法,或者 finalize()方法以及被虛擬機調用過,則虛擬機視爲"沒有必要執行",當前對象斷定爲不可觸及的。
② 若是對象重寫了 finalize()方法,且還未執行過,那麼當前對象會被插入到 F-Queue 隊列中,由一個虛擬機自動建立的、低優先級的 Finalizer 線程觸發其 finalize()方法執行。
③ finalize()方法是對象逃脫死亡的最後機會
,稍後 GC 會對 F-Queue 隊列中的對象進行第二次標記,若是對象在 finalize()方法終於引用鏈上的任何一個對象創建了聯繫,那麼在第二次標記時,對象會被移出"即將回收"的集合,以後,若是對象再次出現沒有引用存在的狀況,finalize()方法不會再次被調用,對象會直接變成不可觸及的狀態,也就是說,一個對象的 finalize()方法只會被調用一次。
代碼示例:
/** * 測試Object類中finalize()方法,即對象的finalization機制。 */ public class CanReliveObj { public static CanReliveObj obj;//類變量,屬於 GC Root //finalize方法只能被調用一次 @Override protected void finalize() throws Throwable { super.finalize(); System.out.println("調用當前類重寫的finalize()方法"); obj = this;//當前待回收的對象在finalize()方法中與引用鏈上的一個對象obj創建了聯繫。 obj是GC Roots,this是當前對象,也就是要被回收的對象。 } public static void main(String[] args) { try { obj = new CanReliveObj(); // 對象第一次成功拯救本身 obj = null; System.gc();//調用垃圾回收器。第一次GC的時候,會執行finalize方法,在finalize方法中,因爲obj指向了this,obj變量是一個GC Root,要被回收的對象與引用鏈上的對象創建了又聯繫,因此對象被複活了 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(); // 第二次調用GC,因爲finalize只能被調用一次,因此對象會直接被回收 // 由於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
概述
當成功區分出內存中存活對象和死亡對象後, GC 接下來的任務就是執行垃圾回收,釋放掉無用對象所佔用的內存空間,以便有足夠的可用內存空間爲新對象分配內存。
目前在 JVM 中比較常見的三種垃圾收集算法是
標記-清除算法(Mark-Sweep)是一種很是基礎和常見的垃圾收集算法,該算法被 J.McCarthy 等人在 1960 年提出並並應用於 Lisp 語言。
執行過程
當堆中的有效內存空間(available memory)被耗盡的時候,就會中止整個程序(也被稱爲 stop the world),而後進行兩項工做,第一項則是標記,第二項則是清除。
標記
: Collector 從引用根節點開始遍歷,標記全部被引用的對象,通常是在對象的 Header 中記錄爲可達對象。清除
: Collector 對堆內存從頭至尾進行線性的遍歷,若是發現某個對象在其 Header 中沒有標記爲可達對象,則將其回收。
什麼是清除?
這裏所謂的清除並非真的置空,而是把須要清除的對象地址保存在空閒的地址列表裏。下次有新對象須要加載時,判斷垃圾的位置空間是否夠,若是夠,就存放覆蓋原有的地址。
關於空閒列表:
若是內存規整
若是內存不規整
缺點
背景
爲了解決標記-清除算法在垃圾收集效率方面的缺陷,M.L.Minsky 於 1963 年發表了著名的論文,「使用雙存儲區的 Lisp 語言垃圾收集器 CA LISP Garbage Collector Algorithm Using Serial Secondary Storage)」。M.L.Minsky 在該論文中描述的算法被人們稱爲複製(Copying)算法,它也被 M.L.Minsky 本人成功地引入到了 Lisp 語言的一個實現版本中。
核心思想
將活着的內存空間分爲兩塊,每次只使用其中一塊,在垃圾回收時將正在使用的內存中的存活對象複製到未被使用的內存塊中,以後清除正在使用的內存塊中的全部對象,交換兩個內存的角色,最後完成垃圾回收。
優勢
缺點
注意
若是系統中的存活對象不少,那麼複製算法的效率就會大打折扣。理想狀態下須要複製的存活對象數量並不會太大,或者說很是低才行。
在新生代,對常規應用的垃圾回收,一次一般能夠回收 70% - 99% 的內存空間。回收性價比很高。因此如今的商業虛擬機都是用這種收集算法回收新生代。
背景
複製算法的高效性是創建在存活對象少、垃圾對象多的前提下的。這種狀況在新生代常常發生,可是在老年代,更常見的狀況是大部分對象都是存活對象。若是依然使用複製算法,因爲存活對象較多,複製的成本也將很高。所以,基於老年代垃圾回收的特性,須要使用其餘的算法。
標記一清除算法的確能夠應用在老年代中,可是該算法不只執行效率低下,並且在執行完內存回收後還會產生內存碎片,因此 JvM 的設計者須要在此基礎之上進行改進。標記-壓縮(Mark-Compact)算法由此誕生。
1970 年先後,G.L.Steele、C.J.Chene 和 D.s.Wise 等研究者發佈標記-壓縮算法。在許多現代的垃圾收集器中,人們都使用了標記-壓縮算法或其改進版本。
執行過程
第一階段和標記清除算法同樣,從根節點開始標記全部被引用對象。
第二階段將全部的存活對象壓縮到內存的一端,按順序排放。以後,清理邊界外全部的空間。
標記-清除和標記-壓縮的區別
標記-壓縮算法的最終效果等同於標記-清除算法執行完成後,再進行一次內存碎片整理,所以,也能夠把它稱爲標記-清除-壓縮(Mark-Sweep-Compact)算法
。
兩者的本質差別在於標記-清除算法是一種非移動式
的回收算法,標記-壓縮是移動式
的。是否移動回收後的存活對象是一項優缺點並存的風險決策。能夠看到,標記的存活對象將會被整理,按照內存地址依次排列,而未被標記的內存會被清理掉。如此一來,當咱們須要給新對象分配內存時,JVM 只須要持有一個內存的起始地址便可,這比維護一個空閒列表顯然少了許多開銷。
優勢
缺點
標記清除 | 標記壓縮 | 複製 | |
---|---|---|---|
速率 | 中等 | 最慢 | 最快 |
空間開銷 | 少(但會堆積碎片) | 少(不堆積碎片) | 一般須要活對象的 2 倍空間(不堆積碎片) |
移動對象 | 否 | 是 | 是 |
效率上來講,複製算法是當之無愧的老大,可是卻浪費了太多內存。
而爲了儘可能兼顧上面提到的三個指標,標記-整理算法相對來講更平滑一些,可是效率上不盡如人意,它比複製算法多了一個標記的階段,比標記-清除多了一個整理內存的階段。
前面全部這些算法中,並無一種算法能夠徹底替代其餘算法,它們都具備本身獨特的優點和特色。分代收集算法應運而生。
分代收集算法,是基於這樣一個事實:不一樣的對象的生命週期是不同的。所以,不一樣生命週期的對象能夠採起不一樣的收集方式,以便提升回收效率
。通常是把 Java 堆分爲新生代和老年代,這樣就能夠根據各個年代的特色使用不一樣的回收算法,以提升垃圾回收的效率。
在 Java 程序運行的過程當中,會產生大量的對象,其中有些對象是與業務信息相關,好比Http請求中的Session對象、線程、Socket鏈接
,這類對象跟業務直接掛鉤,所以生命週期比較長。可是還有一些對象,主要是程序運行過程當中生成的臨時變量,這些對象生命週期會比較短,好比:String對象
,因爲其不變類的特性,系統會產生大量的這些對象,有些對象甚至只用一次便可回收。
目前幾乎全部的GC都採用分代收集算法執行垃圾回收的。
在 HotSpot 中,基於分代的概念,GC 所使用的內存回收算法必須結合年輕代和老年代各自的特色。
年輕代特色:區域相對老年代較小,對象生命週期短、存活率低,回收頻繁。
這種狀況複製算法的回收整理,速度是最快的。複製算法的效率只和當前存活對象大小有關,所以很適用於年輕代的回收。而複製算法內存利用率不高的問題,經過 hotspot 中的兩個 survivor 的設計獲得緩解。
老年代特色:區域較大,對象生命週期長、存活率高,回收不及年輕代頻繁。
這種狀況存在大量存活率高的對象,複製算法明顯變得不合適。通常是由標記-清除或者是標記-清除與標記-整理的混合實現。
以 HotSpot 中的 CMS 回收器爲例,CMS 是基於 Mark-Sweep 實現的,對於對象的回收效率很高。而對於碎片問題,CMS 採用基於 Mark-Compact 算法的 Serial old 回收器做爲補償措施:當內存回收不佳(碎片致使的 Concurrent Mode Failure 時),將採用 serial old 執行 FullGC 以達到對老年代內存的整理。
分代的思想被現有的虛擬機普遍使用。幾乎全部的垃圾回收器都區分新生代和老年代.
概述
上述現有的算法,在垃圾回收過程當中,應用軟件將處於一種stop the world
的狀態。在 stop the world 狀態下,應用程序全部的線程都會掛起,暫停一切正常的工做,等待垃圾回收的完成。若是垃圾回收時間過長,應用程序會被掛起好久,將嚴重影響用戶體驗或者系統的穩定性
。爲了解決這個問題,即對實時垃圾收集算法的研究直接致使了增量收集(Incremental Collecting)算法的誕生。
若是一次性將全部的垃圾進行處理,須要形成系統長時間的停頓,那麼就可讓垃圾收集線程和應用程序線程交替執行。每次,垃圾收集線程只收集一小片區域的內存空間,接着切換到應用程序線程。依次反覆,直到垃圾收集完成。
總的來講,增量收集算法的基礎還是傳統的標記-清除和複製算法。增量收集算法經過對線程間衝突的妥善處理,容許垃圾收集線程以分階段的方式完成標記、清理或複製工做
。
缺點
使用這種方式,因爲在垃圾回收過程當中,間斷性地還執行了應用程序代碼,因此能減小系統的停頓時間。可是,由於線程切換和上下文轉換的消耗,會使得垃圾回收的整體成本上升,形成系統吞吐量的降低
。
通常來講,在相同條件下,堆空間越大,一次 Gc 時所須要的時間就越長,有關 GC 產生的停頓也越長。爲了更好地控制 GC 產生的停頓時間,將一塊大的內存區域分割成多個小塊,根據目標的停頓時間,每次合理地回收若干個小區間,而不是整個堆空間,從而減小一次 GC 所產生的停頓。
分代算法將按照對象的生命週期長短劃分紅兩個部分,分區算法將整個堆空間劃分紅連續的不一樣小區間。 每個小區間都獨立使用,獨立回收。這種算法的好處是能夠控制一次回收多少個小區間。
在默認狀況下,經過 system.gc()或者 Runtime.getRuntime().gc() 的調用,會顯式觸發FullGC
,同時對老年代和新生代進行回收,嘗試
釋放被丟棄對象佔用的內存。
然而 system.gc() 調用附帶一個免責聲明,沒法保證對垃圾收集器的調用。(不能確保當即生效)
JVM 實現者能夠經過 system.gc() 調用來決定 JVM 的 GC 行爲。而通常狀況下,垃圾回收應該是自動進行的,無須手動觸發,不然就太過於麻煩了。在一些特殊狀況下,如咱們正在編寫一個性能基準,咱們能夠在運行之間調用 System.gc()。
代碼演示:
public class SystemGCTest { public static void main(String[] args) { new SystemGCTest(); // 提醒JVM進行垃圾回收 System.gc(); //強制調用使用引用的對象的finalize()方法 //System.runFinalization(); } @Override protected void finalize() throws Throwable { super.finalize(); System.out.println("執行了 finalize()方法"); } }
運行結果不必定會觸發銷燬的方法,調用 System.runFinalization()會強制調用 失去引用對象的 finalize()。
手動 GC 來理解不可達對象的回收
public class LocalVarGC { /** * 觸發Minor GC沒有回收對象,而後在觸發Full GC將該對象存入old區 */ public void localvarGC1() { byte[] buffer = new byte[10*1024*1024]; System.gc(); } /** * 觸發YoungGC的時候,已經被回收了 */ public void localvarGC2() { byte[] buffer = new byte[10*1024*1024]; buffer = null; System.gc(); } /** * 不會被回收,由於它還存放在局部變量表索引爲1的槽中 */ public void localvarGC3() { { byte[] buffer = new byte[10*1024*1024]; } System.gc(); } /** * 會被回收,由於它還存放在局部變量表索引爲1的槽中,可是後面定義的value把這個槽給替換了 */ public void localvarGC4() { { byte[] buffer = new byte[10*1024*1024]; } int value = 10; System.gc(); } /** * localvarGC5中的數組已經被回收 */ public void localvarGC5() { localvarGC1(); System.gc(); } public static void main(String[] args) { LocalVarGC localVarGC = new LocalVarGC(); localVarGC.localvarGC3(); } }
內存溢出相對於內存泄漏來講,儘管更容易被理解,可是一樣的,內存溢出也是引起程序崩潰的罪魁禍首之一。
因爲 GC 一直在發展,全部通常狀況下,除非應用程序佔用的內存增加速度很是快,形成垃圾回收已經跟不上內存消耗的速度,不然不太容易出現 OOM 的狀況。
大多數狀況下,GC 會進行各類年齡段的垃圾回收,實在不行了就放大招,來一次獨佔式的 Fu11GC 操做,這時候會回收大量的內存,供應用程序繼續使用。
javadoc 中對 outofMemoryError 的解釋是,沒有空閒內存,而且垃圾收集器也沒法提供更多內存
。
首先說沒有空閒內存的狀況:說明 Java 虛擬機的堆內存不夠。緣由以下:
好比:可能存在內存泄漏問題;也頗有可能就是堆的大小不合理,好比咱們要處理比較可觀的數據量,可是沒有顯式指定 JVM 堆大小或者指定數值偏小。咱們能夠經過參數-Xms 、-Xmx 來調整。
對於老版本的 oracle JDK,由於永久代的大小是有限的,而且 JVM 對永久代垃圾回收(如,常量池回收、卸載再也不須要的類型)很是不積極,因此當咱們不斷添加新類型的時候,永久代出現 OutOfMemoryError 也很是多見,尤爲是在運行時存在大量動態類型生成的場合;相似 intern 字符串緩存佔用太多空間,也會致使 OOM 問題。對應的異常信息,會標記出來和永久代相關:「java.lang.OutOfMemoryError:PermGen space"。
隨着元數據區的引入,方法區內存已經再也不那麼窘迫,因此相應的 OOM 有所改觀,出現 OOM 的異常信息則變成了:「java.lang.OutOfMemoryError:Metaspace"。直接內存不足,也會致使 OOM。
這裏面隱含着一層意思是,在拋出 OutofMemoryError 以前,一般垃圾收集器會被觸發,盡其所能去清理出空間。
例如:在引用機制分析中,涉及到 JVM 會去嘗試回收軟引用指向的對象等。在 java.nio.BIts.reserveMemory()方法中,咱們能清楚的看到,System.gc()會被調用,以清理空間。
固然,也不是在任何狀況下垃圾收集器都會被觸發的。好比,咱們去分配一個超大對象,相似一個超大數組超過堆的最大值,JVM 能夠判斷出垃圾收集並不能解決這個問題,因此直接拋出 OutOfMemoryError。
嚴格來講,只有對象不會再被程序用到了,可是GC又不能回收他們的狀況,才叫內存泄漏。
但實際狀況不少時候一些不太好的實踐(或疏忽)會致使對象的生命週期變得很長甚至致使 00M,也能夠叫作寬泛意義上的「內存泄漏」。
儘管內存泄漏並不會馬上引發程序崩潰,可是一旦發生內存泄漏,程序中的可用內存就會被逐步蠶食,直至耗盡全部內存,最終出現 OutOfMemoryError,致使程序崩潰。
舉例
單例的生命週期和應用程序是同樣長的,因此單例程序中,若是持有對外部對象的引用的話,那麼這個外部對象是不能被回收的,則會致使內存泄漏的產生。
數據庫鏈接(dataSourse.getConnection() ),網絡鏈接(socket)和 io 鏈接必須手動 close,不然是不能被回收的。
Stop-The-World,簡稱 STW,指的是 GC 事件發生過程當中,會產生應用程序的停頓。停頓產生時整個應用程序線程都會被暫停,沒有任何響應
,有點像卡死的感受,這個停頓稱爲 STW。
可達性分析算法中枚舉根節點(GC Roots)會致使全部 Java 執行線程停頓。
被 STW 中斷的應用程序線程會在完成 GC 以後恢復,頻繁中斷會讓用戶感受像是網速不快形成電影卡帶同樣,因此咱們須要減小 STW 的發生。
STW 事件和採用哪款 GC 無關,全部的 GC 都有這個事件。
哪怕是 G1 也不能徹底避免 Stop-the-world 狀況發生,只能說垃圾回收器愈來愈優秀,回收效率愈來愈高,儘量地縮短了暫停時間。
STW 是 JVM 在後臺自動發起和自動完成的
。在用戶不可見的狀況下,把用戶正常的工做線程所有停掉。
開發中不要用 System.gc(); 會致使 Stop-The-World 的發生。
併發
在操做系統中,是指一個時間段中有幾個程序都處於已啓動運行到運行完畢之間,且這幾個程序都是在同一個處理器上運行。
併發不是真正意義上的「同時進行」,只是 CPU 把一個時間段劃分紅幾個時間片斷(時間區間),而後在這幾個時間區間之間來回切換,因爲 CPU 處理的速度很是快,只要時間間隔處理得當,便可讓用戶感受是多個應用程序同時在進行。
並行
當系統有一個以上 CPU 時,當一個 CPU 執行一個進程時,另外一個 CPU 能夠執行另外一個進程,兩個進程互不搶佔 CPU 資源,能夠同時進行,咱們稱之爲並行(Parallel)。
其實決定並行的因素不是 CPU 的數量,而是 CPU 的核心數量,好比一個 CPU 多個核也能夠並行。
適合科學計算,後臺處理等弱交互場景。
併發和並行對比
併發
,指的是多個事情,在同一時間段內同時發生了。
並行
,指的是多個事情,在同一時間點上同時發生了。
併發的多個任務之間是互相搶佔資源的。並行的多個任務之間是不互相搶佔資源的。
只有在多 CPU 或者一個 CPU 多核的狀況中,纔會發生並行。
不然,看似同時發生的事情,其實都是併發執行的。
併發和並行,在談論垃圾收集器的上下文語境中,它們能夠解釋以下:
並行(Parallel)
多條垃圾收集線程並行工做
,但此時用戶線程仍處於等待狀態。如 ParNew、Parallel Scavenge、Parallel old;串行(Serial)
併發(Concurrent)
指用戶線程與垃圾收集線程同時執行
(但不必定是並行的,可能會交替執行),垃圾回收線程在執行時不會停頓用戶程序的運行。>用戶程序在繼續運行,而垃圾收集程序線程運行於另外一個 CPU 上;
如:CMS、G1
安全點(Safepoint)
程序執行時並不是在全部地方都能停頓下來開始 GC,只有在特定的位置才能停頓下來開始 GC,這些位置稱爲「安全點(Safepoint)」。
Safe Point 的選擇很重要,若是太少可能致使GC等待的時間太長,若是太頻繁可能致使運行時的性能問題
。大部分指令的執行時間都很是短暫,一般會根據「是否具備讓程序長時間執行的特徵」
爲標準。好比:選擇一些執行時間較長的指令做爲 Safe Point,如方法調用、循環跳轉和異常跳轉
等。
如何在 cc 發生時,檢查全部線程都跑到最近的安全點停頓下來呢?
安全區域(Safe Region)
Safepoint 機制保證了程序執行時,在不太長的時間內就會遇到可進入 GC 的 Safepoint。可是,程序「不執行」的時候呢?例如線程處於 sleep 狀態或 Blocked 狀態,這時候線程沒法響應 JVM 的中斷請求,「走」到安全點去中斷掛起,JVM 也不太可能等待線程被喚醒。對於這種狀況,就須要安全區域(Safe Region)來解決。
安全區域是指在一段代碼片斷中,對象的引用關係不會發生變化,在這個區域中的任何位置開始 Gc 都是安全的
。咱們也能夠把 Safe Region 看作是被擴展了的 Safepoint。
執行流程:
再談引用
咱們但願能描述這樣一類對象:當內存空間還足夠時,則能保留在內存中;若是內存空間在進行垃圾收集後仍是很緊張,則能夠拋棄這些對象。
【既偏門
又很是高頻
的面試題】強引用、軟引用、弱引用、虛引用有什麼區別?具體使用場景是什麼? 在 JDK1.2 版以後,Java 對引用的概念進行了擴充,將引用分爲:
這4種引用強度依次逐漸減弱
。除強引用外,其餘 3 種引用都可以在 java.lang.ref 包中找到它們的身影。以下圖,顯示了這 3 種引用類型對應的類,開發人員能夠在應用程序中直接使用它們。
1.強引用(StrongReference)
最廣泛的一種引用方式,如 String s = "abc",變量 s 就是字符串「abc」的強引用,只要強引用存在,則垃圾回收器就不會回收這個對象。
2.軟引用(SoftReference)
用於描述還有用但非必須的對象,若是內存足夠,不回收,若是內存不足,則回收。通常用於實現內存敏感的高速緩存,軟引用能夠和引用隊列 ReferenceQueue 聯合使用,若是軟引用的對象被垃圾回收,JVM 就會把這個軟引用加入到與之關聯的引用隊列中。
3.弱引用(WeakReference)
弱引用和軟引用大體相同,弱引用與軟引用的區別在於:只具備弱引用的對象擁有更短暫的生命週期。在垃圾回收器線程掃描它所管轄的內存區域的過程當中,一旦發現了只具備弱引用的對象,無論當前內存空間足夠與否,都會回收它的內存。
4.虛引用(PhantomReference)
就是形同虛設,與其餘幾種引用都不一樣,虛引用並不會決定對象的生命週期。若是一個對象僅持有虛引用,那麼它就和沒有任何引用同樣,在任什麼時候候均可能被垃圾回收器回收。 虛引用主要用來跟蹤對象被垃圾回收器回收的活動。
虛引用與軟引用和弱引用的一個區別在於:
虛引用必須和引用隊列 (ReferenceQueue)聯合使用。當垃圾回收器準備回收一個對象時,若是發現它還有虛引,就會在回收對象的內存以前,把這個虛引用加入到與之關聯的引用隊列中。
強引用(Strong Reference)
在 Java 程序中,最多見的引用類型是強引用(普通系統99%以上都是強引用)
,也就是咱們最多見的普通對象引用,也是默認的引用類型
。
當在 Java 語言中使用 new 操做符建立一個新的對象,並將其賦值給一個變量的時候,這個變量就成爲指向該對象的一個強引用。
強引用的對象是可觸及的,垃圾收集器就永遠不會回收掉被引用的對象。
對於一個普通的對象,若是沒有其餘的引用關係,只要超過了引用的做用域或者顯式地將相應(強)引用賦值爲 nu11,就是能夠當作垃圾被收集了,固然具體回收時機仍是要看垃圾收集策略。
相對的,軟引用、弱引用和虛引用的對象是軟可觸及、弱可觸及和虛可觸及的,在必定條件下,都是能夠被回收的。因此,強引用是形成Java內存泄漏的主要緣由之一
。
強引用的案例說明
StringBuffer str = new StringBuffer("hello world");
局部變量 str 指向 stringBuffer 實例所在堆空間,經過 str 能夠操做該實例,那麼 str 就是 stringBuffer 實例的強引用。
對應內存結構:
若是此時,再運行一個賦值語句
StringBuffer str1 = str;
對應的內存結構爲:
那麼咱們將 str = null; 則原來堆中的對象也不會被回收,由於還有其它對象指向該區域。
總結
本例中的兩個引用,都是強引用,強引用具有如下特色:
軟引用(Soft Reference)
不足即回收
軟引用是用來描述一些還有用,但非必需的對象。只被軟引用關聯着的對象,在系統將要發生內存溢出異常前,會把這些對象列進回收範圍之中進行第二次回收
,若是此次回收尚未足夠的內存,纔會拋出內存溢出異常。
第一次回收是不可達的對象。
軟引用一般用來實現內存敏感的緩存。好比:高速緩存
就有用到軟引用。若是還有空閒內存,就能夠暫時保留緩存,當內存不足時清理掉,這樣就保證了使用緩存的同時,不會耗盡內存。
垃圾回收器在某個時刻決定回收軟可達的對象的時候,會清理軟引用,並可選地把引用存放到一個引用隊列(Reference Queue)。
相似弱引用,只不過 Java 虛擬機會盡可能讓軟引用的存活時間長一些,無可奈何才清理。
在 JDK1.2 版以後提供了 SoftReference 類來實現軟引用
// 聲明強引用 Object obj = new Object(); // 建立一個軟引用 SoftReference<Object> sf = new SoftReference<>(obj); obj = null; //銷燬強引用
弱引用(Weak Reference)
發現即回收
弱引用也是用來描述那些非必需對象,被弱引用關聯的對象只能生存到下一次垃圾收集發生爲止
。在系統 GC 時,只要發現弱引用,無論系統堆空間使用是否充足,都會回收掉只被弱引用關聯的對象。
可是,因爲垃圾回收器的線程一般優先級很低,所以,並不必定能很快地發現持有弱引用的對象。在這種狀況下,弱引用對象能夠存在較長的時間
。
弱引用和軟引用同樣,在構造弱引用時,也能夠指定一個引用隊列,當弱引用對象被回收時,就會加入指定的引用隊列,經過這個隊列能夠跟蹤對象的回收狀況。
軟引用、弱引用都很是適合來保存那些無關緊要的緩存數據
。若是這麼作,當系統內存不足時,這些緩存數據會被回收,不會致使內存溢出。而當內存資源充足時,這些緩存數據又能夠存在至關長的時間,從而起到加速系統的做用。
在 JDK1.2 版以後提供了 WeakReference 類來實現弱引用
// 聲明強引用 Object obj = new Object(); // 建立一個弱引用 WeakReference<Object> sf = new WeakReference<>(obj); obj = null; //銷燬強引用
弱引用對象與軟引用對象的最大不一樣就在於,當 GC 在進行回收時,須要經過算法檢查是否回收軟引用對象,而對於弱引用對象,GC 老是進行回收。弱引用對象更容易、更快被GC回收
。
虛引用(Phantom Reference)
對象回收跟蹤
也稱爲「幽靈引用」或者「幻影引用」,是全部引用類型中最弱的一個。
一個對象是否有虛引用的存在,徹底不會決定對象的生命週期。若是一個對象僅持有虛引用,那麼它和沒有引用幾乎是同樣的,隨時均可能被垃圾回收器回收。
它不能單獨使用,也沒法經過虛引用來獲取被引用的對象。當試圖經過虛引用的 get()方法取得對象時,老是 null。
爲一個對象設置虛引用關聯的惟一目的在於跟蹤垃圾回收過程。好比:能在這個對象被收集器回收時收到一個系統通知
。
虛引用必須和引用隊列一塊兒使用。虛引用在建立時必須提供一個引用隊列做爲參數。當垃圾回收器準備回收一個對象時,若是發現它還有虛引用,就會在回收對象後,將這個虛引用加入引用隊列,以通知應用程序對象的回收狀況。
因爲虛引用能夠跟蹤對象的回收時間,所以,也能夠將一些資源釋放操做放置在虛引用中執行和記錄。
虛引用沒法獲取到咱們的數據
在 JDK1.2 版以後提供了 PhantomReference 類來實現虛引用。
// 聲明強引用 Object obj = new Object(); // 聲明引用隊列 ReferenceQueue phantomQueue = new ReferenceQueue(); // 聲明虛引用(還須要傳入引用隊列) PhantomReference<Object> sf = new PhantomReference<>(obj, phantomQueue); obj = null;
咱們使用一個案例,來結合虛引用,引用隊列,finalize() 方法進行講解:
public class PhantomReferenceTest { // 當前類對象的聲明 public static PhantomReferenceTest obj; // 引用隊列 static ReferenceQueue<PhantomReferenceTest> phantomQueue = null; @Override protected void finalize() throws Throwable { super.finalize(); System.out.println("調用當前類的finalize方法"); obj = this; } public static void main(String[] args) { Thread thread = new Thread(() -> { while (true) { if (phantomQueue != null) { PhantomReference<PhantomReferenceTest> objt = null; try { objt = (PhantomReference<PhantomReferenceTest>) phantomQueue.remove(); } catch (Exception e) { e.getStackTrace(); } if (objt != null) { System.out.println("追蹤垃圾回收過程:PhantomReferenceTest實例被GC了"); } } } }, "t1"); thread.setDaemon(true); thread.start(); phantomQueue = new ReferenceQueue<>(); obj = new PhantomReferenceTest(); // 構造了PhantomReferenceTest對象的虛引用,並指定了引用隊列 PhantomReference<PhantomReferenceTest> phantomReference = new PhantomReference<>(obj, phantomQueue); try { System.out.println(phantomReference.get()); // 去除強引用 obj = null; // 第一次進行GC,因爲對象可復活,GC沒法回收該對象 System.out.println("第一次GC操做"); System.gc(); Thread.sleep(1000); if (obj == null) { System.out.println("obj 是 null"); } else { System.out.println("obj 可用"); } System.out.println("第二次GC操做"); obj = null; System.gc(); Thread.sleep(1000); if (obj == null) { System.out.println("obj 是 null"); } else { System.out.println("obj 可用"); } } catch (Exception e) { e.printStackTrace(); } finally { } } }
運行結果:
null 第一次GC操做 調用當前類的finalize方法 obj 可用 第二次GC操做 追蹤垃圾回收過程:PhantomReferenceTest實例被GC了 obj 是 null
終結器引用
它用於實現對象的 finalize() 方法,也能夠稱爲終結器引用。
無需手動編碼,其內部配合引用隊列使用。
在 GC 時,終結器引用入隊。由 Finalizer 線程經過終結器引用找到被引用對象調用它的 finalize()方法,第二次 GC 時纔回收被引用的對象。
垃圾回收器分類
按線程數
分,可用分爲串行垃圾回收器和並行垃圾回收器。
串行回收指的是在同一時間段內只容許有一個 CPU 用於執行垃圾回收操做,此時工做線程被暫停,直至垃圾收集工做結束。
串行回收默認被應用在客戶端的Client模式下的JVM中
。和串行回收相反,並行收集能夠運用多個 CPU 同時執行垃圾回收,所以提高了應用的吞吐量,不過並行回收仍然與串行回收同樣,採用獨佔式,使用了「Stop-The-World」機制。
按照工做模式
分,能夠分爲併發式垃圾回收器和獨佔式垃圾回收器。
按碎片處理方式
分,可分爲壓縮武垃圾回收器和非壓縮式垃圾回收器。
按工做的內存區間
分,又可分爲年輕代垃圾回收器和老年代垃圾回收器。
吞吐量、暫停時間、內存佔用 這三者共同構成一個「不可能三角」。三者整體的表現會隨着技術進步而愈來愈好。一款優秀的收集器一般最多同時知足其中的兩項。
這三項裏,暫停時間的重要性日益凸顯。由於隨着硬件發展,內存佔用多些愈來愈能容忍,硬件性能的提高也有助於下降收集器運行時對應用程序的影響,即提升了吞吐量。而內存的擴大,對延遲反而帶來負面效果。 簡單來講,主要抓住兩點:
吞吐量就是 CPU 用於運行用戶代碼的時間與 CPU 總消耗時間的比值,即吞吐量=運行用戶代碼時間 /(運行用戶代碼時間+垃圾收集時間)。
好比:虛擬機總共運行了 100 分鐘,其中垃圾收集花掉 1 分鐘,那吞吐量就是 99%。
這種狀況下,應用程序能容忍較高的暫停時間,所以,高吞吐量的應用程序有更長的時間基準,快速響應是沒必要考慮的。
吞吐量優先,意味着在單位時間內,STW 的時間最短:0.2+0.2=0.4
「暫停時間」是指一個時間段內應用程序線程暫停,讓 GC 線程執行的狀態。
例如,GC 期間 100 毫秒的暫停時間意味着在這 100 毫秒期間內沒有應用程序線程是活動的。暫停時間優先,意味着儘量讓單次 STW 的時間最短:0.1+0.1 + 0.1+ 0.1+ 0.1=0.5
吞吐量 VS 暫停時間
有時候甚至短暫的200毫秒暫停均可能打斷終端用戶體驗
。所以,具備低的較大暫停時間是很是重要的,特別是對於一個交互式應用程序
。不幸的是」高吞吐量」和」低暫停時間」是一對相互競爭的目標(矛盾)。
只能頻繁地執行內存回收
,但這又引發了年輕代內存的縮減和致使程序吞吐量的降低。在設計(或使用)GC 算法時,咱們必須肯定咱們的目標:一個 GC 算法只可能針對兩個目標之一(即只專一於較大吞吐量或最小暫停時間),或嘗試找到一個兩者的折衷。
如今標準:在最大吞吐量優先的狀況下,下降停頓時間
。
7 種經典的垃圾收集器
7 種經典的垃圾收集器與垃圾分代之間的關係
垃圾收集器的組合關係
爲何要有不少收集器,一個不夠嗎?由於 Java 的使用場景不少,移動端,服務器等。因此就須要針對不一樣的場景,提供不一樣的垃圾收集器,提升垃圾收集的性能。
雖然咱們會對各個收集器進行比較,但並不是爲了挑選一個最好的收集器出來。沒有一種放之四海皆準、任何場景下都適用的完美收集器存在,更加沒有萬能的收集器。因此咱們選擇的只是對具體應用最合適的收集器。
-XX:+PrintcommandLineFlags:查看命令行相關參數(包含使用的垃圾收集器)
使用命令行指令:jinfo -flag 相關垃圾回收器參數 進程 ID
Serial收集器採用複製算法、串行回收和「Stop-The-World」機制的方式執行內存回收
。Serial Old收集器也採用了串行回收和「Stop-The-World」機制,只不過內存回收算法使用的是標記-壓縮算法
。Serial Old 在 server 模式下主要由兩個用途:
這個收集器是一個單線程的收集器,但它的「單線程」的意義並不只僅說明它只會使用一個 CPU 或一條收集線程去完成垃圾收集工做,更重要的是在它進行垃圾收集時,必須暫停其餘全部的工做線程,直到它收集結束(Stop The World)
。
優點:簡單而高效(與其餘收集器的單線程比),對於限定單個 CPU 的環境來講,Serial 收集器因爲沒有線程交互的開銷,專心作垃圾收集天然能夠得到最高的單線程收集效率。
運行在 client 模式下的虛擬機是個不錯的選擇。
在用戶的桌面應用場景中,可用內存通常不大(幾十 MB 至一兩百 MB),能夠在較短期內完成垃圾收集(幾十 ms 至一百多 ms),只要不頻繁發生,使用串行回收器是能夠接受的。
在 HotSpot 虛擬機中,使用-XX:+UseSerialGC 參數能夠指定年輕代和老年代都使用串行收集器。
等價於新生代用 Serial GC,且老年代用 Serial old GC。
總結
這種垃圾收集器你們瞭解,如今已經不用串行的了。並且在限定單核 cpu 才能夠用。如今都不是單核的了。
對於交互較強的應用而言,這種垃圾收集器是不能接受的。通常在 Java web 應用程序中是不會採用串行垃圾收集器的。
若是說 serialGC 是年輕代中的單線程垃圾收集器,那麼 ParNew 收集器則是 serial 收集器的多線程版本。
Par 是 Parallel 的縮寫,New:只能處理的是新生代。
ParNew 收集器除了採用並行回收的方式執行內存回收外,兩款垃圾收集器之間幾乎沒有任何區別。ParNew 收集器在年輕代中一樣也是採用複製算法、"Stop-The-World"機制
。
ParNew 是不少 JVM 運行在 Server 模式下新生代的默認垃圾收集器。
目前只有 ParNew GC 能與 CMS 收集器配合工做
在程序中,開發人員能夠經過選項"-XX:+UseParNewGC"手動指定使用 ParNew 收集器執行內存回收任務。它表示年輕代使用並行收集器,不影響老年代。
-XX:ParallelGCThreads 限制線程數量,默認開啓和 CPU 數據相同的線程數。
HotSpot 的年輕代中除了擁有 ParNew 收集器是基於並行回收的之外,Parallel Scavenge 收集器一樣也採用了複製算法、並行回收和"Stop The World"機制
。
那麼 Parallel 收集器的出現是否畫蛇添足?
可控制的吞吐量(Throughput)
,它也被稱爲吞吐量優先的垃圾收集器。高吞吐量則能夠高效率地利用 CPU 時間,儘快完成程序的運算任務,主要適合在後臺運算而不須要太多交互的任務
。所以,常見在服務器環境中使用。例如,那些執行批量處理、訂單處理、工資支付、科學計算的應用程序。
Parallel 收集器在 JDK1.6 時提供了用於執行老年代垃圾收集的 Parallel Old 收集器,用來代替老年代的 Serial Old 收集器。
Parallel Old 收集器採用了標記-壓縮算法
,但一樣也是基於並行回收和"Stop-The-World"機制
。
在程序吞吐量優先的應用場景中,Parallel 收集器和 Parallel Old 收集器的組合,在 server 模式下的內存回收性能很不錯。在 Java8 中,默認是此垃圾收集器。
參數配置
-XX:+UseParallelGC
手動指定年輕代使用 Parallel 並行收集器執行內存回收任務。-XX:+UseParallelOldGC
手動指定老年代使用並行回收收集器。
-XX:ParallelGCThreads
設置年輕代並行收集器的線程數。通常最好與 CPU 數量相等,以免過多的線程數影響垃圾收集性能。
-XX:MaxGCPauseMillis
設置垃圾收集器最大停頓時間(即 STW 的時間)。單位是毫秒。
須要謹慎使用該參數
。-XX:GCTimeRatio
垃圾收集時間佔總時間的比例(= 1 / (N + 1))。
-XX:+UseAda[tiveSizePolicy
設置 Parallel Scavenge 收集器具備自適應調節策略
在 JDK1.5 時期,Hotspot 推出了一款在強交互應用
中幾乎可認爲有劃時代意義的垃圾收集器:cMS(Concurrent-Mark-Sweep)收集器,這款收集器是HotSpot虛擬機中第一款真正意義上的併發收集器,它第一次實現了讓垃圾收集線程與用戶線程同時工做
。
CMS 收集器的關注點是儘量縮短垃圾收集時用戶線程的停頓時間。停頓時間越短(低延遲)就越適合與用戶交互的程序,良好的響應速度能提高用戶體驗。
目前很大一部分的 Java 應用集中在互聯網站或者 B/S 系統的服務端上,這類應用尤爲重視服務的響應速度,但願系統停頓時間最短,以給用戶帶來較好的體驗。CMS 收集器就很是符合這類應用的需求。
CMS 的垃圾收集算法採用標記-清除算法
,而且也會"Stop-The-World"。
不幸的是,CMS 做爲老年代的收集器,卻沒法與 JDK1.4.0 中已經存在的新生代收集器 Parallel Scavenge 配合工做,因此在 JDK1.5 中使用 CMS 來收集老年代的時候,新生代只能選擇 ParNew 或者 Serial 收集器中的一個。
在 G1 出現以前,CMS 使用仍是很是普遍的。一直到今天,仍然有不少系統使用 CMS GC。
CMS 整個過程比以前的收集器要複雜,整個過程分爲 4 個主要階段,即初始標記階段、併發標記階段、從新標記階段和併發清除階段。(涉及 STW 的階段主要是:初始標記 和 從新標記)
初始標記
(Initial-Mark)階段:在這個階段中,程序中全部的工做線程都將會由於"Stop-The-World"機制而出現短暫的暫停,這個階段的主要任務僅僅只是標記GC Roots能直接關聯到的對象
。一旦標記完成以後就會恢復以前被暫停的全部應用線程,因爲直接關聯對象比較小,因此這個階段速度很是快
。併發標記
(Concurrent-Mark)階段:從 GC Roots 的直接關聯對象開始遍歷整個對象圖的過程
,這個過程耗時較長
可是不須要停頓用戶線程
,能夠與垃圾收集線程一塊兒併發運行。從新標記
(Remark)階段:因爲在併發標記階段中,程序的工做線程會和垃圾收集線程同時運行或者交叉運行,所以爲了修正併發標記期間,因用戶程序繼續運做而致使標記產生變更的那一部分對象的標記記錄
,這個階段的停頓時間一般會比初始標記階段稍長一些,但也遠比並發標記階段的時間短。併發清除
(Concurrent-Sweep)階段:此階段清理刪除掉標記階段判斷的已經死亡的對象,釋放內存空間
。因爲不須要移動存活對象,因此這個階段也是能夠與用戶線程同時併發的。儘管 CMS 收集器採用的是併發回收(非獨佔式),可是在其初始化標記和再次標記這兩個階段中仍然須要執行「Stop-the-World」機制
暫停程序中的工做線程,不過暫停時間並不會太長,所以能夠說明目前全部的垃圾收集器都作不到徹底不須要「Stop-The-World」,只是儘量地縮短暫停時間。
因爲最耗費時間的併發標記與併發清除階段都不須要暫停工做,因此總體的回收是低停頓的。
另外,因爲在垃圾收集階段用戶線程沒有中斷,因此在CMS回收過程當中,還應該確保應用程序用戶線程有足夠的內存可用
。所以,CMS 收集器不能像其餘收集器那樣等到老年代幾乎徹底被填滿了再進行收集,而是當堆內存使用率達到某一閾值時,便開始進行回收
,以確保應用程序在 CMS 工做過程當中依然有足夠的空間支持應用程序運行。要是 CMS 運行期間預留的內存沒法知足程序須要,就會出現一次「Concurrent Mode Failure」 失敗,這時虛擬機將啓動後備預案:臨時啓用 Serial Old 收集器來從新進行老年代的垃圾收集,這樣停頓時間就很長了。
CMS 收集器的垃圾收集算法採用的是標記清除算法
,這意味着每次執行完內存回收後,因爲被執行內存回收的無用對象所佔用的內存空間極有多是不連續的一些內存塊,不可避免地將會產生一些內存碎片。那麼 CMS 在爲新對象分配內存空間時,將沒法使用指針碰撞(Bump the Pointer)技術,而只可以選擇空閒列表(Free List)執行內存分配。
CMS 爲何不使用標記壓縮算法?
答案其實很簡答,由於當併發清除的時候,用 Compact 整理內存的話,原來的用戶線程使用的內存還怎麼用呢?要保證用戶線程能繼續執行,前提的它運行的資源不受影響。Mark Compact 更適合「Stop The World」 這種場景下使用。
優勢
缺點
會產生內存碎片
,致使併發清除後,用戶線程可用的空間不足。在沒法分配大對象的狀況下,不得不提早觸發 FullGC。CMS收集器對CPU資源很是敏感
。在併發階段,它雖然不會致使用戶停頓,可是會由於佔用了一部分線程而致使應用程序變慢,總吞吐量會下降。CMS收集器沒法處理浮動垃圾
。可能出現「Concurrent Mode Failure"失敗而致使另外一次 Full GC 的產生。在併發標記階段因爲程序的工做線程和垃圾收集線程是同時運行或者交叉運行的,那麼在併發標記階段若是產生新的垃圾對象,CMS 將沒法對這些垃圾對象進行標記,最終會致使這些新產生的垃圾對象沒有被及時回收
,從而只能在下一次執行 GC 時釋放這些以前未被回收的內存空間。參數配置
-XX:+UseConcMarkSweepGC
手動指定使用 CMS 收集器執行內存回收任務。開啓該參數後會自動將-XX:+UseParNewGC 打開。即:ParNew(Young 區用)+CMS(Old 區用)+Serial Old 的組合。-XX:CMSInitiatingoccupanyFraction
設置堆內存使用率的閾值,一旦達到該閾值,便開始進行回收。JDK5 及之前版本的默認值爲 68,即當老年代的空間使用率達到 68%時,會執行一次 CMS 回收。JDK6 及以上版本默認值爲 92%
若是內存增加緩慢,則能夠設置一個稍大的值,大的閥值能夠有效下降 CMS 的觸發頻率,減小老年代回收的次數能夠較爲明顯地改善應用程序性能。反之,若是應用程序內存使用率增加很快,則應該下降這個閾值,以免頻繁觸發老年代串行收集器。所以經過該選項即可以有效下降 Full GC 的執行次數
。
-XX:+UseCMSCompactAtFullCollection
用於指定在執行完 Full GC 後對內存空間進行壓縮整理,以此避免內存碎片的產生。不過因爲內存壓縮整理過程沒法併發執行,所帶來的問題就是停頓時間變得更長了。-XX:CMSFullGCsBeforecompaction
設置在執行多少次 Full GC 後對內存空間進行壓縮整理。-XX:ParallelCMSThreads
設置 CMS 的線程數量。CMS 默認啓動的線程數是(ParallelGCThreads+3)/4,ParallelGCThreads 是年輕代並行收集器的線程數。當 CPU 資源比較緊張時,受到 CMS 收集器線程的影響,應用程序的性能在垃圾回收階段可能會很是糟糕。
小結
HotSpot 有這麼多的垃圾回收器,那麼若是有人問,Serial GC、Parallel GC、Concurrent Mark Sweep GC 這三個 GC 有什麼不一樣呢?
請記住如下口令:
既然咱們已經有了前面幾個強大的 GC,爲何還要發佈 Garbage First(G1)?
緣由就在於應用程序所應對的業務愈來愈龐大、複雜,用戶愈來愈多,沒有 GC 就不能保證應用程序正常進行,而常常形成 STW 的 GC 又跟不上實際的需求,因此纔會不斷地嘗試對 GC 進行優化。G1(Garbage-First)垃圾回收器是在 Java7 update4 以後引入的一個新的垃圾回收器,是當今收集器技術發展的最前沿成果之一。
與此同時,爲了適應如今不斷擴大的內存和不斷增長的處理器數量
,進一步下降暫停時間(pause time),同時兼顧良好的吞吐量。
官方給 G1 設定的目標是在延遲可控的狀況下得到儘量高的吞吐量,因此才擔當起「全功能收集器」的重任與指望。
爲何名字叫 Garbage First(G1)呢?
由於 G1 是一個並行回收器,它把堆內存分割爲不少不相關的區域(Region)(物理上不連續的)。使用不一樣的 Region 來表示 Eden、倖存者 0 區,倖存者 1 區,老年代等。
G1 GC 有計劃地避免在整個 Java 堆中進行全區域的垃圾收集。G1 跟蹤各個 Region 裏面的垃圾堆積的價值大小(回收所得到的空間大小以及回收所需時間的經驗值),在後臺維護一個優先列表,每次根據容許的收集時間,優先回收價值最大的 Region。
因爲這種方式的側重點在於回收垃圾最大量的區間(Region),因此咱們給 G1 一個名字:垃圾優先(Garbage First)。
G1(Garbage-First)是一款面向服務端應用的垃圾收集器,主要針對配備多核 CPU 及大容量內存的機器
,以極高機率知足 GC 停頓時間的同時,還兼具高吞吐量的性能特徵。
在 JDK1.7 版本正式啓用,移除了 Experimental 的標識,是 JDK9 之後的默認垃圾回收器
,取代了 CMS 回收器以及 Parallel+Parallel Old 組合。被 oracle 官方稱爲「全功能的垃圾收集器
」。
與此同時,CMS 已經在 JDK9 中被標記爲廢棄(deprecated)。在 jdk8 中還不是默認的垃圾回收器,須要使用-XX:+UseG1GC 來啓用。
G1 垃圾收集器的優勢
與其餘 GC 收集器相比,G1 使用了全新的分區算法,其特色以下所示:
並行與併發
分代收集
G1依然屬於分代型垃圾回收器
,它會氣氛年輕代和老年代,年輕代依然有 Eden 區和 Survivor 區,但從堆結構上看,它不要求整個 Eden 區、年輕代、或者老年代都是連續的,也再也不堅持固定大小和固定數量。堆空間分爲若干個區域(Region)
,這些區域中包含了邏輯上的年輕代和老年代。兼顧年輕代和老年代
。對比其它收集器,或者工做在年輕代,或者工做在老年代。G1 的分代,已經不是下面這樣的了
而是以下圖這樣的一個區域
空間整合
Region之間使用的是複製算法
,但總體上實際可看做是標記-壓縮(Mark-Compact)算法
,兩種算法均可以免內存碎片,這種特性有利於程序長時間運行,分配大對象時不會由於沒法找到連續內存空間而提早觸發 GC,尤爲是當 Java 堆很是大的時候,G1 的優點更加明顯。可預測的停頓時間模型(即:軟實時 soft real-time)
這是 G1 相對於 CMS 的另外一大優點,G1 除了追求低停頓外,還能創建可預測的停頓時間模型,能讓使用者明確指定在一個長度爲 M 毫秒的時間片斷內,消耗在垃圾收集上的時間不得超過 N 毫秒。
每次根據容許的收集時間,優先回收價值最大的 Region
。保證了 G1 收集器在有限的時間內能夠獲取儘量高的收集效率。G1 垃圾收集器的缺點
相較於 CMS,G1 還不具有全方位、壓倒性優點。好比在用戶程序運行過程當中,G1 不管是爲了垃圾收集產生的內存佔用(Footprint)仍是程序運行時的額外執行負載(overload)都要比 CMS 要高。
從經驗上來講,在小內存應用上 CMS 的表現大機率會優於 G1,而 G1 在大內存應用上則發揮其優點。平衡點在 6-8GB 之間。
G1 參數設置
-XX:+UseG1GC
:手動指定使用 G1 垃圾收集器執行內存回收任務。-XX:G1HeapRegionSize
設置每一個 Region 的大小。值是 2 的冪,範圍是 1MB 到 32MB 之間,目標是根據最小的 Java 堆大小劃分出約 2048 個區域。默認是堆內存的 1/2000。-XX:MaxGCPauseMillis
設置指望達到的最大 Gc 停頓時間指標(JVM 會盡力實現,但不保證達到)。默認值是 200ms。-XX:+ParallelGcThread
設置 STW 工做線程數的值。最多設置爲 8。-XX:ConcGCThreads
設置併發標記的線程數。將 n 設置爲並行垃圾回收線程數(ParallelGcThreads)的 1/4 左右。-XX:InitiatingHeapoccupancyPercent
設置觸發併發 Gc 週期的 Java 堆佔用率閾值。超過此值,就觸發 GC。默認值是 45。G1 收集器的常見操做步驟
G1 的設計原則就是簡化 JVM 性能調優,開發人員只須要簡單的三步便可完成調優:
第一步:開啓 G1 垃圾收集器
第二步:設置堆的最大內存
第三步:設置最大的停頓時間
G1 中提供了三種垃圾回收模式:Young GC、Mixed GC 和 Full GC,在不一樣的條件下被觸發。
G1 收集器的適用場景
用來替換掉 JDK1.5 中的 CMS 收集器;在下面的狀況時,使用 G1 可能比 CMS 好:
分區 Region:化整爲零
使用 G1 收集器時,它將整個 Java 堆劃分紅約 2048 個大小相同的獨立 Region 塊,每一個 Region 塊大小根據堆空間的實際大小而定,總體被控制在 1MB 到 32MB 之間,且爲 2 的 N 次冪,即 1MB,2MB,4MB,8MB,16MB,32MB。能夠經過 XX:G1HeapRegionsize 設定。全部的Region大小相同,且在JVM生命週期內不會被改變。
雖然還保留有新生代和老年代的概念,但新生代和老年代再也不是物理隔離的了,它們都是一部分 Region(不須要連續)的集合。經過 Region 的動態分配方式實現邏輯上的連續。
一個 region 有可能屬於 Eden,Survivor 或者 old/Tenured 內存區域。可是一個 region 只可能屬於一個角色。圖中的 E 表示該 region 屬於 Eden 內存區域,s 表示屬於 survivor 內存區域,o 表示屬於 Old 內存區域。圖中空白的表示未使用的內存空間。
G1 垃圾收集器還增長了一種新的內存區域,叫作 Humongous 內存區域,如圖中的 H 塊。主要用於存儲大對象,若是超過 1.5 個 region,就放到 H。
設置 H 的緣由
對於堆中的大對象,默認直接會被分配到老年代,可是若是它是個短時間存在的大對象,就會對垃圾收集器形成負面影響。爲了解決這個問題,G1 劃分了一個 Humongous 區,它用來專門存放大對象。若是一個 H 區裝不下一個大對象,那麼 G1 會尋找連續的 H 區來存儲。爲了能找到連續的H區,有時候不得不啓動 Full GC
。G1 的大多數行爲都把 H 區做爲老年代的一部分來看待。
每一個 Region 都是經過指針碰撞來分配空間,還能夠爲每一個線程分配 TLAB。
G1 垃圾回收器的回收過程
G1 GC 的垃圾回收過程主要包括以下三個環節:
應用程序分配內存,當年輕代的Eden區用盡時開始年輕代回收過程
;G1 的年輕代收集階段是一個並行
的獨佔式
收集器。在年輕代回收期,G1 GC 暫停全部應用程序線程,啓動多線程執行年輕代回收。而後從年輕代區間移動存活對象到Survivor區間或者老年區間,也有多是兩個區間都會涉及
。
當堆內存使用達到必定值(默認 45%)時,開始老年代併發標記過程。
標記完成立刻開始混合回收過程。對於一個混合回收期,G1 GC 從老年區間移動存活對象到空閒區間,這些空閒區間也就成爲了老年代的一部分。和年輕代不一樣,老年代的 G1 回收器和其餘 GC 不一樣,G1 的老年代回收器不須要整個老年代被回收,一次只須要掃描/回收一小部分老年代的 Region 就能夠了
。同時,這個老年代 Region 是和年輕代一塊兒被回收的。
Remembered Set(記憶集)
一個對象被不一樣區域引用的問題
一個 Region 不多是孤立的,一個 Region 中的對象可能被其餘任意 Region 中對象引用,判斷對象存活時,是否須要掃描整個 Java 堆才能保證準確?
在其餘的分代收集器,也存在這樣的問題(而 G1 更突出)回收新生代也不得不一樣時掃描老年代?這樣的話會下降 MinorGC 的效率;
解決方法:
每一個 Region 都有一個對應的 Remembered Set;
G1 回收過程一:年輕代 GC
JVM 啓動時,G1 先準備好 Eden 區,程序在運行過程當中不斷建立對象到 Eden 區,當 Eden 空間耗盡時,G1 會啓動一次年輕代垃圾回收過程。
年輕代垃圾回收只會回收Eden區和Survivor區。
Young GC 時,首先 G1 中止應用程序的執行(STW),G1 建立回收集(Collection Set),回收集是指須要被回收的內存分段的集合,年輕代回收過程的回收集包含年輕代 Eden 區和 Survivor 區全部的內存分段。
而後開始以下回收過程:
根是指 static 變量指向的對象,正在執行的方法調用鏈條上的局部變量等。根引用連同 RSet 記錄的外部引用做爲掃描存活對象的入口。
處理 dirty card queue 中的 card,更新 RSet。此階段完成後,RSet能夠準確的反映老年代對所在的內存分段中對象的引用
。
對於應用程序的引用賦值語句 object.field=object,JVM 會在以前和以後執行特殊的操做以在 dirty card queue 中入隊一個保存了對象引用信息的 card。在年輕代回收的時候,G1 會對 dirty card queue 中全部的 card 進行處理,以更新 RSet,保證 RSet 實時準確的反映引用關係。那爲何不在引用賦值語句處直接更新 RSet 呢?這是爲了性能的須要,RSet 的處理須要線程同步,開銷會很大,使用隊列性能會好不少。
識別被老年代對象指向的 Eden 中的對象,這些被指向的 Eden 中的對象被認爲是存活的對象。
此階段,對象樹被遍歷,Eden 區內存段中存活的對象會被複制到 Survivor 區中空的內存分段,Survivor 區內存段中存活的對象若是年齡未達閾值,年齡會加 1,達到閥值會被會被複制到 o1d 區中空的內存分段。若是 Survivor 空間不夠,Eden 空間的部分數據會直接晉升到老年代空間。
處理 Soft,Weak,Phantom,Final,JNI Weak 等引用。最終 Eden 空間的數據爲空,GC 中止工做,而目標內存中的對象都是連續存儲的,沒有碎片,因此複製過程能夠達到內存整理的效果,減小碎片。
G1 回收過程二:併發標記過程
標記從根節點直接可達的對象。這個階段是 STW 的,而且會觸發一次年輕代 GC。
G1 GC 掃描 survivor 區直接可達的老年代區域對象,並標記被引用的對象。這一過程必須在 Young GC 以前完成。
在整個堆中進行併發標記(和應用程序併發執行),此過程可能被 Young GC 中斷。在併發標記階段,若發現區域對象中的全部對象都是垃圾,那這個區域會被當即回收。
同時,併發標記過程當中,會計算每一個區域的對象活性(區域中存活對象的比例)。
因爲應用程序持續進行,須要修正上一次的標記結果。是 STW 的。G1 中採用了比 CMS 更快的初始快照算法:snapshot-at-the-beginning(SATB)。
計算各個區域的存活對象和 GC 回收比例,並進行排序,識別能夠混合回收的區域。爲下階段作鋪墊。是 STW 的。這個階段並不會實際上去作垃圾的收集。
識別並清理徹底空閒的區域。
G1 回收過程三: 混合回收
當愈來愈多的對象晉升到老年代 Old Region 時,爲了不堆內存被耗盡,虛擬機會觸發一個混合的垃圾收集器,即 Mixed GC,該算法並非一個 Old GC,除了回收整個 Young Region,還會回收一部分的 Old Region。這裏須要注意:是一部分老年代,而不是所有老年代
。能夠選擇哪些 Old Region 進行收集,從而能夠對垃圾回收的耗時時間進行控制。也要注意的是 Mixed GC 並非 Full GC。
併發標記結束之後,老年代中百分百爲垃圾的內存分段被回收了,部分爲垃圾的內存分段被計算了出來。默認狀況下,這些老年代的內存分段會分 8 次(能夠經過-XX:G1MixedGCCountTarget 設置)被回收
混合回收的回收集(Collection Set)包括八分之一的老年代內存分段,Eden 區內存分段,Survivor 區內存分段。混合回收的算法和年輕代回收的算法徹底同樣,只是回收集多了老年代的內存分段。具體過程請參考上面的年輕代回收過程。
因爲老年代中的內存分段默認分 8 次回收,G1 會優先回收垃圾多的內存分段。垃圾佔內存分段比例越高的,越會被先回收
。而且有一個閾值會決定內存分段是否被回收,
-XX:G1MixedGCLiveThresholdPercent,默認爲 65%,意思是垃圾佔內存分段比例要達到 65%纔會被回收。若是垃圾佔比過低,意味着存活的對象佔比高,在複製的時候會花費更多的時間。
混合回收並不必定要進行 8 次。有一個閾值-XX:G1HeapWastePercent,默認值爲 10%,意思是容許整個堆內存中有 10%的空間被浪費,意味着若是發現能夠回收的垃圾佔堆內存的比例低於 10%,則再也不進行混合回收。由於 GC 會花費不少的時間可是回收到的內存卻不多。
G1 回收過程三: Full GC
G1 的初衷就是要避免 Full GC 的出現。可是若是上述方式不能正常工做,G1 會中止應用程序的執行
(Stop-The-World),使用單線程的內存回收算法進行垃圾回收,性能會很是差,應用程序停頓時間會很長。
要避免 Full GC 的發生,一旦發生須要進行調整。何時會發生 Full GC 呢?好比堆內存過小
,當 G1 在複製存活對象的時候沒有空的內存分段可用,則會回退到 Full GC,這種狀況能夠經過增大內存解決。
致使 G1 Full GC 的緣由可能有兩個:
截止 JDK1.8,一共有 7 款不一樣的垃圾收集器。每一款的垃圾收集器都有不一樣的特色,在具體使用的時候,須要根據具體的狀況選用不一樣的垃圾收集器。
GC 發展階段:Seria l=> Parallel(並行)=> CMS(併發)=> G1 => ZGC
不一樣廠商、不一樣版本的虛擬機實現差距比較大。HotSpot 虛擬機在 JDK7/8 後全部收集器及組合以下圖
怎麼選擇垃圾回收器
Java 垃圾收集器的配置對於 JVM 優化來講是一個很重要的選擇,選擇合適的垃圾收集器可讓 JVM 的性能有一個很大的提高。怎麼選擇垃圾收集器?
最後須要明確一個觀點:
沒有最好的收集器,更沒有萬能的收集;調優永遠是針對特定場景、特定需求,不存在一勞永逸的收集器。
經過閱讀 Gc 日誌,咱們能夠了解 Java 虛擬機內存分配與回收策略。 內存分配與垃圾回收的參數列表
-XX:+PrintGC
輸出 GC 日誌。相似:-verbose:gc
-XX:+PrintGCDetails
輸出 GC 的詳細日誌-XX:+PrintGCTimestamps
輸出 GC 的時間戳(以基準時間的形式)-XX:+PrintGCDatestamps
輸出 GC 的時間戳-XX:+PrintHeapAtGC
在進行 GC 的先後打印出堆的信息-Xloggc:../logs/gc.log
日誌文件的輸出路徑-verbose:gc
打開 GC 日誌:
-verbose:gc
這個只會顯示總的 GC 堆的變化,以下:
參數解析:
PrintGCDetails
打開 GC 日誌:
-verbose:gc -XX:+PrintGCDetails
輸入信息以下:
參數解析:
Allocation Failure 代表本次引發 GC 的緣由是由於在年輕代中沒有足夠的空間可以存儲新的數據了。
Young GC Detail
Full GC Detail
GC 回收舉例
public class GCUseTest { static final Integer _1MB = 1024 * 1024; public static void main(String[] args) { byte [] allocation1, allocation2, allocation3, allocation4; allocation1 = new byte[2 *_1MB]; allocation2 = new byte[2 *_1MB]; allocation3 = new byte[2 *_1MB]; allocation4 = new byte[4 *_1MB]; } }
-Xms20m -Xmx20m -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:UseSerialGC
首先咱們會將 3 個 2M 的數組存放到 Eden 區,而後後面 4M 的數組來了後,將沒法存儲,由於 Eden 區只剩下 2M 的剩餘空間了,那麼將會進行一次 Young GC 操做,將原來 Eden 區的內容,存放到 Survivor 區,可是 Survivor 區也存放不下,那麼就會直接晉級存入 Old 區。
而後咱們將 4M 對象存入到 Eden 區中。
ZGC 與 shenandoah 目標高度類似,在儘量對吞吐量影響不大的前提下,實如今任意堆內存大小下均可以把垃圾收集的停頗時間限制在十毫秒之內的低延遲。
《深刻理解 Java 虛擬機》一書中這樣定義 ZGC:ZGC 收集器是一款基於 Region 內存佈局的,(暫時)不設分代的,使用了讀屏障、染色指針和內存多重映射等技術來實現可併發的標記-壓縮算法
的,以低延遲爲首要目標的一款垃圾收集器。
ZGC 的工做過程能夠分爲 4 個階段:併發標記 - 併發預備重分配 - 併發重分配 - 併發重映射
等。
ZGC 幾乎在全部地方併發執行的,除了初始標記的是 STW 的。因此停頓時間幾乎就耗費在初始標記上,這部分的實際時間是很是少的。
吞吐量對比:
停頓時間對比:
JDK14 以前,ZGC 僅 Linux 才支持。
儘管許多使用 ZGC 的用戶都使用類 Linux 的環境,但在 Windows 和 Mac OS 上,人們也須要 ZGC 進行開發部署和測試。許多桌面應用也能夠從 ZGC 中受益。所以,ZGC 特性被移植到了 Windows 和 Mac OS 上。
如今 Mac 或 Windows 上也能使用 ZGC 了,示例以下:
-XX:+UnlockExperimentalVMOptions-XX:+UseZGC