1. 程式人生 > >深入淺出 JVM GC(2)

深入淺出 JVM GC(2)

沒有 內存空間 清空 名詞 出了 all 留下 基本 alt

技術分享圖片

# 前言

在 深入淺出 JVM GC(1) 中,限於上篇文章的篇幅,我們留下了一個問題 : 如何回收? 這篇文章將重點講述這個問題。

在上篇文章中,我們也列出了一些大綱,今天我們就按照那個大綱來逐個講解。在此,我將大綱復制過來。

垃圾回收算法

  1. 標記清除算法
  2. 復制算法
  3. 標記整理算法
  4. 分代收集算法(堆如何分代)

有哪些垃圾收集器

  1. Serial 串行收集器(只適用於堆內存256m 以下的 JVM )
  2. ParNew 並行收集器(Serial 收集器的多線程版本)
  3. Parallel Scavenge (PS 收集器,該收集器以吞吐量為主要目的,是1.8的默認 GC)
  4. CMS 收集器(該收集器全稱 Concurrent Mark Sweep,是一種關註最短停頓時間的垃圾收集器)
  5. G1 收集器(JDK 9 的默認 GC)

有哪些GC

  1. Young GC(又稱 YGC,minor GC,年輕代 GC)
  2. Old GC (老年代 GC,只有 CMS 才會單獨回收 Old 區)
  3. Full GC(又稱 major GC)
  4. Mixed GC(混合 GC,G1 收集器獨有)

1. 有哪些垃圾回收算法

  1. 標記清除算法
  2. 復制算法
  3. 標記整理算法
  4. 分代收集算法(堆如何分代)
1. 標記清除算法

GC 中最基礎的算法就是標記-清除算法,所謂標記清除,就是通過可達性分析,標記哪些是垃圾對象,然後清除。之所以說是最簡單的算法,是因為後面的幾種算法都是基於它的。

但是這個算法有2個不足之處:1. 碎片問題,標記清除之後會導致大量內存不連續的碎片,空間碎片太多會導致分配大對象時無法找到足夠的連續內存從而提前觸發 Full GC (此 GC 嚴重影響應用性能)。2. 效率問題,標記和清除這兩個過程的效率都不高。我們通過一幅圖來看看標記清除的算法:

技術分享圖片

可以從上圖看出,回收後,出現了大量的內存不連續的內存塊。

2. 復制算法

為了解決效率問題,人們發明了一種復制算法(Coping)。它將可用內存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的用完了,就開始垃圾回收,將有用的對象復制到另一個空閑的內存上,清空之前使用的內存塊。這樣使得每次都對整個半區回收,而且也不用考慮內存碎片問題,只需要移動堆頂指針,按順序分配即可,實現簡單,運行高效。

但是凡事都是有缺點的,復制算法的缺點就是內存縮小到了原來的一半,無法充分利用內存空間。

總是有取舍的。

現代的所有商業虛擬機都是采用這種算法來回收新生代。基於統計學,人們得出99% 的對象都是朝生夕死的,所以不需要留出那麽大的空間保存存活的對象,也就是不要1:1 的比例來劃分內存。

通常的做法是:

將內存分為一個較大的Eden(伊甸園)空間和兩塊較小的 Survivor(幸存區)空間,每次使用 Eden 和其中一塊 Survivor ,當回收時,將 Eden 和 Survivor 還存活著的對象一次性的復制到另外一塊 Survivor 空間,最後清理掉 Eden 和剛剛使用的 Survivor 空間。Hotspot 默認的比例是 8:1:1,也就是說,每次新生代可用內存空間為新生代總空間的90%,只有10%的內存會被浪費,從一定程度上解決了復制算法浪費空間的問題。

當然,98% 的對象可回收只是一般的情況下,我們無法保證每次回收都只有不多於 10% 的對象存活,當 Survivor 空間不夠用時怎麽辦呢?肯定需要依賴其他內存(老年代)進行所謂的分配擔保(Handle Promotion)。

什麽是分配擔保呢?

如果另外一塊 Survivor 區域沒有足夠空間存放上一次新生代手機下來的存活對象時,這些對象將直接通過分配擔保機制進入老年代。當然,具體細節這句話無法詳細說明,我們將會在之後闡述具體細節。

3. 標記整理算法

從上面我們可以看出,復制算法的效率很高,請註意,該算法只有在對象存活率較低的時候(98% 對象可被回收)才能體現出效率。而如果一次 GC 活動之後,存活對象很多,那麽就需要復制大量的對象,很明顯,會導致效率不高;更關鍵的是,還需要額外的空間進行分配擔保

所以,存活對象時間很長的老年代一般不使用該算法。

根據老年代的特點,一般使用“標記-整理(Mark-Compact)”算法,標記過程仍然與 “標記清除” 算法一樣,但我們知道,標記清除算法會產生大量的內存碎片,對性能影響很大,所以標記整理算法後續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象像一個方向移動,然後清理掉邊界之外的內存。也就是將那些原來散落的對象移動在一起,讓碎片不再存在。

可以說,標記整理算法相對於標記清除算法犧牲了一些性能,但卻避免了內存碎片的產生,在大部分場合,可抵消掉整理過程中產生的性能損耗。

4. 分代收集算法

上面我們提到了幾個名詞,新生代,老年代,這些就是分代算法中名詞。分代算法最主要的就是根據對象存活周期的不同將內存分成幾塊,一般是把 Java 堆分成新生代和老年代,這樣就可以根據各個年代的特點采用最適當的收集算法。

