JVM性能優化(二)垃圾回收算法詳解

一、什麼是垃圾回收

程序的運行必然需要申請內存資源,無效的對象資源如果不及時處理就會一直佔用內存資源,最終將導致內存溢出,所以對內存資源的管理是非常重要了。

1.1 C/C++語言的垃圾回收

在C/C++語言中,沒有自動垃圾回收機制,是通過new關鍵字申請內存資源,通過delete 關鍵字釋放內存資源,如果程序員在某些位置沒有寫delete進行釋放,那麼申請的對象將一直佔用內存資源,最終可能會導致內存溢出。

1.2 Java語言的垃圾回收

爲了讓程序員更專注於代碼的實現,而不用過多的考慮內存釋放的問題,所以在Java語言中,有了自動的垃圾回收機制,也就是我們熟悉的GC。
有了垃圾回收機制後,程序員只需要關心內存的申請即可,內存的釋放由系統自動識別完成。
換句話說,自動的垃圾回收的算法會變得非常重要了,如果因爲算法的不合理,導致內存一直沒有釋放,同樣也可能會導致內存溢出的。
除了Java語言,C#、Python等語言也都有自動的垃圾回收機制。

二、垃圾回收的常見算法

自動化的管理內存資源,垃圾回收機制必須要有一套算法來進行計算,哪些是有效的對象,哪些是無效的對象,對於無效的對象就要進行回收處理。
常見的垃圾回收算法有:引用計數法、標記清除法、標記壓縮法、複製算法、分代算法等。

2.1 引用計數法

引用計數是歷史最悠久的一種算法,最早George E.Collins在1960的時候首次提出,50年後的今天,該算法依然被很多編程語言使用。

2.1.1 原理

假設有一個對象A,任何一個對象對A的引用,那麼對象A的引用計數器+1,當引用失敗時,對象A的引用計數器就-1,如果對象A的計數器的值爲0,就說明對象A沒有引用了,可以被回收。

如圖所示:
在這裏插入圖片描述

2.1.2 優缺點
  • 優點:

    1. 實時性較高,無需等到內存不夠的時候,纔開始回收,運行時根據對象的計數器是否爲0,就可以直接回收。
    2. 在垃圾回收過程中,應用無需掛起,如果申請內存時,內存不足,則立刻報 outofmember 錯誤。
    3. 區域性,更新對象的計數器時,只是影響到該對象,不會掃描全部對象。
  • 缺點:

    1. 每次對象被引用時,都需要去更新計數器,有一點時間開銷。
    2. 浪費CPU資源,即使內存夠用,仍然在運行時進行計數器的統計。
    3. 無法解決循環引用問題。(最大缺點)

什麼是循環引用:

class TestA{
	public TestB b;
}

class TestB{
	public TestA a;
}
public class TestCycle {

	public static void main(String[] args) {
		TestA a = new TestA();
		TestB b = new TestB();
		a.b = b;
		b.a = a;
		a = null;
		b = null;
	}
}

雖然 a 和 b都爲 null,但是由於a和b存在循環引用,這樣a和b永遠都不會被回收。

如圖所示:
在這裏插入圖片描述

2.2 標記清除法

標記清除算法,是將垃圾回收分爲2個階段,分別是 標記和清除

  • 標記:從根節點開始標記引用的對象
  • 清除:未被標記引用的對象就是垃圾對象,可以被清理
2.2.1 原理:

在這裏插入圖片描述
這張圖代表的是程序運行期間所有對象的狀態,他們的標誌位全部是0(也就是未標記,以下默認0就是未標記,1爲已標記),假設現在有效內存空間耗盡了,JVM將會停止應用程序的運行並開啓GC線程,然後開始進行標記工作,按照根據搜索算法,標記完成以後,對象的狀態如下圖所示:
在這裏插入圖片描述

2.2.2 優缺點:

