1. 程式人生 > 其它 >深入淺出JVM之垃圾回收

深入淺出JVM之垃圾回收

垃圾回收

1、如何判斷物件可以回收

引用計數法

弊端:迴圈引用時,兩個物件的計數都為1,導致兩個物件都無法被釋放

可達性分析演算法

  • JVM中的垃圾回收器通過可達性分析來探索所有存活的物件
  • 掃描堆中的物件,看能否沿著GC Root物件為起點的引用鏈找到該物件,如果找不到,則表示可以回收
  • 可以作為GC Root的物件
    • 虛擬機器棧(棧幀中的本地變量表)中引用的物件。 
    • 方法區中類靜態屬性引用的物件
    • 方法區中常量引用的物件
    • 本地方法棧中JNI(即一般說的Native方法)引用的物件

五種引用

強引用

只有GC Root都不引用該物件時,才會回收強引用物件

  • 如上圖B、C物件都不引用A1物件時,A1物件才會被回收
軟引用

當GC Root指向軟引用物件時,在記憶體不足時,會回收軟引用所引用的物件

  • 如上圖如果B物件不再引用A2物件且記憶體不足時,軟引用所引用的A2物件就會被回收
軟引用的使用
public class Demo1 {
	public static void main(String[] args) {
		final int _4M = 4*1024*1024;
		//使用軟引用物件 list和SoftReference是強引用,而SoftReference和byte陣列則是軟引用
		List<SoftReference<byte[]>> list = new ArrayList<>();
		SoftReference<byte[]> ref= new SoftReference<>(new byte[_4M]);
	}
}Copy

如果在垃圾回收時發現記憶體不足,在回收軟引用所指向的物件時,軟引用本身不會被清理

如果想要清理軟引用,需要使用引用佇列

public class Demo1 {
	public static void main(String[] args) {
		final int _4M = 4*1024*1024;
		//使用引用佇列,用於移除引用為空的軟引用物件
		ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
		//使用軟引用物件 list和SoftReference是強引用,而SoftReference和byte陣列則是軟引用
		List<SoftReference<byte[]>> list = new ArrayList<>();
		SoftReference<byte[]> ref= new SoftReference<>(new byte[_4M]);

		//遍歷引用佇列,如果有元素,則移除
		Reference<? extends byte[]> poll = queue.poll();
		while(poll != null) {
			//引用佇列不為空,則從集合中移除該元素
			list.remove(poll);
			//移動到引用佇列中的下一個元素
			poll = queue.poll();
		}
	}
}Copy

大概思路為:檢視引用佇列中有無軟引用,如果有,則將該軟引用從存放它的集合中移除(這裡為一個list集合)

弱引用

只有弱引用引用該物件時,在垃圾回收時,無論記憶體是否充足,都會回收弱引用所引用的物件

  • 如上圖如果B物件不再引用A3物件,則A3物件會被回收

弱引用的使用和軟引用類似,只是將 SoftReference 換為了 WeakReference

虛引用

當虛引用物件所引用的物件被回收以後,虛引用物件就會被放入引用佇列中,呼叫虛引用的方法

  • 虛引用的一個體現是釋放直接記憶體所分配的記憶體,當引用的物件ByteBuffer被垃圾回收以後,虛引用物件Cleaner就會被放入引用佇列中,然後呼叫Cleaner的clean方法來釋放直接記憶體
  • 如上圖,B物件不再引用ByteBuffer物件,ByteBuffer就會被回收。但是直接記憶體中的記憶體還未被回收。這時需要將虛引用物件Cleaner放入引用佇列中,然後呼叫它的clean方法來釋放直接記憶體
終結器引用

所有的類都繼承自Object類,Object類有一個finalize方法。當某個物件不再被其他的物件所引用時,會先將終結器引用物件放入引用佇列中,然後根據終結器引用物件找到它所引用的物件,然後呼叫該物件的finalize方法。呼叫以後,該物件就可以被垃圾回收了

  • 如上圖,B物件不再引用A4物件。這是終結器物件就會被放入引用佇列中,引用佇列會根據它,找到它所引用的物件。然後呼叫被引用物件的finalize方法。呼叫以後,該物件就可以被垃圾回收了
引用佇列
  • 軟引用和弱引用可以配合引用佇列
    • 弱引用虛引用所引用的物件被回收以後,會將這些引用放入引用佇列中,方便一起回收這些軟/弱引用物件
  • 虛引用和終結器引用必須配合引用佇列
    • 虛引用和終結器引用在使用時會關聯一個引用佇列

