JVM 垃圾回收?全面詳細安排!

寫在前面:java

小夥伴兒們,你們好!今天來學習Java虛擬機相關內容,做爲面試必問的知識點,來深刻了解一波!程序員

思惟導圖:面試

image-20201207153125210image-20201207153125210算法

1,判斷對象是否死亡

咱們在進行垃圾回收(Garbage Collection,簡稱GC)以前確定要先判斷哪些是垃圾。緩存

在堆中幾乎放着全部的對象實例,對堆垃圾回收前的第一步就是要判斷那些對象已經死亡(即不能再被任何途徑使用的對象)。安全

image-20201207101110001image-20201207101110001服務器

1.1,引用計數算法

給對象添加一個引用計數器,每當有一個地方引用該對象時,計數器+1,當引用失效時,計數器-1,任什麼時候候當計數器爲0的時候,該對象再也不被引用。微信

引用計數器這個方法實現簡單,斷定效率也高。可是,當前主流的虛擬機都沒有采用這個算法來管理內存,其中最主要的緣由是它很難解決對象之間互相循環引用的問題。多線程

所謂對象之間互相循環引用,以下面代碼所示:除了對象 objA 和 objB 相互引用着對方以外,這兩個對象之間再無任何引用。可是它們由於互相引用對方,致使它們的引用計數器都不爲 0,因而引用計數算法沒法通知 GC 回收器回收他們。閉包

public class ReferenceCountingGc {
    public Object instance = null;
    public static final int _1MB = 1024*1024;
    public static void main(String[] args) {
        ReferenceCountingGc objA = new ReferenceCountingGc();
        ReferenceCountingGc objB = new ReferenceCountingGc();
        objA.instance = objB;
        objB.instance = objA;
        objA = null;
        objB = null;
    }
}

1.2,可達性分析算法

這個算法的基本思想就是經過一系列的稱爲 「GC Roots」 的對象做爲起點,從這些節點開始向下搜索,節點所走過的路徑稱爲引用鏈,當一個對象到 GC Roots 沒有任何引用鏈相連的話,則證實此對象是不可用的。

image-20201206094626579image-20201206094626579

在Java語言中,可做爲GC Roots的對象包括下面幾種:

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

2,再談引用

不管是經過引用計數法判斷對象引用數量,仍是經過可達性分析法判斷對象的引用鏈是否可達,斷定對象的存活都與「引用」有關。

JDK1.2 以前,Java 中引用的定義很傳統:若是 reference 類型的數據存儲的數值表明的是另外一塊內存的起始地址,就稱這塊內存表明一個引用。 JDK1.2 之後,Java 對引用的概念進行了擴充,將引用分爲強引用、軟引用、弱引用、虛引用四種(引用強度逐漸減弱)。

2.1,強引用

之前咱們使用的大部分引用實際上都是強引用,這是使用最廣泛的引用。相似於「Object obj=new Object()」這類的引用,若是一個對象具備強引用,那就相似於必不可少的生活用品,垃圾回收器毫不會回收它。當內存空間不足,Java虛擬機寧願拋出OutOfMemoryError錯誤,使程序異常終止,也不會靠隨意回收具備強引用的對象來解決內存不足問題。

2.2,軟引用

若是一個對象只具備軟引用,那就相似於無關緊要的生活用品。若是內存空間足夠,垃圾回收器就不會回收它,若是內存空間不足了,就會回收這些對象的內存。只要垃圾回收器沒有回收它,該對象就能夠被程序使用。軟引用可用來實現內存敏感的高速緩存。

軟引用能夠和一個引用隊列(ReferenceQueue)聯合使用,若是軟引用所引用的對象被垃圾回收,JAVA 虛擬機就會把這個軟引用加入到與之關聯的引用隊列中。

2.3,弱引用

若是一個對象只具備弱引用,那就相似於無關緊要的生活用品。弱引用與軟引用的區別在於:只具備弱引用的對象擁有更短暫的生命週期。在垃圾回收器線程掃描它所管轄的內存區域的過程當中,一旦發現了只具備弱引用的對象,無論當前內存空間足夠與否,都會回收它的內存。不過,因爲垃圾回收器是一個優先級很低的線程, 所以不必定會很快發現那些只具備弱引用的對象。

弱引用能夠和一個引用隊列(ReferenceQueue)聯合使用,若是弱引用所引用的對象被垃圾回收,Java 虛擬機就會把這個弱引用加入到與之關聯的引用隊列中。

2.4,虛引用

