JVM調優(yōu)-GC篇

原作者 https://smartan123.github.io/book/?file=home-%E9%A6%96%E9%A1%B5

怕丟失做CV拷貝處理

慕課網(wǎng)課程地址: https://coding.imooc.com/class/chapter/442.html#Anchor

請多多支持正版

一、什么是垃圾回收?

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

1、c/c++語言中的垃圾回收

在C/C++語言中,沒有自動垃圾回收機制,是通過new關鍵字申請內存資源,通過delete關鍵字釋放內存資源。

如果,程序員在某些位置沒有寫delete進行釋放,那么申請的對象將一直占用內存資源,最終可能會導致內存溢出 。

2、java語言中的垃圾回收

為了讓程序員更專注于代碼的實現(xiàn),而不用過多的考慮內存釋放的問題,所以,在Java語言中,有了自動的垃圾回收機制,也就是我們熟悉的GC。

有了垃圾回收機制后,程序員只需要關心內存的申請即可,內存的釋放由系統(tǒng)自動識別完成。

換句話說,自動的垃圾回收的算法就會變得非常重要了,如果因為算法的不合理,導致內存資源一直沒有釋放,同樣也可能會導致內存溢出的。

當然,除了Java語言,C#、Python等語言也都有自動的垃圾回收機制。

二、垃圾回收的常見算法

自動化的管理內存資源,垃圾回收機制必須要有一套算法來進行計算,哪些是有效的對象,哪些是無效的對象,對于無效的對象就要進行回收處理。

常見的垃圾回收算法有:引用計數(shù)法、標記清除法、標記壓縮法、復制算法、分代算法等。

1、引用計數(shù)法

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

1)、原理

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

2)、優(yōu)缺點

優(yōu)點:

  • 實時性較高,無需等到內存不夠的時候,才開始回收,運行時根據(jù)對象的計數(shù)器是否為0,就可以直接回收。
  • 在垃圾回收過程中,應用無需掛起。如果申請內存時,內存不足,則立刻報outofmemory 錯誤。
  • 區(qū)域性,更新對象的計數(shù)器時,只是影響到該對象,不會掃描全部對象。

缺點:

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

什么是循環(huán)引用?

1590300681893

具體講解請觀看視頻

2、標記清除法

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

標記:從根節(jié)點開始標記引用的對象

清除:未被標記引用的對象就是垃圾對象,可以被清理

1)、原理

1590300826193

上面這張圖代表的是程序運行期間所有對象的狀態(tài),它們的標志位全部是0(也就是未標記,以下默認0就是未標記,1為已標記),假設這會兒有效內存空間耗盡了,JVM將會停止應用程序的運行并開啟GC線程,然后開始進行標記工作,按照根搜索算法,標記完以后,所有從root對象可達的對象就被標記為了存活的對象,此時已經(jīng)完成了第一階段標記。接下來,就要執(zhí)行第二階段清除了,那么清除完以后,剩下的對象以及對象的狀態(tài)如下圖所示 :

1590300926143

可以看到,沒有被標記的對象將會回收清除掉,而被標記的對象將會留下,并且會將標記位重新歸0。接下來就不用說了,喚醒停止的程序線程,讓程序繼續(xù)運行即可。

2)、優(yōu)缺點

優(yōu)點:

  • 標記清除算法解決了引用計數(shù)算法中的循環(huán)引用的問題,沒有從root節(jié)點引用的對象都會被回收。

缺點:

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

3、標記壓縮法

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

1)、原理

1590301249595

2)、優(yōu)缺點

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

4、復制算法

復制算法的核心就是,將原有的內存空間一分為二,每次只用其中的一塊,在垃圾回收時,將正在使用的對象復制到另一個內存空間中,然后將該內存空間清空,交換兩個內存的角色,完成垃圾的回收。

如果內存中的垃圾對象較多,需要復制的對象就較少,這種情況下適合使用該方式并且效率比較高,反之,則不適合。

1)、原理

1590301371613

具體分析講解見視頻

2)、JVM中年輕代內存空間

