JVM垃圾回收概述

JVM類加載概述
JVM運行時數據區概述java

概述

垃圾收集機制是 Java 的招牌能力,極大的提升了開發效率。現在,垃圾收集幾乎成爲了現代語言的標配,即便通過了如此長時間的發展,Java 的垃圾收集機制仍然在不斷的演進中,不一樣大小的設備、不一樣特徵的應用場景都對垃圾收集提出了新的挑戰,也是面試的熱門考點web

什麼是垃圾

垃圾是指在運行程序中沒有任何指針指向的對象,這個對象就是須要被回收的垃圾。面試

若是不及時對內存中的垃圾進行清理,那麼這些垃圾所佔的內存空間會一直保留到應用程序結束,被保留的空間沒法被其它對象所使用,甚至可能致使內存溢出算法

爲何須要 GC

  • 對於高級語言來講,不進行垃圾回收,內存早晚都會被消耗完
  • 釋放沒用的獨享,垃圾回收也能夠清除內存裏的記錄碎片,碎片整理將全部佔用的堆內存到堆的一段,以便JVM將整理出的內存分配給的新的對象
  • 隨着應付業務愈來愈龐大、複雜、用戶愈來愈多,沒有GC就不能保證應用程序的正常進行,常常形成 STW 的 GC 又跟不上實際的需求,因此纔會不短地嘗試對 GC 進行優化。

Java 自動內存管理介紹

自動內存管理,無需開發人員手動參與內存的分配與回收,下降內存泄漏和內存溢出的風險數據庫

自動內存管理機制,將開發人員從繁重的內存管理中釋放出來,能夠更專一與業務開發segmentfault

壞處數組

  • 對於 Java 開發人員而言,自動內存管理就像一個黑匣子,若是過分依賴於自動內存管理,那麼可能會弱化開發人員在程序中出現內存溢出時定位問題和解決問題的能力
  • 因此瞭解 JVM 的自動內存分配和內存回收原理就顯得很是重要,只有在真正瞭解 JVM 是如何管理內存後,咱們纔可以在碰見 OutOfMemoryError 時,快速的根據錯誤日誌定位問題和解決問題。
  • 當須要排查各類內存溢出、內存泄露問題時、當垃圾收集成爲系統併發量的瓶頸時,咱們就必須對"自動化"技術實施必需要的監控和調優。

GC 發生的區域緩存

垃圾回收器能夠對年輕代回收,也能夠對老年代回收,甚至是整個堆和方法區的回收。安全

其中 Java堆是垃圾收集器的工做重點服務器

從次數上講:

  • 頻繁收集 Young 區
  • 較少收集 Old 區
  • 基本不收集 Perm 區(元空間)

垃圾回收相關算法

垃圾標記階段之引用計數算法

垃圾標記階段:對象存活判斷

  • 在堆裏存放着幾乎全部的 Java 對象實例,在 GC 執行垃圾回收以前,首先須要區分出內存中哪些是存活對象,哪些是已經死亡的對象,只有被標記已經死亡的對象,GC 纔會執行垃圾回收時,釋放掉其所佔內存空間,所以這個過程咱們能夠稱爲垃圾標記階段。
  • 那麼在 JVM 中是如何標記一個死亡對象呢?簡單來講,就是當一個對象已經再也不被任何的存活對象繼續引用時,就能夠宣判死亡。
  • 判斷對象存活通常有兩種方式:引用計數算法可達性分析算法

引用計數算法

  • 引用計數算法(Reference Counting)比較簡單,對每一個對象保存一個整型的引用計數器屬性,用於記錄對象被引用的狀況。
  • 對於一個對象 A,只要有任何一個對象引用了 A,則 A 的引用計數器就加 1;當引用失效的時候就減 1。只要對象 A 的引用計數器爲 0,即表示對象 A 不可能在被使用,則能夠進行回收。

優勢

  • 實現簡單,垃圾對象便於辨識,斷定效率高,回收沒有延遲性。

缺點

  • 須要單獨的字段存儲計數器,這樣的作法增長了存儲空間的開銷
  • 每次賦值都要更新計數器,增長了時間開銷
  • 引用計數器有個嚴重的問題,即沒法處理循環引用的狀況。這個問題是致命的,因此致使在 Java 的垃圾回收器中沒有使用這類算法。

垃圾標記階段之可達性分析算法

相對於引用計數算法而言,可達性分析算法不只一樣具有實現簡單和執行高效等特色,更重要的是該算法能夠有效地解決在引用計數算法中循環引用的問題,防止內存泄漏的發生

這樣類型的垃圾收集一般也叫作追蹤性垃圾收集(Tracing Garbage Collection)

所謂GC Roots根集合就是一組必須活躍的引用。

基本思路:

  • 可達性分析算法是以根對象集合(GC Roots)爲起始點,按照從上至下的方式搜索被根對象集合所鏈接的目標對象是否可達
  • 使用可達性分析算法後,內存中的存貨對象都會被根對象集合直接或間接鏈接着,搜索所走過的路徑稱爲引用鏈(Reference Chain)
  • 若是目標對象沒有任何引用鏈相連,則說明是不可達的,意思着該對象已經死亡,能夠標記爲垃圾對象。
  • 在可達性分析算法中,只有可以被根對象集合直接或者間接鏈接的對象纔是存活對象。

能夠做爲 GC Roots 的對象有哪些?

  • 虛擬機棧中引用的對象,好比:各個線程被調用的方法中使用到的參數、局部變量等。
  • 本地方法棧內引用的對象。
  • 方法區中靜態屬性引用變量。
  • 方法區中常量引用的對象,好比:字符串常量池(String Table)裏的引用。
  • 全部被同步鎖 synchronized 持有的對象。
  • Java 虛擬機內部的引用。基本數據類型對象的 Class 對象,一些常駐的異常對象(如:NullPointerException、OutOfMemoryError),系統類加載器。
  • 反應 Java 虛擬機內部狀況的 JMXBean、JVMTI 中註冊的回調、本地代碼緩存等。