"虛引用"顧名思義,就是形同虛設,與其餘幾種引用都不一樣,虛引用並不會決定對象的生命週期。若是一個對象僅持有虛引用,那麼它就和沒有任何引用同樣,在任什麼時候候均可能被垃圾回收。

虛引用主要用來跟蹤對象被垃圾回收的活動。虛引用與軟引用和弱引用的一個區別在於:虛引用必須和引用隊列(ReferenceQueue)聯合使用。當垃圾回收器準備回收一個對象時,若是發現它還有虛引用,就會在回收對象的內存以前,把這個虛引用加入到與之關聯的引用隊列中。程序能夠經過判斷引用隊列中是否已經加入了虛引用,來了解被引用的對象是否將要被垃圾回收。程序若是發現某個虛引用已經被加入到引用隊列,那麼就能夠在所引用的對象的內存被回收以前採起必要的行動。

特別注意,在世紀程序設計中通常不多使用弱引用與虛引用,使用軟用的狀況較多,這是由於軟引用能夠加速JVM對垃圾內存的回收速度,能夠維護系統的運行安全,防止內存溢出(OutOfMemory)等問題的產生。

3,廢棄常量以及無用類

3.1,如何判斷一個常量是廢棄常量?

運行時常量池主要回收的是廢棄的常量。那麼,咱們如何判斷一個常量是廢棄常量呢?

假如在常量池中存在字符串"abc" ,若是當前沒有任何String對象引用該字符串常量的話,就說明常量"abc"就是廢棄常量,若是這時發生內存回收的話並且有必要的話," abc"就會被系統清理出常量池。

3.2,如何判斷一個類是無用的類?

斷定一個常量是不是「廢棄常量」比較簡單,而要斷定一個類是不是「無用的類」的條件則相對苛刻許多。方法區主要回收無用的類,類須要同時知足下面3個條件才能算是 「無用的類」 :

  • 該類全部的實例都已經被回收,也就是 Java 堆中不存在該類的任何實例。
  • 加載該類的 ClassLoader 已經被回收。
  • 該類對應的 java.lang.Class 對象沒有在任何地方被引用,沒法在任何地方經過反射訪問該類的方法。

虛擬機能夠對知足上述3個條件的無用類進行回收,這裏說的僅僅是「能夠」,而並非和對象同樣不使用了就會必然被回收。

4,垃圾收集算法

4.1,標記--清除算法

該算法分爲「標記」和「清除」階段:首先標記出全部不須要回收的對象,在標記完成後統一回收掉全部沒有被標記的對象。它是最基礎的收集算法,後續的算法都是對其不足進行改進獲得。這種垃圾收集算法會帶來兩個明顯的問題:

  • 效率問題:標記和清除兩個過程的效率都不高;
  • 空間問題:標記清除以後會產生大量不連續的內存碎片,空間碎片太多可能會致使之後在程序運行過程當中須要分配較大對象時,沒法找到足夠的連續內存而不得不提早觸發另外一次垃圾收集動做。

標記-清除算法標記-清除算法

4.2,複製算法

爲了解決效率問題,「複製」收集算法出現了。它將可用內存分爲大小相同的兩塊,每次使用其中的一塊。當這一塊的內存使用完後,就將還存活的對象複製到另外一塊去,而後再把使用的空間一次清理掉。這樣就使每次的內存回收都是對內存區間的一半進行回收。實現簡單,運行高效。

複製算法複製算法

4.3,標記--整理算法

根據老年代的特色提出的一種標記算法,標記過程仍然與「標記-清除」算法同樣,但後續步驟不是直接對可回收對象回收,而是讓全部存活的對象向一端移動,而後直接清理掉端邊界之外的內存。

標記-整理算法標記-整理算法

4.4,分代收集算法

當前虛擬機的垃圾收集都採用分代收集算法,這種算法沒有什麼新的思想,只是根據對象存活週期的不一樣將內存分爲幾塊。通常將 java 堆分爲新生代和老年代,這樣咱們就能夠根據各個年代的特色選擇合適的垃圾收集算法。

好比在新生代中,每次收集都會有大量對象死去,因此能夠選擇複製算法,只須要付出少許對象的複製成本就能夠完成每次垃圾收集。而老年代的對象存活概率是比較高的,並且沒有額外的空間對它進行分配擔保,因此咱們必須選擇「標記-清除」或「標記-整理」算法進行垃圾收集。

5,垃圾收集器

若是說收集算法是內存回收的方法論,那麼垃圾收集器就是內存回收的具體實現。