可以看到,標記清楚算法解決了引用計數算法中的循環引用的問題,沒有從root節點引用的對象都會被回收。
同樣標記清楚算法也是有缺點的:

  • 效率較低,標記和清除兩個動作都需要遍歷所有的對象,並且在GC時,需要停止應用程序,對於交互性要求比較高的應用而言這個體驗是非常差的。
  • 通過標記清楚算法清理出來的內存,碎片化較爲嚴重,因爲被回收的對象可能存在於內存的各個角落,所以清理出來的內存時不連貫的。

2.3 標記壓縮算法

標記壓縮算法是在標記清除算法的基礎之上,做了優化改進的算法,和標記清除算法一樣,也是從根節點開始,對對象的引用進行標記,在清理階段,並不是簡單的清理未標記的對象,而是將存活的對象壓縮到內存的一端,然後清理邊界以外的垃圾,從而解決了碎片化的問題。

2.3.1 原理:

在這裏插入圖片描述

2.3.2 優缺點:

優缺點同標記算法,解決了標記清除算法的碎片化的問題,同時,標記壓縮算法多了一步,對象移動內存位置的步驟,其效率也是有一定的影響。

2.4 複製算法

複製算法的核心就是,將原有的內存空間一分爲二,每次只用其中的一塊,在垃圾回收時,將正在使用的對象複製到另一個內存空間中,然後將該內存空間清空,交換兩個內存的角色,完成垃圾的回收。
如果內存的垃圾對象較多,需要複製的對象就較少,這種情況下適合使用該方式並且效率比較高,反之,則不適合。

在這裏插入圖片描述

在這裏插入圖片描述

2.4.1 JVM中年輕代內存空間:

在這裏插入圖片描述

  1. 在GC開始的時候,對象只會存在於Eden區和名爲 “From” 的Survivor區,Survivor區 “To” 是空的
  2. 緊接着進行GC,Eden區中所有存活的對象都會被複制到 “To” 區,而在 “From” 區中,仍存活的對象會根據他們的年齡值來決定去向,年齡達到一定值(年齡閾值,可以通過-XX:MaxTenuringThreshold來設置)的對象會被移動到年老代中,沒有達到閾值的對象會被複制到 “To” 區域中。
  3. 經過這次GC後,Eden區和From區已經被清空,這個時候,“From” 和 “To” 會交換他們的角色,也就是新的 “To” 就是上次GC前的 “From” ,新的 “From” 就是上次GC前的 “To”,不管怎樣,都會保證名爲 To 的 Survivor區域是空的。
  4. GC會一直重複這樣的過程,知道 “To” 區被填滿,“To” 區被填滿之後,會將所有對象移動到年老代中
2.4.2 優缺點

優點:

  • 在垃圾對象多的情況下,效率較高
  • 清理後,內存無碎片

缺點:

  • 在垃圾對象少的情況下,不適用,如:老年代內存
  • 分配的2塊內存空間,在同一個時刻,只能使用一半,內存使用率較低

2.4 分代算法

前面介紹了多種回收算法,沒有算法都有自己的優點也有缺點,誰都不能替代誰,所以根據垃圾回收對象的特點進行選擇,纔是明智的選擇。
分代算法其實就是這樣,根據回收算法的特點進行選擇,在JVM中,年輕代適合使用複製算法,老年代適合使用標記清楚或標記壓縮算法。

三、垃圾收集器以及內存分配

上面我們介紹了垃圾回收的算法,還需要有具體的體現,在jvm中,實現了多種垃圾收集器,包括:串行垃圾收集器、並行垃圾收集器、CMS(併發)垃圾收集器,G1垃圾收集器,接下來,我們一個個的瞭解學習。

3.1、串行垃圾收集器

串行垃圾收集器,是指使用單線程進行垃圾回收,垃圾回收時,只有一個線程在工作,並且java應用中的所有線程都要暫停,等待垃圾回收的完成,這種現象稱之爲 STW(Stop-The-World)。
對於交互性較強的應用而言,這種垃圾收集器是不能夠接收的。
一般在javaweb應用中是不會採用該收集器的。

