我肝了萬字的Java垃圾回收,看完你還敢說不會?

今天來講說 Java 垃圾回收,高頻面試問題。
java

提綱附上,話很少說,直接乾貨web

一、什麼是垃圾回收?

垃圾回收(Garbage Collection,GC):就是釋放垃圾佔用的空間,防止內存泄露。對內存堆中已經死亡的或者長時間沒有使用的對象進行清除和回收。面試

二、垃圾在哪兒?

上圖能夠看到程序計數器、虛擬機棧、本地方法棧都是伴隨着線程而生死,這些區域不須要進行 GC。算法

而方法區/元空間在 1.8 以後就直接放到本地內存了,假設總內存 2G,JVM 被分配內存 100M, 理論上元空間能夠分配 2G-100M = 1.9G,空間仍是足夠的,因此這塊區域也不用管。數組

因此就只剩下了,java 對象實例和數組都是在上分配的,因此垃圾回收器重點照顧安全

三、怎麼發現它?

在發生 GC 的時候,Jvm 是怎麼判斷堆中的對象實例是否是垃圾呢?微信

這裏有兩種方式:多線程

一、引用計數法

就是給對象中添加一個引用計數器,每當有一個地方引用它時,計數器的值就加 1,每當有一個引用失效時,計數器的值就減 1。任什麼時候刻只要對象的計數器值爲 0,那麼就能夠被斷定爲垃圾對象。併發

這種方式,效率挺高,可是 Jvm 並無使用引用計數算法。那是由於在某種場合下存在問題jvm

好比下面的代碼,會出現循環引用的問題:

public class Test {
    Test test;
    public Test(String name) {}

    public static  void main(String[] args) {
        Test a = new Test("A");
        Test b = new Test("B");

        a.test = b;
        b.test = a;

        a = null;
        b = null;
    }
}

即便你把 a 和 b 的引用都置爲 null 了,計數器也不是 0,而是 1,由於它們指向的對象又互相指向了對方,因此沒法回收這兩個對象。

二、可達性分析法

這纔是 jvm 默認使用的尋找垃圾算法

它的原理是經過一些列稱爲「GC Roots」的對象做爲起始點,從這些節點開始向下搜索,搜素所走過的路叫作稱爲引用鏈「Reference Chain」,當一個對象到 GC Roots 沒有任何引用鏈時,就說這個對象是不可達的。

從上圖能夠看到,即便 Object5 和 Object6 之間相互引用,可是沒有 GC Roots 和它們關聯,因此能夠解決循環引用的問題

小知識點:

一、哪些能夠做爲 GC ROOTS 根呢?

  1. 虛擬機棧(棧幀中的本地變量表)中引用的對象
  2. 方法區中類靜態屬性引用的對象
  3. 方法區中常量引用的對象
  4. 本地方法棧中 JNI(即通常說的 Native 方法)引用的對象

二、不得不說的四種引用

  1. 強引用:就是在程序中廣泛存在的,相似「Object a=new Object」這類的引用。 只要強引用關係還存在,垃圾回收器就不會回收掉被引用的對象。
  2. 軟引用:用來描述一些還有用可是並不是必須的對象。 直到內存空間不夠時(拋出 OutOfMemoryError 以前),纔會被垃圾回收,經過 SoftReference 來實現。
  3. 弱引用:比軟引用還弱,也是用來描述非必須的對象的, 當垃圾回收器開始工做時,不管內存是否足夠用,弱引用的關聯的對象都會被回收 WeakReference。
  4. 虛引用:它是最弱的一種引用關係,它的惟一做用是用來做爲一種通知。 採用 PhantomRenference 實現

三、爲何定義這些引用?

我的理解,其實就是給對象加一種中間態,讓一個對象不僅有引用和非引用兩種狀況,還能夠描述一些「食之無味棄之惋惜」的對象。好比說:當內存空間足時,則能保存在內存中,若是內存空間在進行垃圾回收以後還不夠時,纔對這些對象進行回收

四、生存仍是死亡?

要真正宣告一個對象死亡,至少要經歷兩次標記過程和一次篩選。

一張圖帶你看明白:

五、垃圾收集算法

一、標記清除算法

分爲兩個階段「標記」和「清除」,標記出全部要回收的對象,而後統一進行清除。

缺點:

  1. 在對象變多的狀況下,標記和清除效率都不高
  2. 會產生空間碎片

二、複製算法

就是將堆分紅兩塊徹底相同的區域,對象只在其中一塊區域內分配,而後標記出那些是存活的對象,按順序總體移到另一個空間,而後回收掉以前那個區域的全部對象。