雖然咱們對各個收集器進行比較,但並不是要挑選出一個最好的收集器。由於直到如今爲止尚未最好的垃圾收集器出現,更加沒有萬能的垃圾收集器,咱們能作的就是根據具體應用場景選擇適合本身的垃圾收集器。試想一下:若是有一種四海以內、任何場景下都適用的完美收集器存在,那麼咱們的 HotSpot 虛擬機就不會實現那麼多不一樣的垃圾收集器了。

常見的垃圾收集器常見的垃圾收集器

5.1,Serial收集器

Serial收集器是最基本、歷史最悠久的垃圾收集器了。從名字上看是串行的意思,這個收集器是一個單線程的新生代收集器。它的 「單線程」 的意義不只僅意味着它只會使用一條垃圾收集線程去完成垃圾收集工做,更重要的是它在進行垃圾收集工做的時候必須暫停其餘全部的工做線程( "Stop The World" ),直到它收集結束。

Serial收集器Serial收集器

Serial 採起 「複製算法」 實現,若是是在單 CPU 環境下,Serial 收集器沒有線程交互的開銷,理論上是能夠得到最高的單線程執行效率,STW 的時間也能夠控制在幾十到幾百毫秒內,這個時間是徹底能夠接受的。

與其餘單線程收集器相比它的優勢就是:它簡單而高效(與其餘收集器的單線程相比)。 簡單而高效 Serial 收集器因爲沒有線程交互的開銷,天然能夠得到很高的單線程收集效率。 Serial 收集器對於運行在 Client 模式下的虛擬機來講是個不錯的選擇。

5.2,ParNew收集器

ParNew 收集器其實就是 Serial 收集器的多線程版本,除了使用多線程進行垃圾收集外,其他行爲(控制參數、收集算法、回收策略等等)和 Serial 收集器徹底同樣。ParNew 收集器雖然有多線程優點,但在單 CPU 和多 CPU 環境下,效果並不必定會比 Serial 好,至少在單 CPU 環境下是確定不如的 Serial 的。因爲線程交互開銷的時間,效果並不如人意,多線程的好處在於更高效率地利用 CPU ,提升 CPU 的吞吐量,讓 CPU 空閒的時間減小。

新生代採用複製算法,老年代採用標記-整理算法。

ParNew收集器ParNew收集器

它是許多運行在 Server 模式下的虛擬機的首要選擇,除了 Serial 收集器外,只有它能與 CMS 收集器(真正意義上的併發收集器,後面會介紹到)配合工做。

  • 並行(Parallel) :指多條垃圾收集線程並行工做,但此時用戶線程仍然處於等待狀態。
  • 併發(Concurrent):指用戶線程與垃圾收集線程同時執行(但不必定是並行,可能會交替執行),用戶程序在繼續運行,而垃圾收集器運行在另外一個 CPU 上。

5.3,Parallel Scavenge收集器

Parallel Scavenge 收集器也是使用複製算法的多線程收集器,它看上去幾乎和ParNew都同樣。 那麼它有什麼特別之處呢?

Parallel Scavenge 收集器關注點是吞吐量(高效率的利用 CPU)。CMS 等垃圾收集器的關注點更多的是用戶線程的停頓時間(提升用戶體驗)。所謂吞吐量就是 CPU 中用於運行用戶代碼的時間與 CPU 總消耗時間的比值。 Parallel Scavenge 收集器提供了不少參數供用戶找到最合適的停頓時間或最大吞吐量,若是對於收集器運做不太瞭解,手工優化存在困難的時候,使用Parallel Scavenge收集器配合自適應調節策略,把內存管理優化交給虛擬機去完成也是一個不錯的選擇。

新生代採用複製算法,老年代採用標記-整理算法。

Parallel Scavenge收集器Parallel Scavenge收集器

這是 JDK1.8 默認收集器

使用java -XX:+PrintCommandLineFlags -version命令查看

-XX:InitialHeapSize=197918400 -XX:MaxHeapSize=3166694400 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
java version "1.8.0_144"
Java(TM) SE Runtime Environment (build 1.8.0_144-b01)
Java HotSpot(TM) 64-Bit Server VM (build 25.144-b01, mixed mode)

查看結果查看結果

JDK1.8 默認使用的是 Parallel Scavenge + Parallel Old,若是指定了-XX:+UseParallelGC 參數,則默認指定了-XX:+UseParallelOld GC,可使用-XX:-UseParallelOldGC 來禁用該功能。

5.4,Serial Old收集器

Serial 收集器的老年代版本,它一樣是一個單線程收集器。它主要有兩大用途:一種用途是在 JDK1.5 以及之前的版本中與 Parallel Scavenge 收集器搭配使用,另外一種用途是做爲 CMS 收集器的後備方案。