3.1.1 編寫測試代碼
   // 實現:不斷的產生新的數據(對象),隨機的去廢棄對象(垃圾)
    public static void main(String[] args) throws Exception {
        List<Object> list = new ArrayList<>();
        while (true){
            int sleep = new Random().nextInt(100);
            if(System.currentTimeMillis() % 2 ==0){
                //當前的時間戳,是偶數
                list.clear();
            }else{
                //向List中添加1000個對象
                for (int i = 0; i < 10000; i++) {
                    Properties properties = new Properties();
                    properties.put("key_"+i,"value_"+System.currentTimeMillis()+i);
                    list.add(properties);
                }
            }
            Thread.sleep(sleep);
        }
    }
3.1.2 設置垃圾回收爲串行收集器

在這裏插入圖片描述

在程序運行參數添加2個參數,如下:

  • -XX:UseSerialGC:指定年輕代和老年代都是用串行垃圾收集器
  • -XX:PrintGCDetails:打印垃圾回收的詳細信息
# 爲了測試GC,將堆的初始值和最大內存都誰知爲16M
-XX:+UseSerialGC -XX:+PrintGCDetails -Xms16m -Xmx16m

啓動程序後,可以看到下面信息:

[GC (Allocation Failure) [DefNew: 4416K->511K(4928K), 0.0052836 secs] 4416K->1768K(15872K), 0.0053321 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 

[Full GC (Allocation Failure) [Tenured: 10943K->10943K(10944K), 0.0186019 secs] 15871K->14611K(15872K), [Metaspace: 3828K->3828K(1056768K)], 0.0186344 secs] [Times: user=0.02 sys=0.00, real=0.02 secs] 

GC日誌信息解讀:
年輕代的內存GC前後的大小:

  • DefNew:表示使用的是串行垃圾收集器
  • 4416K->511K(4928K): 表示,年輕代GC前,佔有4416K內存,GC後,佔有512K內存,總大小4928K
  • 0.0052836 secs: 表示,GC所用的時間,單位爲毫秒
  • **4416K->1768K(15872K):**表示,GC前,堆內存佔有4416K,GC後,佔有1973K,總大小爲 15872K
  • Full GC: 表示,內存空間全部進行GC

3.2、並行垃圾收集器

並行垃圾收集器在串行垃圾收集器的基礎之上做了改進,將單線程改爲多線程進行垃圾回收,這樣可以縮短垃圾回收的時間(這裏是指,並行能力較強的機器)
不過,並行垃圾收集器在收集的過程中也會暫停應用程序,這個和串行垃圾回收器是一樣的,只是並行執行,速度更快些,暫停的時間更短一些。

3.2.1 parNew垃圾收集器

ParNew垃圾收集器是工作在年輕代上的,只是將串行的垃圾收集器改爲了並行。
通過 -XX:UseParNewGC 參數設置年輕代使用ParNew回收器,老年代使用的依然是串行收集器。

測試:
在這裏插入圖片描述

# 參數
-XX:+UseParNewGC -XX:+PrintGCDetails -Xms16m -Xmx16m

# 打印的日誌信息
[GC (Allocation Failure) [ParNew: 4416K->512K(4928K), 0.0015707 secs] 4416K->1818K(15872K), 0.0016110 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

[Full GC (Allocation Failure) [Tenured: 10944K->1185K(10944K), 0.0086455 secs] 15872K->1185K(15872K), [Metaspace: 3835K->3835K(1056768K)], 0.0086862 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 

由以上信息可以看出,ParNew: 使用的是ParNew收集器,其他信息和串行收集器一致。

3.2.2 ParallelGC垃圾收集器

ParallelGC垃圾收集器工作機制和ParNewGC收集器一樣,只是在此基礎之上,新增了兩個和系統吞吐量相關的參數,使得其使用起來更加的靈活和高效
相關參數如下:

  • -XX:+UserParallelGC: 年輕代使用ParallelGC垃圾回收器,老年代使用串行回收器

  • -XX:+UseParallelOldGC: 年輕代使用ParallelGC垃圾回收器,來年代使用ParallelOldGC垃圾回收器

  • -XX:MaxGCPauseMillis:

    1. 設置最大的垃圾收集時的停頓時間,單位爲毫秒
    2. 需要注意的是,ParallelGC爲了達到設置的停頓時間,可能會調整堆大小或其他的參數,如果堆的大小設置的較小,就會導致GC工作變的頻繁,反而可能影響到性能。
    3. 使用這個參數需謹慎
  • -XX:GCTimeRatio

    1. 設置垃圾回收時間佔程序運行時間的百分比,公式爲 1/(1+n)
    2. 它的值爲 0~100 之間的數字,默認值爲99,也就是垃圾回收時間不能超過1%
  • -XX:UseAdaptiveSizePolicy:

    1. 自適應GC模式,垃圾回收器將自動調整新生代,老年代等參數,達到吞吐量,堆大小、停頓時間之間的平衡
    2. 一般用於,手動調整參數比較困難的場景,讓收集器自動進行調整
#參數
-XX:+UseParallelGC -XX:+UseParallelOldGC -XX:MaxGCPauseMillis=100 -XX:+PrintGCDetails -Xms16m -Xmx16m

#打印的信息
[GC (Allocation Failure) [PSYoungGen: 4096K->512K(4608K)] 4096K->1683K(15872K), 0.0021804 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

[Full GC (Ergonomics) [PSYoungGen: 1472K->0K(3584K)] [ParOldGen: 9577K->3990K(11264K)] 11049K->3990K(14848K), [Metaspace: 3828K->3828K(1056768K)], 0.0191664 secs] [Times: user=0.01 sys=0.00, real=0.02 secs] 

3.3、CMS垃圾收集器

CMS全稱 Concurrent Mark Sweep ,是一款併發的、使用標記-清除算法的垃圾回收器,該回收器是針對老年代垃圾回收的,通過參數 -XX:+UseConcMarkSweepGC進行設置的

CMS垃圾回收器的執行過程中如下:
在這裏插入圖片描述

  • 初始化標記(CMS-initial-mark),標記root,會導致stw
  • 併發標記(CMS-concurrent-mark),與用戶線程同時運行
  • 預清理(CMS-concurrent-preclean),與用戶線程同時運行
  • 重新標記(CMS-remark),會導致stw
  • 併發清除(CMS-concurrent-sweep),與用戶線程同時運行
  • 調整堆大小,設置CMS在清理之後進行內存壓縮,目的是清理內存中的碎片
  • 併發重置狀態等待下次CMS的處罰(CMS-concurrent-reset),與用戶線程同時運行

測試:
在這裏插入圖片描述

#設置啓動參數
-XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -Xms16m -Xmx16m

#運行日誌
[GC (Allocation Failure) [ParNew: 4416K->511K(4928K), 0.0050460 secs] 4416K->1954K(15872K), 0.0050891 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 

#第一步:初始3標記
[GC (CMS Initial Mark) [1 CMS-initial-mark: 6075K(10944K)] 6938K(15872K), 0.0003563 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

#第二步:併發標記
[CMS-concurrent-mark-start]
[CMS-concurrent-mark: 0.002/0.002 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

#第三步:預處理
[CMS-concurrent-preclean-start]
[CMS-concurrent-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

#第四步:重新標記
[GC (CMS Final Remark) [YG occupancy: 862 K (4928 K)][Rescan (parallel) , 0.0001950 secs][weak refs processing, 0.0000292 secs][class unloading, 0.0003607 secs][scrub symbol table, 0.0005794 secs][scrub string table, 0.0002047 secs][1 CMS-remark: 6075K(10944K)] 6938K(15872K), 0.0015122 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

#第五步:併發清理
[CMS-concurrent-sweep-start]
[CMS-concurrent-sweep: 0.002/0.002 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

#第六步:重置
[CMS-concurrent-reset-start]
[CMS-concurrent-reset: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

四、結束語

今天的內容就到這裏了,文中詳細描述了 垃圾回收算法和垃圾收集器的類型和作用,也通過案例給大家展示出來了,大家有什麼疑問或者問題可以在下方留言,我會在第一時間回覆大家,下一篇會講最重要的垃圾收集器——G1垃圾收集器,感興趣的小夥伴記得關注,大家加油,我是牧小農,我爲自己代言。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章