對象的 finalization 機制

  • Java 語言提供了對象終止(finalization)機制來容許開發人員提供對象被銷燬以前的自定義處理邏輯
  • 當垃圾回收器發現沒有引用指向一個對象,就會調用這個對象的 finalize()方法。
  • finalize()方法容許在子類中被重寫,用於在對象被回收時進行資源釋放。一般在這個方法中進行一些資源釋放和清理工做,好比關閉文件、數據庫鏈接等。
  • 不要主動的調用 finalize()方法,應該交給垃圾回收機制來調用。

緣由:

  1. finalize()可能致使對象復活。
  2. finalize()執行時間沒有保障,徹底由 GC 線程決定,極端狀況下,若不發生 GC,finalize()就不會被調用。
  3. 一個糟糕的 finalize()會影響 GC 的性能。

因爲 finalize()方法的存在,虛擬機中的對象通常處於三種可能的狀態

若是從全部的根節點都沒法訪問到某對象,說明對象已經再也不使用了。通常來講,此對象須要被回收。但事實上,也並不是是"非死不可"的,這時候它們暫時處於"緩刑"階段。一個沒法觸及的對象有可能在某一個條件下"復活"本身,若是這樣,那麼對它的回收就是不合理的,爲此,定義虛擬機中的對象可能的三種狀態。

以下:

  1. 可觸及的:從根節點開始,能夠到達這個對象。
  2. 可復活的:對象的全部引用都被釋放,可是對象有可能在 finalize()中復活。
  3. 不可觸及的:對象的 finalize()被調用,而且沒有復活,那麼就會進入不可觸及狀態,不可觸及的對象不可能被複活,由於finalize()只會被調用一次

以上三種狀態中,是因爲 finalize()方法的存在,進行的區分,只有在對象不可觸及時才能夠被回收。

具體過程

  • 判斷一個對象是否能夠回收,至少要經歷兩次標記過程:

    1. 若是對象到 GC Roots 沒有引用鏈,則進行第一次標記。
    2. 進行篩選,判斷此對象是否有必要執行 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)。
  • 複製算法(Copying)。
  • 標記-壓縮算法(Mark-Compact)。

標記-清除算法(Mark-Sweep)是一種很是基礎和常見的垃圾收集算法,該算法被 J.McCarthy 等人在 1960 年提出並並應用於 Lisp 語言。

執行過程

當堆中的有效內存空間(available memory)被耗盡的時候,就會中止整個程序(也被稱爲 stop the world),而後進行兩項工做,第一項則是標記,第二項則是清除。

  • 標記: Collector 從引用根節點開始遍歷,標記全部被引用的對象,通常是在對象的 Header 中記錄爲可達對象。
  • 清除: Collector 對堆內存從頭至尾進行線性的遍歷,若是發現某個對象在其 Header 中沒有標記爲可達對象,則將其回收。

什麼是清除?

這裏所謂的清除並非真的置空,而是把須要清除的對象地址保存在空閒的地址列表裏。下次有新對象須要加載時,判斷垃圾的位置空間是否夠,若是夠,就存放覆蓋原有的地址。

關於空閒列表:

  • 若是內存規整

    • 採用指針碰撞的方式進行內存分配
  • 若是內存不規整

    • 虛擬機須要維護一個列表
    • 空閒列表分配

缺點

  • 效率不算高。
  • 在進行 GC 的時候,須要中止整個應用程序,致使用戶體驗差。
  • 這種方式清理出來的空閒內存是不連續的,會產生內存碎片,須要維護一個空閒列表。

垃圾清除階段之複製算法

背景

爲了解決標記-清除算法在垃圾收集效率方面的缺陷,M.L.Minsky 於 1963 年發表了著名的論文,「使用雙存儲區的 Lisp 語言垃圾收集器 CA LISP Garbage Collector Algorithm Using Serial Secondary Storage)」。M.L.Minsky 在該論文中描述的算法被人們稱爲複製(Copying)算法,它也被 M.L.Minsky 本人成功地引入到了 Lisp 語言的一個實現版本中。

核心思想

將活着的內存空間分爲兩塊,每次只使用其中一塊,在垃圾回收時將正在使用的內存中的存活對象複製到未被使用的內存塊中,以後清除正在使用的內存塊中的全部對象,交換兩個內存的角色,最後完成垃圾回收。

優勢

  • 沒有標記和清除過程,實現簡單,運行高效。
  • 複製過去之後保證空間的連續性,不會出現「碎片」問題。

缺點

  • 此算法的缺點也是很明顯的,就是須要兩倍的內存空間。
  • 對於 G1 這種分拆成爲大量 region 的 GC,複製而不是移動,意味着 GC 須要維護 region 之間對象引用關係,無論是內存佔用或者時間開銷也不小。

注意

若是系統中的存活對象不少,那麼複製算法的效率就會大打折扣。理想狀態下須要複製的存活對象數量並不會太大,或者說很是低才行。

在新生代,對常規應用的垃圾回收,一次一般能夠回收 70% - 99% 的內存空間。回收性價比很高。因此如今的商業虛擬機都是用這種收集算法回收新生代。

垃圾清除階段之標記-壓縮算法

背景

複製算法的高效性是創建在存活對象少、垃圾對象多的前提下的。這種狀況在新生代常常發生,可是在老年代,更常見的狀況是大部分對象都是存活對象。若是依然使用複製算法,因爲存活對象較多,複製的成本也將很高。所以,基於老年代垃圾回收的特性,須要使用其餘的算法。

標記一清除算法的確能夠應用在老年代中,可是該算法不只執行效率低下,並且在執行完內存回收後還會產生內存碎片,因此 JvM 的設計者須要在此基礎之上進行改進。標記-壓縮(Mark-Compact)算法由此誕生。