2、垃圾回收演算法

標記-清除

定義:標記清除演算法顧名思義,是指在虛擬機器執行垃圾回收的過程中,先採用標記演算法確定可回收物件,然後垃圾收集器根據標識清除相應的內容,給堆記憶體騰出相應的空間

  • 這裡的騰出記憶體空間並不是將記憶體空間的位元組清0,而是記錄下這段記憶體的起始結束地址,下次分配記憶體的時候,會直接覆蓋這段記憶體

缺點容易產生大量的記憶體碎片,可能無法滿足大物件的記憶體分配,一旦導致無法分配物件,那就會導致jvm啟動gc,一旦啟動gc,我們的應用程式就會暫停,這就導致應用的響應速度變慢

標記-整理

標記-整理 會將不被GC Root引用的物件回收,清楚其佔用的記憶體空間。然後整理剩餘的物件,可以有效避免因記憶體碎片而導致的問題,但是因為整體需要消耗一定的時間,所以效率較低

複製

將記憶體分為等大小的兩個區域,FROM和TO(TO中為空)。先將被GC Root引用的物件從FROM放入TO中,再回收不被GC Root引用的物件。然後交換FROM和TO。這樣也可以避免記憶體碎片的問題,但是會佔用雙倍的記憶體空間。

3、分代回收

回收流程

新建立的物件都被放在了新生代的伊甸園

當伊甸園中的記憶體不足時,就會進行一次垃圾回收,這時的回收叫做 Minor GC

Minor GC 會將伊甸園和倖存區FROM存活的物件複製到 倖存區 TO中, 並讓其壽命加1,再交換兩個倖存區

再次建立物件,若新生代的伊甸園又滿了,則會再次觸發 Minor GC(會觸發 stop the world, 暫停其他使用者執行緒,只讓垃圾回收執行緒工作),這時不僅會回收伊甸園中的垃圾,還會回收倖存區中的垃圾,再將活躍物件複製到倖存區TO中。回收以後會交換兩個倖存區,並讓倖存區中的物件壽命加1

如果倖存區中的物件的壽命超過某個閾值(最大為15,4bit),就會被放入老年代

如果新生代老年代中的記憶體都滿了,就會先觸發Minor GC,再觸發Full GC,掃描新生代和老年代中所有不再使用的物件並回收

GC 分析

大物件處理策略

當遇到一個較大的物件時,就算新生代的伊甸園為空,也無法容納該物件時,會將該物件直接晉升為老年代

執行緒記憶體溢位

某個執行緒的記憶體溢位了而拋異常(out of memory),不會讓其他的執行緒結束執行

這是因為當一個執行緒丟擲OOM異常後它所佔據的記憶體資源會全部被釋放掉,從而不會影響其他執行緒的執行,程序依然正常

4、垃圾回收器

相關概念

並行收集:指多條垃圾收集執行緒並行工作,但此時使用者執行緒仍處於等待狀態

併發收集:指使用者執行緒與垃圾收集執行緒同時工作(不一定是並行的可能會交替執行)。使用者程式在繼續執行,而垃圾收集程式執行在另一個CPU上

吞吐量:即CPU用於執行使用者程式碼的時間與CPU總消耗時間的比值(吞吐量 = 執行使用者程式碼時間 / ( 執行使用者程式碼時間 + 垃圾收集時間 )),也就是。例如:虛擬機器共執行100分鐘,垃圾收集器花掉1分鐘,那麼吞吐量就是99%

序列

  • 單執行緒
  • 記憶體較小,個人電腦(CPU核數較少)

安全點:讓其他執行緒都在這個點停下來,以免垃圾回收時移動物件地址,使得其他執行緒找不到被移動的物件

因為是序列的,所以只有一個垃圾回收執行緒。且在該執行緒執行回收工作時,其他執行緒進入阻塞狀態

Serial 收集器

Serial收集器是最基本的、發展歷史最悠久的收集器

特點:單執行緒、簡單高效(與其他收集器的單執行緒相比),採用複製演算法。對於限定單個CPU的環境來說,Serial收集器由於沒有執行緒互動的開銷,專心做垃圾收集自然可以獲得最高的單執行緒手機效率。收集器進行垃圾回收時,必須暫停其他所有的工作執行緒,直到它結束(Stop The World)

ParNew 收集器

ParNew收集器其實就是Serial收集器的多執行緒版本