缺點:

  1. 雖然可以解決空間碎片的問題,可是空間少了一半。也太多了吧!!

三、標記整理算法

這種算法是,先找到存活的對象,而後將它們向空間的一端移動,最後回收掉邊界之外的垃圾對象。

四、分代收集

其實就是整合了上面三種算法,揚長避短。

之因此叫分代,是由於根據對象存活週期的不一樣將整個 Java 堆切割成爲三個部分:

  • Young(年輕代)
    • Eden(伊利園):新生對象
    • Survivor(倖存者):垃圾回收後還活着的對象
  • Tenured(老年代):對象屢次回收都沒有被清理,會移到老年代
  • Perm(永久代):存放加載的類別還有方法對象,java8 以後移除了永久代,替換爲元空間(Metaspace)

在新生代中,每次垃圾收集都有大量的對象死去,只有少許的存活,那就選用 複製算法 ,由於複製成本很小,只須要複製少許存活對象。

老年代中,存活對象較多,沒有額外的空間擔保,就得使用 標記清除 或者 標記整理

六、垃圾收集器

在說垃圾回收器以前須要瞭解幾個概念:

一、幾個概念

吞吐量

CPU 用於運行用戶代碼的時間與 CPU 總消耗時間的比值。

好比說虛擬機總運行了 100 分鐘,用戶代碼時間 99 分鐘,垃圾回收時間 1 分鐘,那麼吞吐量就是 99%。

STW

全稱 Stop-The-World,即在 GC 期間,只有垃圾回收器線程在工做,其餘工做線程則被掛起。

爲何須要 STW 呢?

在 java 程序中引用關係是不斷會變化的,那麼就會有不少種狀況來致使垃圾標識出錯。

想一想一下若是一個對象 A 當前是個垃圾,GC 把它標記爲垃圾,可是在清除前又有其餘引用指向了 A,那麼此刻又不是垃圾了。

那麼,若是沒有 STW 的話,就要去無限維護這種關係來去採集正確的信息,顯然是不可取的。

安全點

從線程角度看,安全點能夠理解成是在代碼執行過程當中的一些特殊位置,當線程執行到這些位置的時候,說明虛擬機當前的狀態是安全的。

好比:方法調用循環跳轉異常跳轉等這些地方纔會產生安全點。

若是有須要,能夠在這個位置暫停,好比發生 GC 時,須要暫停全部活動線程,可是線程在這個時刻,尚未執行到一個安全點,因此該線程應該繼續執行,到達下一個安全點的時候暫停,等待 GC 結束。

串行、並行

串行:是指垃圾回收線程在進行垃圾回收工做,此時用戶線程處於等待狀態。

並行:是指用戶線程和多條垃圾回收線程分別在不一樣 CPU 上同時工做。

二、回收器

下面是一張很經典的圖,展現了 7 種不一樣分代的收集器,若是兩個收集器之間存在連線,說明能夠搭配使用。

Serial

Serial 收集器是一個單線程收集器,在進行垃圾回收器的時候,必須暫停其餘工做線程,也就是發生 STW。在 GC 期間,應用是不可用的。

特色:一、採用複製算法  二、單線程收集器  三、效率會比較慢,可是由於是單線程,因此消耗內存小

ParNew

ParNew 是 Serial 的多線程版本,也是工做在新生代,能與 CMS 配合使用。

在多 CPU 的狀況下,因爲 ParNew 的多線程回收特性,毫無疑問垃圾收集會更快,也能有效地減小 STW 的時間,提高應用的響應速度。

特色:一、採用複製算法  二、多線程收集器  三、效率高,能大大減小 STW 時間。

Parallel Scavenge

Parallel Scavenge 收集器也是一個使用複製算法,多線程,工做於新生代的垃圾收集器,看起來功能和 ParNew 收集器基本同樣。

可是它有啥特別之處呢?關注點不一樣

  • ParNew 垃圾收集器關注的是儘量縮短垃圾收集時用戶線程的停頓時間,更適合用到與用戶交互的程序,由於停頓時間越短,用戶體驗確定就好呀!!
  • Parallel Scavenge 目標是達到一個可控制的吞吐量,因此更適合作後臺運算等不須要太多用戶交互的任務。

Parallel Scavenge 收集器提供了兩個參數來控制吞吐量,

  • -XX:MaxGCPauseMillis:控制最大垃圾收集時間
  • -XX:GCTimeRati:直接設置吞吐量大小

特色:一、採用複製算法 二、多線程收集器 三、吞吐量優先

Serial Old