[圖片上傳中...(image-f4e431-1600420999679-25)]

  • 在GC開始的時候,對象只會存在于Eden區(qū)和名為“From”的Survivor區(qū),Survivor區(qū)“To”是空的。
  • 緊接著進行GC,Eden區(qū)中所有存活的對象都會被復制到“To”,而在“From”區(qū)中,仍存活的對象會根據(jù)他們的年齡值來決定去向。年齡達到一定值(年齡閾值,可以通過-XX:MaxTenuringThreshold來設置)的對象會被移動到年老代中,沒有達到閾值的對象會被復制到“To”區(qū)域。
  • 經(jīng)過這次GC后,Eden區(qū)和From區(qū)已經(jīng)被清空。這個時候,“From”和“To”會交換他們的角色,也就是的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎樣,都會保證名為To的Survivor區(qū)域是空的。
  • GC會一直重復這樣的過程,直到“To”區(qū)被填滿,“To”區(qū)被填滿之后,會將所有對象移動到年老代中。

3)、優(yōu)缺點

優(yōu)點:

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

缺點:

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

5、分代算法

前面介紹了多種回收算法,每一種算法都有自己的優(yōu)點也有缺點,誰都不能替代誰,所以根據(jù)垃圾回收對象的特點進行選擇,才是明智的選擇。

分代算法其實就是這樣的,根據(jù)回收對象的特點進行選擇,在jvm中,年輕代適合使用復制算法,老年代適合使用標記清除或標記壓縮算法。

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

前面我們講了垃圾回收的算法,還需要有具體的實現(xiàn),在jvm中,實現(xiàn)了多種垃圾收集器,包括:串行垃圾收集器、并行垃圾收集器、CMS(并發(fā))垃圾收集器、G1垃圾收集器,接下來,我們一個個的了解學習。

垃圾收集器種類

1590301827427

HotSpot虛擬機所包含的收集器

1590301854328

垃圾收集器部分源碼

1590301881521

垃圾收集器后臺日志參數(shù)說明與配對關系

  • DefNew - Default New Generation
  • Tenured - Old
  • ParNew - Parallel New Generation
  • PSYoungGen - Parallel Scavenge
  • ParOldGen - Parallel Old Generation

以上不同種類,已經(jīng)回收器的配對使用分析講解見視頻

1、串行垃圾收集器

串行垃圾收集器是最基本的、發(fā)展歷史最悠久的收集器。

特點:單線程、簡單高效(與其他收集器的單線程相比),對于限定單個CPU的環(huán)境來說,Serial收集器由于沒有線程交互的開銷,專心做垃圾收集自然可以獲得最高的單線程收集效率。收集器進行垃圾回收時,必須暫停其他所有的工作線程,直到它結束(Stop The World)。

串行垃圾收集器運行示意圖

1590302084563

串行垃圾收集器原理分析見視頻

1)、編寫測試代碼

思路:while循環(huán)中不斷拼接字符串,直到oom異常,查看gc情況

import java.util.UUID;

/**************************************************
 *
 * @title
 * @desc ling
 * @author smart哥
 *
 **************************************************/
public class TestGC1 {
    /**
     * java -XX:+PrintCommandLineFlags -version
     *
     * @param args
     */
    public static void main(String[] args) {
        String str = "smart哥";
        while (true) {
            str += str + UUID.randomUUID();
            str.intern();
        }
    }
}

2)、設置垃圾回收為串行收集器

在程序運行參數(shù)中添加2個參數(shù),如下:

-XX:+UseSerialGC 指定年輕代和老年代都使用串行垃圾收集器

-XX:+PrintGCDetails 打印垃圾回收的詳細信息

3)、啟動程序,GC日志信息解讀

1590302432800

具體解讀請觀看視頻

2、并行垃圾收集器

并行垃圾收集器在串行垃圾收集器的基礎之上做了改進,將單線程改為了多線程進行垃圾回收,這樣可以縮短垃圾回收的時間。(這里是指,并行能力較強的機器)

當然了,并行垃圾收集器在收集的過程中也會暫停應用程序,這個和串行垃圾回收器是一樣的,只是并行執(zhí)行,速度更快些,暫停的時間更短一些。

并行垃圾收集器-ParNew運行示意圖

1590302579723

ParNew垃圾收集器

通過-XX:+UseParNewGC參數(shù)設置年輕代使用ParNew回收器,老年代使用的依然是串行收集器

通過-XX:+ParallelGCThreads可以限制GC線程數(shù)量,默認開啟和cpu數(shù)目相同的線程數(shù)