特點:多執行緒、ParNew收集器預設開啟的收集執行緒數與CPU的數量相同,在CPU非常多的環境中,可以使用-XX:ParallelGCThreads引數來限制垃圾收集的執行緒數。和Serial收集器一樣存在Stop The World問題

Serial Old 收集器

Serial Old是Serial收集器的老年代版本

特點:同樣是單執行緒收集器,採用標記-整理演算法

吞吐量優先

  • 多執行緒
  • 堆記憶體較大,多核CPU
  • 單位時間內,STW(stop the world,停掉其他所有工作執行緒)時間最短
  • JDK1.8預設使用的垃圾回收器

Parallel Scavenge 收集器

與吞吐量關係密切,故也稱為吞吐量優先收集器

特點:屬於新生代收集器也是採用複製演算法的收集器(用到了新生代的倖存區),又是並行的多執行緒收集器(與ParNew收集器類似)

該收集器的目標是達到一個可控制的吞吐量。還有一個值得關注的點是:GC自適應調節策略(與ParNew收集器最重要的一個區別)

GC自適應調節策略:Parallel Scavenge收集器可設定-XX:+UseAdptiveSizePolicy引數。當開關開啟時不需要手動指定新生代的大小(-Xmn)、Eden與Survivor區的比例(-XX:SurvivorRation)、晉升老年代的物件年齡(-XX:PretenureSizeThreshold)等,虛擬機器會根據系統的執行狀況收集效能監控資訊,動態設定這些引數以提供最優的停頓時間和最高的吞吐量,這種調節方式稱為GC的自適應調節策略。

Parallel Scavenge收集器使用兩個引數控制吞吐量:

  • XX:MaxGCPauseMillis 控制最大的垃圾收集停頓時間
  • XX:GCRatio 直接設定吞吐量的大小
Parallel Old 收集器

是Parallel Scavenge收集器的老年代版本

特點:多執行緒,採用標記-整理演算法(老年代沒有幸存區)

響應時間優先

  • 多執行緒
  • 堆記憶體較大,多核CPU
  • 儘可能讓單次STW時間變短(儘量不影響其他執行緒執行)

CMS 收集器

Concurrent Mark Sweep,一種以獲取最短回收停頓時間為目標的老年代收集器

特點:基於標記-清除演算法實現。併發收集、低停頓,但是會產生記憶體碎片

應用場景:適用於注重服務的響應速度,希望系統停頓時間最短,給使用者帶來更好的體驗等場景下。如web程式、b/s服務

CMS收集器的執行過程分為下列4步:

初始標記:標記GC Roots能直接到的物件。速度很快但是仍存在Stop The World問題

併發標記:進行GC Roots Tracing 的過程,找出存活物件且使用者執行緒可併發執行

重新標記:為了修正併發標記期間因使用者程式繼續執行而導致標記產生變動的那一部分物件的標記記錄。仍然存在Stop The World問題

併發清除:對標記的物件進行清除回收

CMS收集器的記憶體回收過程是與使用者執行緒一起併發執行

G1

定義

Garbage First

JDK 9以後預設使用,而且替代了CMS 收集器

適用場景
  • 同時注重吞吐量和低延遲(響應時間)
  • 超大堆記憶體(記憶體大的),會將堆記憶體劃分為多個大小相等的區域
  • 整體上是標記-整理演算法,兩個區域之間是複製演算法

相關引數:JDK8 並不是預設開啟的,所需要引數開啟

G1垃圾回收階段

新生代伊甸園垃圾回收—–>記憶體不足,新生代回收+併發標記—–>回收新生代伊甸園、倖存區、老年代記憶體——>新生代伊甸園垃圾回收(重新開始)

Young Collection

分割槽演算法region

分代是按物件的生命週期劃分,分割槽則是將堆空間劃分連續幾個不同小區間,每一個小區間獨立回收,可以控制一次回收多少個小區間,方便控制 GC 產生的停頓時間

E:伊甸園 S:倖存區 O:老年代

  • 會STW

Young Collection + CM

CM:併發標記

  • 在 Young GC 時會對 GC Root 進行初始標記
  • 在老年代佔用堆記憶體的比例達到閾值時,對進行併發標記(不會STW),閾值可以根據使用者來進行設定

Mixed Collection

會對E S O 進行全面的回收

  • 最終標記
  • 拷貝存活

-XX:MaxGCPauseMills:xxx 用於指定最長的停頓時間