1970 年先後,G.L.Steele、C.J.Chene 和 D.s.Wise 等研究者發佈標記-壓縮算法。在許多現代的垃圾收集器中,人們都使用了標記-壓縮算法或其改進版本。

執行過程

第一階段和標記清除算法同樣,從根節點開始標記全部被引用對象。

第二階段將全部的存活對象壓縮到內存的一端,按順序排放。以後,清理邊界外全部的空間。

標記-清除和標記-壓縮的區別
標記-壓縮算法的最終效果等同於標記-清除算法執行完成後,再進行一次內存碎片整理,所以,也能夠把它稱爲標記-清除-壓縮(Mark-Sweep-Compact)算法

兩者的本質差別在於標記-清除算法是一種非移動式的回收算法,標記-壓縮是移動式的。是否移動回收後的存活對象是一項優缺點並存的風險決策。能夠看到,標記的存活對象將會被整理,按照內存地址依次排列,而未被標記的內存會被清理掉。如此一來,當咱們須要給新對象分配內存時,JVM 只須要持有一個內存的起始地址便可,這比維護一個空閒列表顯然少了許多開銷。

優勢

  • 消除了標記-清除算法當中,內存區域分散的缺點,咱們須要給新對象分配內存時,JVM 只須要持有一個內存的起始地址便可。
  • 消除了複製算法當中,內存減半的高額代價。

缺點

  • 從效率上來講,標記-整理算法要低於複製算法。
  • 移動對象的同時,若是對象被其餘對象引用,則還須要調整引用的地址。
  • 移動過程當中,須要全程暫停用戶應用程序。即:STW。

小結

標記清除 標記壓縮 複製
速率 中等 最慢 最快
空間開銷 少(但會堆積碎片) 少(不堆積碎片) 一般須要活對象的 2 倍空間(不堆積碎片)
移動對象

效率上來講,複製算法是當之無愧的老大,可是卻浪費了太多內存。

而爲了儘可能兼顧上面提到的三個指標,標記-整理算法相對來講更平滑一些,可是效率上不盡如人意,它比複製算法多了一個標記的階段,比標記-清除多了一個整理內存的階段。

分代收集算法

前面全部這些算法中,並無一種算法能夠徹底替代其餘算法,它們都具備本身獨特的優點和特色。分代收集算法應運而生。

分代收集算法,是基於這樣一個事實:不一樣的對象的生命週期是不同的。所以,不一樣生命週期的對象能夠採起不一樣的收集方式,以便提升回收效率。通常是把 Java 堆分爲新生代和老年代,這樣就能夠根據各個年代的特色使用不一樣的回收算法,以提升垃圾回收的效率。

在 Java 程序運行的過程當中,會產生大量的對象,其中有些對象是與業務信息相關,好比Http請求中的Session對象、線程、Socket鏈接,這類對象跟業務直接掛鉤,所以生命週期比較長。可是還有一些對象,主要是程序運行過程當中生成的臨時變量,這些對象生命週期會比較短,好比:String對象,因爲其不變類的特性,系統會產生大量的這些對象,有些對象甚至只用一次便可回收。

目前幾乎全部的GC都採用分代收集算法執行垃圾回收的。

在 HotSpot 中,基於分代的概念,GC 所使用的內存回收算法必須結合年輕代和老年代各自的特色。

  • 年輕代(Young Gen)

年輕代特色:區域相對老年代較小,對象生命週期短、存活率低,回收頻繁。

這種狀況複製算法的回收整理,速度是最快的。複製算法的效率只和當前存活對象大小有關,所以很適用於年輕代的回收。而複製算法內存利用率不高的問題,經過 hotspot 中的兩個 survivor 的設計獲得緩解。

  • 老年代(Tenured Gen)

老年代特色:區域較大,對象生命週期長、存活率高,回收不及年輕代頻繁。

這種狀況存在大量存活率高的對象,複製算法明顯變得不合適。通常是由標記-清除或者是標記-清除與標記-整理的混合實現。

  • Mark 階段的開銷與存活對象的數量成正比。
  • Sweep 階段的開銷與所管理區域的大小成正相關。
  • Compact 階段的開銷與存活對象的數據成正比。

以 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()的理解

在默認狀況下,經過 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();
    }
}

內存溢出(OOM)

內存溢出相對於內存泄漏來講,儘管更容易被理解,可是一樣的,內存溢出也是引起程序崩潰的罪魁禍首之一。

因爲 GC 一直在發展,全部通常狀況下,除非應用程序佔用的內存增加速度很是快,形成垃圾回收已經跟不上內存消耗的速度,不然不太容易出現 OOM 的狀況。

大多數狀況下,GC 會進行各類年齡段的垃圾回收,實在不行了就放大招,來一次獨佔式的 Fu11GC 操做,這時候會回收大量的內存,供應用程序繼續使用。

javadoc 中對 outofMemoryError 的解釋是,沒有空閒內存,而且垃圾收集器也沒法提供更多內存

首先說沒有空閒內存的狀況:說明 Java 虛擬機的堆內存不夠。緣由以下:

  • 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。

內存泄露(Memory Leak)

嚴格來講,只有對象不會再被程序用到了,可是GC又不能回收他們的狀況,才叫內存泄漏。

但實際狀況不少時候一些不太好的實踐(或疏忽)會致使對象的生命週期變得很長甚至致使 00M,也能夠叫作寬泛意義上的「內存泄漏」。

儘管內存泄漏並不會馬上引發程序崩潰,可是一旦發生內存泄漏,程序中的可用內存就會被逐步蠶食,直至耗盡全部內存,最終出現 OutOfMemoryError,致使程序崩潰。

舉例

  • 單例模式

單例的生命週期和應用程序是同樣長的,因此單例程序中,若是持有對外部對象的引用的話,那麼這個外部對象是不能被回收的,則會致使內存泄漏的產生。

  • 一些提供 close 的資源未關閉致使內存泄漏