具體講解見視頻

1)、編寫測試代碼

同之前的代碼

2)、設置垃圾回收為并行收集器ParNew

在程序運行參數(shù)中添加1個參數(shù),如下

-XX:+UseParNewGC

3)、啟動程序,GC日志信息解讀

1590302860478

具體解讀請觀看視頻

并行垃圾收集器-ParallelGC運行示意圖

1590302913513

ParallelGC垃圾收集器

ParallelGC收集器工作機制和ParNewGC收集器一樣,只是在此基礎之上,新增了兩個和系統(tǒng)吞吐量相關的參數(shù),使得其使用起來更加的靈活和高效。

具體講解見視頻

1)、編寫測試代碼

同之前的代碼

2)、設置垃圾回收為并行收集器ParallelGC

ParallelGC垃圾收集器相關參數(shù)如下:

-XX:+UseParallelGC -XX:+UseParallelOldGC -XX:MaxGCPauseMillis -XX:ParallelGCThreads=N

3)、啟動程序,GC日志信息解讀

1590303148247

具體解讀見視頻

3、CMS垃圾收集器

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

CMS垃圾收集器運行示意圖

1590303242254

示意圖原理講解見視頻

CMS垃圾回收器的執(zhí)行過程如下:

1590303288641

執(zhí)行過程具體講解見視頻

1)、編寫測試代碼

同之前的代碼

2)、設置CMS垃圾回收參數(shù)

-‐XX:+UseConcMarkSweepGC

注意:開啟后將采用ParNew+CMS+Serial Old收集器組合

3)、啟動程序,GC日志信息解讀

1590303375232

具體解讀見視頻

4、G1垃圾收集器

G1垃圾收集器是在jdk1.7update4中正式使用的全新的垃圾收集器,oracle官方在jdk9中將G1變成默認的垃圾收集器,以替代CMS。

G1的設計原則就是簡化JVM性能調優(yōu),開發(fā)人員只需要簡單的三步即可完成調優(yōu):

  • 第一步,開啟G1垃圾收集器
  • 第二步,設置堆的最大內存
  • 第三步,設置最大的停頓時間
    • G1中提供了三種模式垃圾回收模式,Young GC、Mixed GC 和 Full GC,在不同的條件下被觸發(fā)。

G1垃圾收集器相對比其他收集器而言,最大的區(qū)別在于它取消了年輕代、老年代的物理劃分,取而代之的是將堆劃分為若干個區(qū)域(Region),這些區(qū)域中包含了有邏輯上的年輕代、老年代區(qū)域。這樣做的好處就是,我們再也不用單獨的空間對每個代進行設置了,不用擔心每個代內存是否足夠。

G1垃圾收集器(將新生代,老年代的物理空間劃分取消了),示意圖如下

1590303602771

G1垃圾收集器(G1算法將堆劃分為若干個區(qū)域-Region)

1590303631684

G1垃圾收集器原理

1590303683132
1590303692110

在G1劃分的區(qū)域中,年輕代的垃圾收集依然采用暫停所有應用線程的方式,將存活對象拷貝到老年代或者Survivor空間,G1收集器通過將對象從一個區(qū)域復制到另外一個區(qū)域,完成了清理工作。

這就意味著,在正常的處理過程中,G1完成了堆的壓縮(至少是部分堆的壓縮),這樣也就不會有cms內存碎片問題的存在了。

在G1中,有一種特殊的區(qū)域,叫Humongous區(qū)域。如果一個對象占用的空間超過了分區(qū)容量50%以上,G1收集器就認為這是一個巨型對象。

這些巨型對象,默認直接會被分配在老年代,但是如果它是一個短期存在的巨型對象,就會對垃圾收集器造成負面影響。

為了解決這個問題,G1劃分了一個Humongous區(qū),它用來專門存放巨型對象。如果一個H區(qū)裝不下一個巨型對象,那么G1會尋找連續(xù)的H分區(qū)來存儲。為了能找到連續(xù)的H區(qū),有時候不得不啟動Full GC。

針對Young GC主要是對Eden區(qū)進行GC,它在Eden空間耗盡時會被觸發(fā)。Eden空間的數(shù)據(jù)移動到Survivor空間中,如果Survivor空間不夠,Eden空間的部分數(shù)據(jù)會直接晉升到年老代空間。