5.5,Parallel Old 收集器

Parallel Scavenge 收集器的老年代版本。使用多線程和「標記-整理」算法。在注重吞吐量以及 CPU 資源的場合,均可以優先考慮 Parallel Scavenge 收集器和 Parallel Old 收集器。

5.6,CMS 收集器

CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間爲目標的收集器。它很是符合在注重用戶體驗的應用上使用。

CMS(Concurrent Mark Sweep)收集器是 HotSpot 虛擬機第一款真正意義上的併發收集器,它第一次實現了讓垃圾收集線程與用戶線程(基本上)同時工做。

從名字中的Mark Sweep這兩個詞能夠看出,CMS 收集器是一種 「標記-清除」算法實現的,它的運做過程相比於前面幾種垃圾收集器來講更加複雜一些。整個過程分爲四個步驟:

  • 初始標記: 暫停全部的其餘線程,並記錄下直接與 root 相連的對象,速度很快 ;
  • 併發標記: 同時開啓 GC 和用戶線程,用一個閉包結構去記錄可達對象。但在這個階段結束,這個閉包結構並不能保證包含當前全部的可達對象。由於用戶線程可能會不斷的更新引用域,因此 GC 線程沒法保證可達性分析的實時性。因此這個算法裏會跟蹤記錄這些發生引用更新的地方。
  • 從新標記: 從新標記階段就是爲了修正併發標記期間由於用戶程序繼續運行而致使標記產生變更的那一部分對象的標記記錄,這個階段的停頓時間通常會比初始標記階段的時間稍長,遠遠比並發標記階段時間短
  • 併發清除: 開啓用戶線程,同時 GC 線程開始對未標記的區域作清掃。

CMS收集器CMS收集器

兩次STW,從它的名字就能夠看出它是一款優秀的垃圾收集器,主要優勢:併發收集、低停頓。可是它有下面三個明顯的缺點:

  • 對 CPU 資源敏感;
  • 沒法處理浮動垃圾;
  • 它使用的回收算法-「標記-清除」算法會致使收集結束時會有大量空間碎片產生。

什麼是浮動垃圾? CMS在併發清理階段,用戶線程還在運行, 伴隨着程序的運行天然也會產生新的垃圾,這一部分垃圾產生在標記過程以後,CMS沒法再當次過程當中處理,因此只有等到下次gc時候在清理掉,這一部分垃圾就稱做「浮動垃圾」。

5.7,G1 收集器

G1 (Garbage-First) 是一款面向服務器的垃圾收集器,主要針對配備多顆處理器及大容量內存的機器. 以極高機率知足 GC 停頓時間要求的同時,還具有高吞吐量性能特徵。

被視爲 JDK1.7 中 HotSpot 虛擬機的一個重要進化特徵。它具有一下特色:

  • 並行與併發:G1 能充分利用 CPU、多核環境下的硬件優點,使用多個 CPU(CPU 或者 CPU 核心)來縮短 Stop-The-World 停頓時間。部分其餘收集器本來須要停頓 Java 線程執行的 GC 動做,G1 收集器仍然能夠經過併發的方式讓 java 程序繼續執行。
  • 分代收集:雖然 G1 能夠不須要其餘收集器配合就能獨立管理整個 GC 堆,可是仍是保留了分代的概念。
  • 空間整合:與 CMS 的「標記--清理」算法不一樣,G1 從總體來看是基於「標記整理」算法實現的收集器;從局部上來看是基於「複製」算法實現的。
  • 可預測的停頓:這是 G1 相對於 CMS 的另外一個大優點,下降停頓時間是 G1 和 CMS 共同的關注點,但 G1 除了追求低停頓外,還能創建可預測的停頓時間模型,能讓使用者明確指定在一個長度爲 M 毫秒的時間片斷內。

G1收集器運行示意圖G1收集器運行示意圖

G1 收集器在後臺維護了一個優先列表,每次根據容許的收集時間,優先選擇回收價值最大的 Region(這也就是它的名字 Garbage-First 的由來)。這種使用 Region 劃份內存空間以及有優先級的區域回收方式,保證了 G1 收集器在有限時間內能夠儘量高的收集效率(把內存化整爲零)。


微信搜索公衆號《程序員的時光》 好了,今天就先分享到這裏了,下期繼續給你們帶來JVM面試內容! 更多幹貨、優質文章,歡迎關注個人原創技術公衆號~

參考文獻:

深刻理解Java虛擬機(第2版).周志明

相關文章
相關標籤/搜索