數據庫鏈接(dataSourse.getConnection() ),網絡鏈接(socket)和 io 鏈接必須手動 close,不然是不能被回收的。

Stop-The-World

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)

  • 相較於並行的概念,單線程執行。若是內存不夠,則程序暫停,啓動 JM 垃圾回收器進行垃圾回收。回收完,再啓動程序的線程。

併發(Concurrent)

用戶線程與垃圾收集線程同時執行(但不必定是並行的,可能會交替執行),垃圾回收線程在執行時不會停頓用戶程序的運行。>用戶程序在繼續運行,而垃圾收集程序線程運行於另外一個 CPU 上;

如:CMS、G1

安全點與安全區域

安全點(Safepoint)

程序執行時並不是在全部地方都能停頓下來開始 GC,只有在特定的位置才能停頓下來開始 GC,這些位置稱爲「安全點(Safepoint)」。

Safe Point 的選擇很重要,若是太少可能致使GC等待的時間太長,若是太頻繁可能致使運行時的性能問題。大部分指令的執行時間都很是短暫,一般會根據「是否具備讓程序長時間執行的特徵」爲標準。好比:選擇一些執行時間較長的指令做爲 Safe Point,如方法調用、循環跳轉和異常跳轉等。

如何在 cc 發生時,檢查全部線程都跑到最近的安全點停頓下來呢?

  • 搶先式中斷:(目前沒有虛擬機採用了)首先中斷全部線程。若是還有線程不在安全點,就恢復線程,讓線程跑到安全點。
  • 主動式中斷:設置一箇中斷標誌,各個線程運行到 Safe Point 的時候主動輪詢這個標誌,若是中斷標誌爲真,則將本身進行中斷掛起。(有輪詢的機制)。

安全區域(Safe Region)

Safepoint 機制保證了程序執行時,在不太長的時間內就會遇到可進入 GC 的 Safepoint。可是,程序「不執行」的時候呢?例如線程處於 sleep 狀態或 Blocked 狀態,這時候線程沒法響應 JVM 的中斷請求,「走」到安全點去中斷掛起,JVM 也不太可能等待線程被喚醒。對於這種狀況,就須要安全區域(Safe Region)來解決。

安全區域是指在一段代碼片斷中,對象的引用關係不會發生變化,在這個區域中的任何位置開始 Gc 都是安全的。咱們也能夠把 Safe Region 看作是被擴展了的 Safepoint。

執行流程:

  1. 當線程運行到 Safe Region 的代碼時,首先標識已經進入了 Safe Relgion,若是這段時間內發生 GC,JVM 會忽略標識爲 Safe Region 狀態的線程。
  2. 當線程即將離開 Safe Region 時,會檢查 JVM 是否已經完成 GC,若是完成了,則繼續運行,不然線程必須等待直到收到能夠安全離開 Safe Region 的信號爲止;

Java 中幾種不一樣引用的概述

再談引用

咱們但願能描述這樣一類對象:當內存空間還足夠時,則能保留在內存中;若是內存空間在進行垃圾收集後仍是很緊張,則能夠拋棄這些對象。

【既偏門又很是高頻的面試題】強引用、軟引用、弱引用、虛引用有什麼區別?具體使用場景是什麼? 在 JDK1.2 版以後,Java 對引用的概念進行了擴充,將引用分爲:

  • 強引用(Strong Reference)
  • 軟引用(Soft Reference)
  • 弱引用(Weak Reference)
  • 虛引用(Phantom Reference)

這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; 則原來堆中的對象也不會被回收,由於還有其它對象指向該區域。

總結

本例中的兩個引用,都是強引用,強引用具有如下特色:

  • 強引用能夠直接訪問目標對象。
  • 強引用所指向的對象在任什麼時候候都不會被系統回收,虛擬機寧願拋出 OOM 異常,也不會回收強引用所指向對象。
  • 強引用可能致使內存泄漏。