在新生代中,每次垃圾收集都發現有大批對象死去,只有少量存活,那就選用復制算法,只需要付出少量存活對象的復制成本就可以完成收集。而老年代中因為對象存活率高,沒有額外空間對它進行分配擔保,就必須使用“標記清理” 或者 “標記整理” 算法來進行回收。

2. 有哪些垃圾收集器

上面說的這些算法都是實現垃圾收集器的基礎。

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

Hotspot 虛擬機所包含的所有收集器如下圖:

技術分享圖片

從上圖中看到,一共有6種 GC 組合(忽略 G1 和 為CMS備份的 SerialOld 組合 )。

  1. Serial + Serial Old
  2. Serial + CMS
  3. ParNew + CMS
  4. ParNew + Serial Old
  5. Parallel Scavenge + Serial Old
  6. Parallel Scavenge + Parallel Old

大家看到這裏,一定有個疑問,為什麽需要這麽多垃圾收集器?

答案是:沒有任何一種垃圾收集器是完美的,沒有任何一種垃圾收集器適合所有的應用情況。

每個應用都需要自己的特定垃圾收集器,因此,可以說,GC 調優是門藝術,沒有放之四海皆準的 GC。需要工程師們去根據應用的特性不斷調優。

這麽多 GC ,限於篇幅,我們將在 深入淺出 JVM GC(3) 中慢慢解釋。
這裏只是列出一個大綱。

接下來我們將說說關於 GC 的一些概念,方便閱讀後面的關於 GC 處理器的文章。

3. 有哪些GC

  1. Young GC(又稱 YGC,minor GC,年輕代 GC)
  2. Old GC (老年代 GC,只有 CMS 才會單獨回收 Old 區)
  3. Full GC(又稱 major GC)
  4. Mixed GC(混合 GC,G1 收集器獨有)

關於這些 GC 的分類,R 大一個回答比較清楚:Major GC和Full GC的區別是什麽?觸發條件呢?

從大的方面講,GC 只分為兩種,一種是不收集整個堆,一種是收集整個堆。

Partial GC:並不收集整個GC堆的模式

  1. Young GC:只收集young gen的GC
  2. Old GC:只收集old gen的GC。只有CMS的concurrent collection是這個模式
  3. Mixed GC:收集整個young gen以及部分old gen的GC。只有G1有這個模式

Full GC:收集整個堆,包括young gen、old gen、perm gen(如果存在的話)等所有部分的模式。

1. YGC

YGC 又稱 Young GC ,minor GC ,年輕代 GC。顧名思義,該 GC 過程發生在年輕代中。從分代算法中,我們知道,JVM 為了性能考慮,通常將內存區域根據對象生命周期的不同分為年輕代和年老代。

新創建的對象基本上都存放在年輕代(除了一些大對象),因為大多數對象都是很快變成引用不可達,所以大多數對象都在年輕代中創建,然後消失。當對象從這塊內存區域消失時,我們稱之為 YGC。

什麽時候發生 YGC 呢?當 Eden 不夠放入新創建的對象時,也就是Eden 區滿了,JVM 就會清理Eden 區的空間,將存活的對象放入 to 區,如果 to 區放不下,則直接進入老年代。如果 to 區能放下,則放入 to 區,然後清理掉無用對象,第二次 YGC 時,GC 掃描 Eden 區和 to 區,將這兩個區的存活對象放入到 from 區,將 to 區清空(總之一定會保證有一個 Survivor 區是幹凈的),同樣的,如果 from 區放不下,則通過分配擔保機制進入老年代。如果 YGC 後,仍放不下新對象,則也通過分配擔保進入老年代。

2. Old GC

通常,我們將 Old GC 等同於 Full GC,為什麽呢?我們詳細解釋一下。

什麽時候發生 Old GC? 當老年代空間滿了的時候。也就是說通常是 YGC 後有很多對象進入到老年代,而老年代無法放下這些對象,這時候就需要對老年代 GC。而通常的 Old GC 其實就是 Full GC 。

3. Full GC

也就是全 GC ,對整個堆和方法區(如果存在)進行 GC。
哪些情況會 Full GC 呢?

  1. System.gc() 方法的調用
  2. heap dump 帶 GC
  3. 永久代(方法區)空間不夠
  4. 當準備出發 YGC 時,發現之前 YGC 後晉升對象的大小比目前 Old 區的剩余空間大,則不會觸發 YGC ,轉而直接觸發 Full GC。

第四條說到晉升,什麽是晉升呢?YGC 後,幸存的對象會放入到 Survivor 區,如果一個對象在多次 YGC 後仍然存活,則進入老年代,這個過程叫做晉升。每次 YGC 後,這個對象的年齡加一。當然,晉升的條件比較復雜。我們後面會詳細講述。

4. Mixed GC

G1 專屬GC,這裏不準備講述這個 GC。

總結

到這裏,我們解釋了3種垃圾回收算法,第四個不算是算法,而是一種設計。還大致講了5種收集器,並將這個坑留在了後面的文章裏,最後講了一些 GC 術語,YGC ,Old GC ,Full GC 等。

好了,關於5種垃圾收集器的詳細介紹,我們將在 深入淺出 JVM GC(3)中詳細說明。

good luck!!!

深入淺出 JVM GC(2)