Serial 收集器是工做於新生代的單線程收集器,與之相對地,Serial Old 是工做於老年代的單線程收集器。

做用:

  • 在 Client 模式下與 Serial 回收器配合使用
  • Server 模式下,則它還有兩大用途:一種是在 JDK 1.5 及以前的版本中與 Parallel Scavenge 配合使用,另外一種是做爲 CMS 收集器的後備預案,在併發收集發生 Concurrent Mode Failure 時使用

它與 Serial 收集器配合使用示意圖以下:

特色:一、標記-整理算法 二、單線程 三、老年代工做

Parallel Old

Parallel Old 是一個多線程的垃圾回收器,採用標記整理算法,負責老年代的垃圾回收工做,能夠與 Parallel Scavenge 垃圾回收器一塊兒搭配工做。真正的實現吞吐量優先

示意圖以下:

特色:一、標記-整理算法 二、多線程 三、老年代工做

CMS

CMS 能夠說是一款具備"跨時代"意義的垃圾回收器,若是應用很重視服務的響應速度,但願給用戶最好的體驗,則 CMS 收集器是很是合適的,它是以獲取最短回收停頓時間爲目標的收集器!

CMS 雖然工做在老年代,和以前收集器不一樣的是,使用的標記清除算法

示意圖以下:

垃圾回收的 4 個步驟:

  1. 初始標記:標記出來和 GC Roots 直接關聯的對象,整個速度是很是快的,會發生 STW,確保標記的準確性。
  2. 併發標記:併發標記這個階段會直接根據第一步關聯的對象找到全部的引用關係,耗時較長,可是這個階段會與用戶線程併發運行,不會有很大的影響。
  3. 從新標記:這個階段是爲了解決第二步併發標記所致使的標錯狀況。併發階段會和用戶線程並行,有可能會出現判斷錯誤的狀況,這個階段就是對上一個階段的修正。
  4. 併發清除:最後一個階段,將以前確認爲垃圾的對象進行回收,會和用戶線程一塊兒併發執行。

缺點:

  1. 影響用戶線程的執行效率:CMS 默認啓動的回收線程數是(處理器核心數 + 3)/ 4 ,因爲是和用戶線程一塊兒併發清理,那麼勢必會影響到用戶線程的執行速度
  2. 會產生浮動垃圾:CMS 的第 4 個階段 併發清除是和用戶線程一塊兒的,會產生新的垃圾,就叫浮動垃圾
  3. 會產生碎片化的空間:標記清除的缺點

G1

全稱:Garbage-First

G1 回收的目標再也不是整個新生代或者是老年代。G1 能夠回收堆內存的任何空間來進行,再也不是根據年代來區分,而是那塊空間垃圾多就去回收,經過 Mixed GC 的方式去進行回收。

先看下堆空間的劃分:

G1 垃圾回收器把堆劃分紅大小相同的 Region,每一個 Region 都會扮演一個角色,分別爲 H、S、E、O。

  1. E 表明伊甸區
  2. S 表明 Survivor 區
  3. H 表明的是 Humongous 區
  4. O 表明 Old 區

G1 的工做流程圖:

  • 初始標記:標記出來 GC Roots 能直接關聯到的對象,修改 TAMS 的值以便於併發回收時新對象分配
  • 併發標記:根據剛剛關聯的對像掃描整個對象引用圖,和用戶線程併發執行,記錄 SATB(原始快照) 在併發時有引用的值
  • 最終標記:處理第二步遺留下來的少許 SATB(原始快照) 記錄,會發生 STW
  • 篩選回收:維護以前提到的優先級列表,根據優先級列表、用戶設置的最大暫停時間來回收 Region

特色:

  1. 並行與併發:G1 能充分利用多 CPU、多核環境下的硬件優點,能夠經過併發的方式讓 Java 程序繼續執行,進一步縮短 STW 的時間。
  2. 分代收集:分代概念在 G1 中依然得以保留,它可以採用不一樣的方式去處理新建立的對象和已經存活了一段時間、熬過屢次 GC 的舊對象來得到更好的收集效果。
  3. 空間整合:G1 從總體上看是基於標記-整理算法實現的,從局部(兩個 Region 之間)上看是基於複製算法實現的,G1 運行期間不會產生內存空間碎片。
  4. 可預測停頓:G1 比 CMS 厲害在能創建可預測的停頓時間模型,能讓使用者明確指定在一個長度爲 M 毫秒的時間片斷內,消耗在垃圾收集上的時間不得超過 N 毫秒。

七、內存分配與回收策略

上文說的一直都是回收內存的內容,那麼怎麼給對象分配內存呢?