軟引用(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 時纔回收被引用的對象。

垃圾回收器

概述

  • 垃圾收集器沒有在規範中進行過多的規定,能夠由不一樣的廠商、不一樣版本的 JVM 來實現。
  • 因爲 JDK 的版本處於高速迭代過程當中,所以 Java 發展至今已經衍生了衆多的 GC 版本。
  • 從不一樣角度分析垃圾收集器,能夠將 GC 分爲不一樣的類型。

垃圾回收器分類

線程數分,可用分爲串行垃圾回收器和並行垃圾回收器。

串行回收指的是在同一時間段內只容許有一個 CPU 用於執行垃圾回收操做,此時工做線程被暫停,直至垃圾收集工做結束。

  • 在諸如單 CPU 處理器或者較小的應用內存等硬件平臺不是特別優越的場合,串行回收器的性能表現能夠超過並行回收器和併發回收器。因此,串行回收默認被應用在客戶端的Client模式下的JVM中
  • 在併發能力比較強的 CPU 上,並行回收器產生的停頓時間要短於串行回收器。

和串行回收相反,並行收集能夠運用多個 CPU 同時執行垃圾回收,所以提高了應用的吞吐量,不過並行回收仍然與串行回收同樣,採用獨佔式,使用了「Stop-The-World」機制。

按照工做模式分,能夠分爲併發式垃圾回收器和獨佔式垃圾回收器。

  • 併發式垃圾回收器與應用程序線程交替工做,以儘量減小應用程序的停頓時間。
  • 獨佔式垃圾回收器(Stop-The-World)一旦運行,就中止應用程序中的全部用戶線程,直到垃圾回收過程徹底結束。

碎片處理方式分,可分爲壓縮武垃圾回收器和非壓縮式垃圾回收器。

  • 壓縮式垃圾回收器會在回收完成後,對存活對象進行壓縮整理,消除回收後的碎片,使用指針碰撞進行分配。
  • 非壓縮式的垃圾回收器不進行這步操做,使用空閒列表進行分配。

工做的內存區間分,又可分爲年輕代垃圾回收器和老年代垃圾回收器。

評估 GC 的性能指標

  • 吞吐量:運行用戶代碼的時間佔總運行時間的比例(總運行時間 = 程序的運行時間 + 內存回收的時間)。
  • 垃圾收集開銷:吞吐量的補數,垃圾收集所用時間與總運行時間的比例。
  • 暫停時間:執行垃圾收集時,程序的工做線程被暫停的時間。
  • 收集頻率:相對於應用程序的執行,收集操做發生的頻率。
  • 內存佔用:Java 堆區所佔的內存大小。
  • 快速:一個對象從誕生到被回收所經歷的時間。

吞吐量、暫停時間、內存佔用 這三者共同構成一個「不可能三角」。三者整體的表現會隨着技術進步而愈來愈好。一款優秀的收集器一般最多同時知足其中的兩項。

這三項裏,暫停時間的重要性日益凸顯。由於隨着硬件發展,內存佔用多些愈來愈能容忍,硬件性能的提高也有助於下降收集器運行時對應用程序的影響,即提升了吞吐量。而內存的擴大,對延遲反而帶來負面效果。 簡單來講,主要抓住兩點:

  • 吞吐量
  • 暫停時間

性能指標:吞吐量

吞吐量就是 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 暫停時間

  • 高吞吐量較好由於這會讓應用程序的最終用戶感受只有應用程序線程在作「生產性」工做。直覺上,吞吐量越高程序運行越快。
  • 低暫停時間(低延遲)較好由於從最終用戶的角度來看無論是 GC 仍是其餘緣由致使一個應用被掛起始終是很差的。這取決於應用程序的類型,有時候甚至短暫的200毫秒暫停均可能打斷終端用戶體驗。所以,具備低的較大暫停時間是很是重要的,特別是對於一個交互式應用程序
  • 不幸的是」高吞吐量」和」低暫停時間」是一對相互競爭的目標(矛盾)。

    • 由於若是選擇以吞吐量優先,那麼必然須要下降內存回收的執行頻率,可是這樣會致使 GC 須要更長的暫停時間來執行內存回收。
    • 相反的,若是選擇以低延遲優先爲原則,那麼爲了下降每次執行內存回收時的暫停時間,也只能頻繁地執行內存回收,但這又引發了年輕代內存的縮減和致使程序吞吐量的降低。

在設計(或使用)GC 算法時,咱們必須肯定咱們的目標:一個 GC 算法只可能針對兩個目標之一(即只專一於較大吞吐量或最小暫停時間),或嘗試找到一個兩者的折衷。

如今標準:在最大吞吐量優先的狀況下,下降停頓時間

不一樣的垃圾回收器概述

7 種經典的垃圾收集器

  • 串行回收器:Serial、Serial Old
  • 並行回收器:ParNew、Parallel Scavenge、Parallel Old
  • 併發回收器:CMS、G1

7 種經典的垃圾收集器與垃圾分代之間的關係

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

垃圾收集器的組合關係

  • 兩個收集器間有連線,代表它們能夠搭配使用:Serial/Serial Old、Serial/CMS、ParNew/Serial Old、ParNew/CMS、Parallel Scavenge/Serial Old、Parallel Scavenge/Parallel Old、G1;
  • 其中 Serial Old 做爲 CMS 出現"Concurrent Mode Failure"失敗的後備預案。
  • (紅色虛線)因爲維護和兼容性測試的成本,在 JDK 8 時將 Serial+CMS、ParNew+Serial Old 這兩個組合聲明爲廢棄(JEP173),並在 JDK9 中徹底取消了這些組合的支持(JEP214),即:移除。
  • (綠色虛線)JDK14 中:棄用 Paralle1 Scavenge 和 Serialold GC 組合(JEP366)
  • (青色虛線)JDK14 中:刪除 CMS 垃圾回收器(JEP363)。

爲何要有不少收集器,一個不夠嗎?由於 Java 的使用場景不少,移動端,服務器等。因此就須要針對不一樣的場景,提供不一樣的垃圾收集器,提升垃圾收集的性能。

雖然咱們會對各個收集器進行比較,但並不是爲了挑選一個最好的收集器出來。沒有一種放之四海皆準、任何場景下都適用的完美收集器存在,更加沒有萬能的收集器。因此咱們選擇的只是對具體應用最合適的收集器。

如何查看默認垃圾收集器

-XX:+PrintcommandLineFlags:查看命令行相關參數(包含使用的垃圾收集器)

使用命令行指令:jinfo -flag 相關垃圾回收器參數 進程 ID

Serial 回收器:串行回收

  • Serial 收集器是最基本、歷史最悠久的垃圾收集器了。JDK1.3 以前回收新生代惟一的選擇。
  • Serial 收集器做爲 HotSpot 中 Client 模式下的默認新生代垃圾收集器。
  • Serial收集器採用複製算法、串行回收和「Stop-The-World」機制的方式執行內存回收
  • 除了年輕代以外,Serial 收集器還提供用於執行老年代垃圾收集的 Serial Old 收集器。Serial Old收集器也採用了串行回收和「Stop-The-World」機制,只不過內存回收算法使用的是標記-壓縮算法
  • Serial Old 是運行在 Client 模式下默認的老年代垃圾回收器。
  • Serial Old 在 server 模式下主要由兩個用途:

    • 與新生代的 Parallel Scavenge 配合使用。
    • 做爲老年代 CMS 收集器的後備垃圾收集方案。

這個收集器是一個單線程的收集器,但它的「單線程」的意義並不只僅說明它只會使用一個 CPU 或一條收集線程去完成垃圾收集工做,更重要的是在它進行垃圾收集時,必須暫停其餘全部的工做線程,直到它收集結束(Stop The World)

優點:簡單而高效(與其餘收集器的單線程比),對於限定單個 CPU 的環境來講,Serial 收集器因爲沒有線程交互的開銷,專心作垃圾收集天然能夠得到最高的單線程收集效率。

運行在 client 模式下的虛擬機是個不錯的選擇。

在用戶的桌面應用場景中,可用內存通常不大(幾十 MB 至一兩百 MB),能夠在較短期內完成垃圾收集(幾十 ms 至一百多 ms),只要不頻繁發生,使用串行回收器是能夠接受的。

在 HotSpot 虛擬機中,使用-XX:+UseSerialGC 參數能夠指定年輕代和老年代都使用串行收集器。

等價於新生代用 Serial GC,且老年代用 Serial old GC。

總結

這種垃圾收集器你們瞭解,如今已經不用串行的了。並且在限定單核 cpu 才能夠用。如今都不是單核的了。

對於交互較強的應用而言,這種垃圾收集器是不能接受的。通常在 Java web 應用程序中是不會採用串行垃圾收集器的。

ParNew 回收器:並行回收

若是說 serialGC 是年輕代中的單線程垃圾收集器,那麼 ParNew 收集器則是 serial 收集器的多線程版本。

Par 是 Parallel 的縮寫,New:只能處理的是新生代。

ParNew 收集器除了採用並行回收的方式執行內存回收外,兩款垃圾收集器之間幾乎沒有任何區別。ParNew 收集器在年輕代中一樣也是採用複製算法、"Stop-The-World"機制

ParNew 是不少 JVM 運行在 Server 模式下新生代的默認垃圾收集器。

  • 對於新生代,回收次數頻繁,使用並行方式高效。
  • 對於老年代,回收次數少,使用串行方式節省資源。(CPU 並行須要切換線程,串行能夠省去切換線程的資源)。

目前只有 ParNew GC 能與 CMS 收集器配合工做

在程序中,開發人員能夠經過選項"-XX:+UseParNewGC"手動指定使用 ParNew 收集器執行內存回收任務。它表示年輕代使用並行收集器,不影響老年代。

-XX:ParallelGCThreads 限制線程數量,默認開啓和 CPU 數據相同的線程數。

Parallel 回收器:吞吐量優先

HotSpot 的年輕代中除了擁有 ParNew 收集器是基於並行回收的之外,Parallel Scavenge 收集器一樣也採用了複製算法、並行回收和"Stop The World"機制

那麼 Parallel 收集器的出現是否畫蛇添足?

  • 和 ParNew 收集器不一樣,Parallel Scavenge 收集器的目標則是達到一個可控制的吞吐量(Throughput),它也被稱爲吞吐量優先的垃圾收集器。
  • 自適應調節策略也是 Parallel Scavenge 與 ParNew 一個重要區別。

高吞吐量則能夠高效率地利用 CPU 時間,儘快完成程序的運算任務,主要適合在後臺運算而不須要太多交互的任務。所以,常見在服務器環境中使用。例如,那些執行批量處理、訂單處理、工資支付、科學計算的應用程序。

Parallel 收集器在 JDK1.6 時提供了用於執行老年代垃圾收集的 Parallel Old 收集器,用來代替老年代的 Serial Old 收集器。

Parallel Old 收集器採用了標記-壓縮算法,但一樣也是基於並行回收和"Stop-The-World"機制

在程序吞吐量優先的應用場景中,Parallel 收集器和 Parallel Old 收集器的組合,在 server 模式下的內存回收性能很不錯。在 Java8 中,默認是此垃圾收集器。

參數配置

  • -XX:+UseParallelGC 手動指定年輕代使用 Parallel 並行收集器執行內存回收任務。
  • -XX:+UseParallelOldGC 手動指定老年代使用並行回收收集器。

    • 分別適用於新生代和老年代,默認 JDK8 是開啓的。
    • 上面兩個參數,默認開啓一個後,另外一個也會被開啓(互相激活)。
  • -XX:ParallelGCThreads 設置年輕代並行收集器的線程數。通常最好與 CPU 數量相等,以免過多的線程數影響垃圾收集性能。

    • 在默認狀況下,當 CPU 數量小於 8 個,ParallelGcThreads 的值等於 CPU 數量。
    • 當 CPU 數量大於 8 個,ParallelGCThreads 的值等於 3+[5*CPU Count]/8]。
  • -XX:MaxGCPauseMillis 設置垃圾收集器最大停頓時間(即 STW 的時間)。單位是毫秒。

    • 爲了儘量的把停頓時間控制在 MaxGCPauseMillis 之內,收集器在工做時會調整 Java 堆大小或者其它一些參數。
    • 對於用戶來講,停頓時間越短體驗越好。可是在服務端,咱們注重高併發,總體的吞吐量,因此服務器端適合 Parallel ,進行控制。
    • 須要謹慎使用該參數
  • -XX:GCTimeRatio 垃圾收集時間佔總時間的比例(= 1 / (N + 1))。

    • 用於衡量吞吐量的大小。取值範圍(0,100)。默認值 99,也就是垃圾回收時間不超過 1%。
    • 與前一個-xx:MaxGCPauseMillis 參數有必定矛盾性。暫停時間越長,Radio 參數就容易超過設定的比例。
  • -XX:+UseAda[tiveSizePolicy 設置 Parallel Scavenge 收集器具備自適應調節策略

    • 在這種模式下,年輕代的大小、Eden 和 Survivor 的比例、晉升老年代的對象年齡等參數會被自動調整,已達到在堆大小、吞吐量和停頓時間之間的平衡點。
    • 在手動調優比較困難的場合,能夠直接使用這種自適應的方式,僅指定虛擬機的最大堆、目標的吞吐量(GCTimeRatio)和停頓時間(MaxGCPauseMillis),讓虛擬機本身完成調優工做。

CMS 回收器:低延遲

在 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 有什麼不一樣呢?

請記住如下口令:

  • 若是你想要最小化地使用內存和並行開銷,請選 Serial GC。
  • 若是你想要最大化應用程序的吞吐量,請選 Parallel GC。
  • 若是你想要最小化 GC 的中斷或停頓時間,請選 CMS GC。

G1 回收器:區域分代化

既然咱們已經有了前面幾個強大的 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 在回收期間,能夠有多個 GC 線程同時工做,有效利用多核計算能力。此時用戶線程 STW。
  • 併發性:G1 擁有與應用程序交替執行的能力,部分工做能夠和應用程序同時執行,所以,通常來講,不會在整個回收階段發生徹底阻塞應用程序的狀況。

分代收集

  • 從分代上看,G1依然屬於分代型垃圾回收器,它會氣氛年輕代和老年代,年輕代依然有 Eden 區和 Survivor 區,但從堆結構上看,它不要求整個 Eden 區、年輕代、或者老年代都是連續的,也再也不堅持固定大小和固定數量。
  • 堆空間分爲若干個區域(Region),這些區域中包含了邏輯上的年輕代和老年代。
  • 和以前的各種回收器不一樣,它同時兼顧年輕代和老年代。對比其它收集器,或者工做在年輕代,或者工做在老年代。

G1 的分代,已經不是下面這樣的了

而是以下圖這樣的一個區域

空間整合

  • CMS:「標記-清除」 算法、內存碎片、若干次 GC 後進行一次碎片整理。
  • G1 將內存劃分爲一個個 region。內存的回收是以 region 做爲基本單位的。Region之間使用的是複製算法,但總體上實際可看做是標記-壓縮(Mark-Compact)算法,兩種算法均可以免內存碎片,這種特性有利於程序長時間運行,分配大對象時不會由於沒法找到連續內存空間而提早觸發 GC,尤爲是當 Java 堆很是大的時候,G1 的優點更加明顯。

可預測的停頓時間模型(即:軟實時 soft real-time)

這是 G1 相對於 CMS 的另外一大優點,G1 除了追求低停頓外,還能創建可預測的停頓時間模型,能讓使用者明確指定在一個長度爲 M 毫秒的時間片斷內,消耗在垃圾收集上的時間不得超過 N 毫秒。

  • 因爲分區的緣由,G1 能夠只選取部分區域進行內存回收,這樣縮小了回收的範圍,所以對於全局停頓狀況的發生也能獲得較好的控制。
  • G1 跟蹤各個 Region 裏面的垃圾堆積的價值大小(回收所得到的空間大小以及回收所需時間的經驗值),在後臺維護一個優先列表,每次根據容許的收集時間,優先回收價值最大的 Region。保證了 G1 收集器在有限的時間內能夠獲取儘量高的收集效率。
  • 相比於 CMS GC,G1 未必能作到 CMS 在最好狀況下的延時停頓,可是最差狀況要好不少。

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 收集器的適用場景

  • 面向服務端應用,針對具備大內存、多處理器的機器。(在普通大小的堆裏表現並不驚喜)
  • 最主要的應用是須要低 GC 延遲,並具備大堆的應用程序提供解決方案;
  • 如:在堆大小約 6GB 或更大時,可預測的暫停時間能夠低於 0.5 秒;(G1 經過每次只清理一部分而不是所有的 Region 的增量式清理來保證每次 GC 停頓時間不會過長)。
  • 用來替換掉 JDK1.5 中的 CMS 收集器;在下面的狀況時,使用 G1 可能比 CMS 好:

    • 超過 50% 的 Java 堆被活動數據佔用;
    • 對象分配頻率或年代提高頻率變化很大;
    • GC 停頓時間過長(長於 0.5 至 1 秒)
  • HotSpot 垃圾收集器裏,除了 G1 之外,其餘的垃圾收集器使用內置的 JVM 線程執行 GC 的多線程操做,而 G1 GC 能夠採用應用線程承擔後臺運行的 GC 工做,即當 JVM 的 GC 線程處理速度慢時,系統會調用應用程序線程幫助加速垃圾回收過程。

分區 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 的垃圾回收過程主要包括以下三個環節:

  • 年輕代 GC(Young GC)
  • 老年代併發標記過程(Concurrent Marking)
  • 混合回收(Mixed GC)
  • 若是須要,單線程、獨佔式、高強度的 Full GC 仍是繼續存在的。它針對 GC 的評估失敗提供了一種失敗保護機制,即強力回收。

應用程序分配內存,當年輕代的Eden區用盡時開始年輕代回收過程;G1 的年輕代收集階段是一個並行獨佔式收集器。在年輕代回收期,G1 GC 暫停全部應用程序線程,啓動多線程執行年輕代回收。而後從年輕代區間移動存活對象到Survivor區間或者老年區間,也有多是兩個區間都會涉及

當堆內存使用達到必定值(默認 45%)時,開始老年代併發標記過程。

標記完成立刻開始混合回收過程。對於一個混合回收期,G1 GC 從老年區間移動存活對象到空閒區間,這些空閒區間也就成爲了老年代的一部分。和年輕代不一樣,老年代的 G1 回收器和其餘 GC 不一樣,G1 的老年代回收器不須要整個老年代被回收,一次只須要掃描/回收一小部分老年代的 Region 就能夠了。同時,這個老年代 Region 是和年輕代一塊兒被回收的。

Remembered Set(記憶集)

一個對象被不一樣區域引用的問題

一個 Region 不多是孤立的,一個 Region 中的對象可能被其餘任意 Region 中對象引用,判斷對象存活時,是否須要掃描整個 Java 堆才能保證準確?

在其餘的分代收集器,也存在這樣的問題(而 G1 更突出)回收新生代也不得不一樣時掃描老年代?這樣的話會下降 MinorGC 的效率;

解決方法:

  • 不管 G1 仍是其餘分代收集器,JVM 都是使用 Remembered Set 來避免全局掃描:
  • 每一個 Region 都有一個對應的 Remembered Set;
  • 每次 Reference 類型數據寫操做時,都會產生一個 Write Barrier 暫時中斷操做;
  • 而後檢查將要寫入的引用指向的對象是否和該 Reference 類型數據在不一樣的 Region(其餘收集器:檢查老年代對象是否引用了新生代對象);
  • 若是不一樣,經過 CardTable 把相關引用信息記錄到引用指向對象的所在 Region 對應的 Remembered Set 中;
  • 當進行垃圾收集時,在 GC 根節點的枚舉範圍加入 Remembered Set;就能夠保證不進行全局掃描,也不會有遺漏。

G1 回收過程一:年輕代 GC

JVM 啓動時,G1 先準備好 Eden 區,程序在運行過程當中不斷建立對象到 Eden 區,當 Eden 空間耗盡時,G1 會啓動一次年輕代垃圾回收過程。

年輕代垃圾回收只會回收Eden區和Survivor區。

Young GC 時,首先 G1 中止應用程序的執行(STW),G1 建立回收集(Collection Set),回收集是指須要被回收的內存分段的集合,年輕代回收過程的回收集包含年輕代 Eden 區和 Survivor 區全部的內存分段。

而後開始以下回收過程:

  • 第一階段,掃描根

根是指 static 變量指向的對象,正在執行的方法調用鏈條上的局部變量等。根引用連同 RSet 記錄的外部引用做爲掃描存活對象的入口。

  • 第二階段,更新 RSet

處理 dirty card queue 中的 card,更新 RSet。此階段完成後,RSet能夠準確的反映老年代對所在的內存分段中對象的引用

對於應用程序的引用賦值語句 object.field=object,JVM 會在以前和以後執行特殊的操做以在 dirty card queue 中入隊一個保存了對象引用信息的 card。在年輕代回收的時候,G1 會對 dirty card queue 中全部的 card 進行處理,以更新 RSet,保證 RSet 實時準確的反映引用關係。那爲何不在引用賦值語句處直接更新 RSet 呢?這是爲了性能的須要,RSet 的處理須要線程同步,開銷會很大,使用隊列性能會好不少。
  • 第三階段,處理 RSet

識別被老年代對象指向的 Eden 中的對象,這些被指向的 Eden 中的對象被認爲是存活的對象。

  • 第四階段,複製對象

此階段,對象樹被遍歷,Eden 區內存段中存活的對象會被複制到 Survivor 區中空的內存分段,Survivor 區內存段中存活的對象若是年齡未達閾值,年齡會加 1,達到閥值會被會被複制到 o1d 區中空的內存分段。若是 Survivor 空間不夠,Eden 空間的部分數據會直接晉升到老年代空間。

  • 第五階段,處理引用

處理 Soft,Weak,Phantom,Final,JNI Weak 等引用。最終 Eden 空間的數據爲空,GC 中止工做,而目標內存中的對象都是連續存儲的,沒有碎片,因此複製過程能夠達到內存整理的效果,減小碎片。

G1 回收過程二:併發標記過程

  • 初始標記階段

標記從根節點直接可達的對象。這個階段是 STW 的,而且會觸發一次年輕代 GC。

  • 根區域掃描(Root Region Scanning)

G1 GC 掃描 survivor 區直接可達的老年代區域對象,並標記被引用的對象。這一過程必須在 Young GC 以前完成。

  • 併發標記(Concurrent Marking)

在整個堆中進行併發標記(和應用程序併發執行),此過程可能被 Young GC 中斷。在併發標記階段,若發現區域對象中的全部對象都是垃圾,那這個區域會被當即回收。同時,併發標記過程當中,會計算每一個區域的對象活性(區域中存活對象的比例)。

  • 再次標記(Remark)

因爲應用程序持續進行,須要修正上一次的標記結果。是 STW 的。G1 中採用了比 CMS 更快的初始快照算法:snapshot-at-the-beginning(SATB)。

  • 獨佔清理(cleanup,STW)

計算各個區域的存活對象和 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 的緣由可能有兩個:

  1. EVacuation 的時候沒有足夠的 to-space 來存放晉升的對象。
  2. 併發處理過程完成以前空間耗盡。

垃圾回收器總結

截止 JDK1.8,一共有 7 款不一樣的垃圾收集器。每一款的垃圾收集器都有不一樣的特色,在具體使用的時候,須要根據具體的狀況選用不一樣的垃圾收集器。

GC 發展階段:Seria l=> Parallel(並行)=> CMS(併發)=> G1 => ZGC

不一樣廠商、不一樣版本的虛擬機實現差距比較大。HotSpot 虛擬機在 JDK7/8 後全部收集器及組合以下圖

怎麼選擇垃圾回收器

Java 垃圾收集器的配置對於 JVM 優化來講是一個很重要的選擇,選擇合適的垃圾收集器可讓 JVM 的性能有一個很大的提高。怎麼選擇垃圾收集器?

  • 優先調整堆的大小讓 JVM 自適應完成。
  • 若是內存小於 100M,使用串行收集器。
  • 若是是單核、單機程序,而且沒有停頓時間的要求,串行收集器。
  • 若是是多 CPU、須要高吞吐量、容許停頓時間超過 1 秒,選擇並行或者 JVM 本身選擇。
  • 若是是多 CPU、追求低停頓時間,需快速響應(好比延遲不能超過 1 秒,如互聯網應用),使用併發收集器。
  • 官方推薦 G1,性能高。如今互聯網的項目,基本都是使用 G1。

最後須要明確一個觀點:

沒有最好的收集器,更沒有萬能的收集;調優永遠是針對特定場景、特定需求,不存在一勞永逸的收集器。

GC 日誌分析

經過閱讀 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

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
相關文章
相關標籤/搜索