:為什麼有的老年代被拷貝了,有的沒拷貝?

因為指定了最大停頓時間,如果對所有老年代都進行回收,耗時可能過高。為了保證時間不超過設定的停頓時間,會回收最有價值的老年代(回收後,能夠得到更多記憶體)

Full GC

G1在老年代記憶體不足時(老年代所佔記憶體超過閾值)

  • 如果垃圾產生速度慢於垃圾回收速度,不會觸發Full GC,還是併發地進行清理
  • 如果垃圾產生速度快於垃圾回收速度,便會觸發Full GC
Young Collection 跨代引用
  • 新生代回收的跨代引用(老年代引用新生代)問題

  • 卡表與Remembered Set
    • Remembered Set 存在於E中,用於儲存新生代物件對應的髒卡
      • 髒卡:O被劃分為多個區域(一個區域512K),如果該區域引用了新生代物件,則該區域被稱為髒卡
  • 在引用變更時通過post-write barried + dirty card queue
  • concurrent refinement threads 更新 Remembered Set

Remark

重新標記階段

在垃圾回收時,收集器處理物件的過程中

黑色:已被處理,需要保留的 灰色:正在處理中的 白色:還未處理的

但是在併發標記過程中,有可能A被處理了以後未引用C,但該處理過程還未結束,在處理過程結束之前A引用了C,這時就會用到remark

過程如下

  • 之前C未被引用,這時A引用了C,就會給C加一個寫屏障,寫屏障的指令會被執行,將C放入一個隊列當中,並將C變為 處理中 狀態
  • 併發標記階段結束以後,重新標記階段會STW,然後將放在該佇列中的物件重新處理,發現有強引用引用它,就會處理它

JDK 8u20 字串去重

過程

  • 將所有新分配的字串(底層是char[])放入一個佇列
  • 當新生代回收時,G1併發檢查是否有重複的字串
  • 如果字串的值一樣,就讓他們引用同一個字串物件
  • 注意,其與String.intern的區別
    • intern關注的是字串物件
    • 字串去重關注的是char[]
    • 在JVM內部,使用了不同的字串標

優點與缺點

  • 節省了大量記憶體
  • 新生代回收時間略微增加,導致略微多佔用CPU
JDK 8u40 併發標記類解除安裝

在併發標記階段結束以後,就能知道哪些類不再被使用。如果一個類載入器的所有類都不在使用,則解除安裝它所載入的所有類

JDK 8u60 回收巨型物件
  • 一個物件大於region的一半時,就稱為巨型物件
  • G1不會對巨型物件進行拷貝
  • 回收時被優先考慮
  • G1會跟蹤老年代所有incoming引用,如果老年代incoming引用為0的巨型物件就可以在新生代垃圾回收時處理掉

5、GC 調優

檢視虛擬機器引數命令

"F:\JAVA\JDK8.0\bin\java" -XX:+PrintFlagsFinal -version | findstr "GC"Copy

可以根據引數去查詢具體的資訊

調優領域

  • 記憶體
  • 鎖競爭
  • CPU佔用
  • IO
  • GC

確定目標

低延遲/高吞吐量? 選擇合適的GC

  • CMS G1 ZGC
  • ParallelGC
  • Zing

最快的GC是不發生GC

首先排除減少因為自身編寫的程式碼而引發的記憶體問題

  • 檢視Full GC前後的記憶體佔用,考慮以下幾個問題
    • 資料是不是太多?
    • 資料表示是否太臃腫
      • 物件圖
      • 物件大小
    • 是否存在記憶體洩漏

新生代調優

  • 新生代的特點
    • 所有的new操作分配記憶體都是非常廉價的
      • TLAB
    • 死亡物件回收零代價
    • 大部分物件用過即死(朝生夕死)
    • MInor GC 所用時間遠小於Full GC
  • 新生代記憶體越大越好麼?
    • 不是
      • 新生代記憶體太小:頻繁觸發Minor GC,會STW,會使得吞吐量下降
      • 新生代記憶體太大:老年代記憶體佔比有所降低,會更頻繁地觸發Full GC。而且觸發Minor GC時,清理新生代所花費的時間會更長
    • 新生代記憶體設定為內容納[併發量*(請求-響應)]的資料為宜

倖存區調優

  • 倖存區需要能夠儲存 當前活躍物件+需要晉升的物件
  • 晉升閾值配置得當,讓長時間存活的物件儘快晉升

老年代調優