Survivor區(qū)的數(shù)據(jù)移動到新Survivor區(qū)中,也有部分數(shù)據(jù)晉升到老年代空間中。最終Eden空間的數(shù)據(jù)為空,GC停止工作,應用線程繼續(xù)執(zhí)行。

G1垃圾回收模式:Young GC

1590303868180

具體模式講解見視頻

G1垃圾回收模式:Mixed GC

分2步:

  • 全局并發(fā)標記(global concurrent marking)
  • 拷貝存活對象(evacuation)

G1垃圾收集器運行示意圖

1590303955056

具體講解見視頻

1)、編寫測試代碼

同之前的代碼

2)、設置G1垃圾回收參數(shù)

‐XX:+PrintGC 輸出GC日志 ‐XX:+PrintGCDetails 輸出GC的詳細日志 ‐XX:+PrintGCTimeStamps 輸出GC的時間戳(以基準時間的形式) ‐XX:+PrintGCDateStamps 輸出GC的時間戳(以日期的形式,如 2013‐05‐04T21:53:59.234+0800) ‐XX:+PrintHeapAtGC 在進行GC的前后打印出堆的信息 ‐Xloggc:F://test//gc.log 日志文件的輸出路徑

3)、啟動程序,GC日志信息解讀

1590304335832

G1垃圾收集器 vs CMS垃圾收集器

  • G1不會產生碎片
  • G1可以精準控制停頓,它把整堆劃分為多個固定大小的區(qū)域,每次根據(jù)停頓時間去收集垃圾最多的區(qū)域

G1垃圾收集器優(yōu)化建議

  • 年輕代大小

    • 避免使用 -Xmn 選項或 -XX:NewRatio 等其他相關選項顯式設置年輕代大小
    • 固定年輕代的大小會覆蓋暫停時間目標
  • 暫停時間目標不要太過嚴苛

    • G1 GC 的吞吐量目標是 90% 的應用程序時間和 10%的垃圾回收時間
    • 評估 G1 GC 的吞吐量時,暫停時間目標不要太嚴苛。目標太過嚴苛表示您愿意承受更多的垃圾回收開銷,而這會直接影響到吞吐量

五、可視化GC日志分析工具

1、GC日志輸出參數(shù)

前面通過-XX:+PrintGCDetails可以對GC日志進行打印,我們就可以在控制臺查看,這樣雖然可以查看GC的信息,但是并不直觀,可以借助于第三方的GC日志分析工具進行查看。 在日志打印輸出涉及到的參數(shù)如下:

‐XX:+PrintGC 輸出GC日志
‐XX:+PrintGCDetails 輸出GC的詳細日志
‐XX:+PrintGCTimeStamps 輸出GC的時間戳(以基準時間的形式)
‐XX:+PrintGCDateStamps 輸出GC的時間戳(以日期的形式,如 2013‐05‐
04T21:53:59.234+0800)
‐XX:+PrintHeapAtGC 在進行GC的前后打印出堆的信息
‐Xloggc:../logs/gc.log 日志文件的輸出路徑

測試:

‐XX:+UseG1GC ‐XX:MaxGCPauseMillis=100 ‐Xmx256m ‐XX:+PrintGCDetails ‐
XX:+PrintGCTimeStamps ‐XX:+PrintGCDateStamps ‐XX:+PrintHeapAtGC ‐
Xloggc:F://test//gc.log

最后生成gc.log,我們利用下面的可視化工具進行分析。

[](https://smartan123.github.io/book/?file=001-%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/002-%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/0024-JVM%E8%B0%83%E4%BC%98-GC%E7%AF%87#2、GC Easy可視化工具)2、GC Easy可視化工具

GC Easy是一款在線的可視化工具,易用、功能強大, GCEasy官網(wǎng)地址:http://gceasy.io/

打開官網(wǎng)上傳gc.log,點擊分析即可。分析完之后它會給我們出相關的分析報告,那查看指標如何解讀呢?

具體解讀見視頻

GC Easy查看gc報告

1590304703407
1590304748166
1590304759459
1590304768991
1590304777129

分析報告解讀請觀看視頻

六、本章小結

1590304790493
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
禁止轉載,如需轉載請通過簡信或評論聯(lián)系作者。

友情鏈接更多精彩內容