堆空間的結構:

Eden 區

研究代表,有將近 98%的對象是朝生夕死,因此針對這一現狀,大多數狀況下,對象會在新生代 Eden 區中進行分配。

當 Eden 區沒有足夠空間進行分配時,虛擬機會發起一次 Minor GC,Minor GC 相比 Major GC 更頻繁,回收速度也更快。

經過 Minor GC 以後,Eden 會被清空,Eden 區中絕大部分對象會被回收,而那些無需回收的存活對象,將會進到 Survivor 的 From 區(若 From 區不夠,則直接進入 Old 區)。

Survivor 區

Survivor 區至關因而 Eden 區和 Old 區的一個緩衝,Survivor 又分爲 2 個區,一個是 From 區,一個是 To 區。每次執行 Minor GC,會將 Eden 區和 From 存活的對象放到 Survivor 的 To 區(若是 To 區不夠,則直接進入 Old 區)。

問題 1:爲何須要 Survivor?

若是沒有 Survivor 區,Eden 區每進行一次 Minor GC,存活的對象就會被送到老年代,老年代很快就會被填滿。而有不少對象雖然一次 Minor GC 沒有消滅,但其實或許第二次,第三次就須要被清除。

這時候移入老年區,很明顯不是一個明智的決定。

因此,Survivor 的存在乎義就是減小被送到老年代的對象,進而減小老年代 GC 的發生。Survivor 的預篩選保證,只有經歷 15 次 Minor GC 還能在新生代中存活的對象,纔會被送到老年代。

問題 2:爲何須要 From 和 To 兩個呢?

這種機制最大的好處就是能夠解決內存碎片化,整個過程當中,永遠有一個 Survivor 區是空的,另外一個非空的 Survivor 區是無碎片的。

假設只有一個 Survivor 區。

Minor GC 執行後,Eden 區被清空了,存活的對象放到了 Survivor 區,而以前 Survivor 區中的對象,可能也有一些是須要被清除的。

那麼問題來了,這時候咱們怎麼清除它們?

在這種場景下,咱們只能標記清除,而咱們知道標記清除最大的問題就是內存碎片,在新生代這種常常會消亡的區域,採用標記清除必然會讓內存產生嚴重的碎片化。

由於 Survivor 有 2 個區域,因此每次 Minor GC,會將以前 Eden 區和 From 區中的存活對象複製到 To 區域。第二次 Minor GC 時,To 區 到 From 區 ,以此反覆。

Old 區

老年代佔據着 2/3 的堆內存空間,只有在 Major GC 的時候纔會進行清理,每次 GC 都會觸發「Stop-The-World」。內存越大,STW 的時間也越長,因此內存也不只僅是越大就越好。

因爲複製算法在對象存活率較高的老年代會進行不少次的複製操做,效率很低,因此在這裏老年代採用的是標記整理算法。

下面三種狀況也會直接進入老年代:

大對象

大對象指須要大量連續內存空間的對象,這部分對象無論是否是「朝生夕死」,都會直接進到老年代。這樣作主要是爲了不在 Eden 區及 2 個 Survivor 區之間發生大量的內存複製。當你的系統有很是多「朝生夕死」的大對象時,須要注意。

長期存活對象

虛擬機給每一個對象定義了一個對象年齡 Age 計數器。正常狀況下對象會不斷的在 Survivor 的 From 區與 To 區之間移動,對象在 Survivor 區中每經歷一次 Minor GC,年齡就增長 1 歲。當年齡增長到 15 歲時,這時候就會被轉移到老年代。

動態對象年齡

虛擬機並不重視要求對象年齡必須到 15 歲,纔會放入老年區,若是 Survivor 空間中相同年齡全部對象大小的總合大於 Survivor 空間的一半,年齡大於等於該年齡的對象就能夠直接進去老年區。

空間分配擔保

在發生 Minor GC 以前,虛擬機會先檢查老年代最大可用的連續空間是否大於新生代全部對象總空間。

若是條件成立的話,Minor GC 是能夠確保安全的。

若是不成立,則虛擬機會查看 HandlePromotionFailure 設置是否擔保失敗,若是容許,那麼會繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代對象的平均大小。

若是大於,嘗試進行一次 Minor GC。

若是小於或者 HandlePromotionFailure 不容許,則進行一次 Full GC。


End

看累了吧,學到了吧,那就關注一下唄!

學到就是賺到,歡迎在看、點贊、轉發,您的承認是我原創的動力!

本文分享自微信公衆號 - 程序IT圈(DeveloperIT)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索