【死磕JVM】給同事講了一遍GC后,他要去面試,年輕人,就是容易沖動!


前言

在一個風和日麗的中午,和同事小勇一起走在公司樓下的小公園里面,看到很多的小姐姐,心想什么時候能夠和這些小姐姐一起討論人生呀,美滋滋,嘿嘿嘿。

收起你的哈喇子好不好,小勇總是在這個時候發出聲音,挺讓人喜(fu)歡(ck)的。
小勇:小農,現在不是推崇垃圾分類嗎,你說到底什么是垃圾?小勇總是在我和他散步的時候,問這么讓人深思的問題!
我:什么是垃圾啊,你不就是垃圾嗎?
小勇:去你大爺的,正經的。
我:小勇啊,答應我以后散步的時候我們討論點輕松點的問題好嘛?垃圾是啥,垃圾就是沒有引用的對象就是垃圾啊
小勇:。。。。,我們還是去午休吧

我:別啊,都講到這里了,給你普及一下,你難道不想以后你的簡歷上出現——熟悉GC常用算法,熟悉常見垃圾收集器,具有實際JVM調優實戰經驗嗎?保證讓你豁然開朗,等你以后去面試的時候,給面試官講這些保證妥妥的。

小勇:你這么說我倒是有點興趣,但是如果講不明白,那你就浪費了我時間了,晚飯就你請吧。
我是沒問題,但是我的三個粉絲不會答應你的
小勇:你沒問題就行了,請開始你的表演吧~

什么是垃圾

什么是垃圾,就是沒有任何引用指向的一個對象或者多個對象(循環引用),但是他們卻依然占據着內存空間。

GC是一種自動的存儲管理機制。當一些被占用的內存不再需要時,就應該予以釋放。這種存儲資源管理,稱為垃圾回收。

就像我們的衣櫃一樣,我們里面可能存放這很多衣服,有可能幾個月或者幾年都不會穿過一次,但是這些我們不穿的衣服一直霸占着我們的衣櫃(內存),我們把這些不會穿的衣服扔掉的或者捐贈出去,這樣我們就可以放更多可以穿的衣服,這個就類似於“垃圾回收”。

在GC里面,只分為可回收和不可回收,如下圖所示:
在這里插入圖片描述

1.1 Java 和 C++ 垃圾回收的區別

Java是你只管扔垃圾就可以,Java會自動幫你處理,而C++要手動處理,但是容易造成一個問題就是忘記回收或者回收多次

  • java

    1. GC處理垃圾
    2. 開發效率高,執行效率低
  • C++

    1. 手工處理垃圾
    2. 忘記回收,會導致內存泄漏
    3. 回收多次,非法訪問
    4. 開發效率地,執行效率高

怎么找垃圾?

上面我們知道了什么是垃圾,那么我們如何去找到垃圾呢?

在堆里面存放這Java中幾乎所有的對象實例,垃圾收集器在對堆進行回收前,首先要做的事情就是確定這些對象哪些還 “存活”,哪些是需要進行回收的(即不再被引用的對象)

找到垃圾有兩種算法

  • reference count (引用計數算法)
  • Root Searching (根可達算法)

1. 引用計數法

會給對象中添加一個引用計數器,每當有一個地方引用它的時候,計數器的值就 +1 ,當引用失效時,計數器值就 -1 ,計數器的值為 0 的對象不可能在被使用,這個時候就可以判定這個對象是垃圾。

在這里插入圖片描述

當圖中的數值變成0時,這個時候使用引用計數算法就可以判定它是垃圾了,但是引用計數法不能解決一個問題,就是當對象是循環引用的時候,計數器值都不為0,這個時候引用計數器無法通知GC收集器來回收他們,如下圖所示:

在這里插入圖片描述
這個時候就需要使用到我們的根可達算法

2. 根可達算法

根可達算法的意思是說從根上開始搜索,當一個程序啟動后,馬上需要的那些個對象就叫做根對象,所謂的根可達算法就是首先找到根對象,然后跟着這根線一直往外找到那些有用的,例如我們Java程序 main() 方法運行,一個main() 方法會啟動一個線程。

線程棧變量: 線程里面會有線程棧和main棧幀,從這個main() 里面開始的這些對象都是我們的根對象。

靜態變量: 一個class 它有一個靜態的變量,load到內存之后馬上就得對靜態變量進行初始化,所以靜態變量到的對象這個叫做根對象。

常量池: 如果你這個class會用到其他的class的那些個類的對象,這些就是根對象。

JNI: 如果我們調用了 C和C++ 寫的那些本地方法所用到的那些個類或者對象

在這里插入圖片描述

圖中的 object5 和object6 雖然他們之間互相引用了,但是從根找不到它,所以就是垃圾,而object8沒有任何引用自然而然也是垃圾,其他的Object對象都有可以從根找到的,所以是有用的,不會被垃圾回收掉。

3. 區別

算法 思想 優點 缺點
引用計數法 給對象添加一個引用計數器,每當一個地方引用這個對象的時候,計數器值就+1;當引用失效時,計數器值-1 判定效率高 不能解決對象之間相互引用的情況,開銷比較大,頻繁且大量的引用變化,帶來大量的額外運算
可達性分析 通過一系列稱為 "GC Roots" 的對象作為起始點,從這些節點向下搜索,當GC Roots到某個對象不可達時,這個對象就是可回收的 更加精確和嚴謹,可以分析出循環數據結構相互引用的情況 實現比較復雜,需要分析大量的數據,消耗大量時間

如何清理垃圾

我們找到對應的垃圾之后,我們如果去清理垃圾呢?GC常用的算法有三種:

  • Mark-Sweep(標記清除)
  • Copying(拷貝)
  • Mark-Compact(標記壓縮)

1. 標記 - 清除算法

就和它的名字一樣 ,算法分為 “標記” 和 “清除” 兩個階段,首先標記出所有需要回收的對象,在標記完成后統一回收所有被標記的對象,這個是最基礎的收集算法,為什么說它是最基礎的,因為后續的收集器都是基礎這種思路並對其不足進行改進而得到的。

在這里插入圖片描述
標記清除算法它有自己的小問題,大家可以看到上面這張圖,我們從GC的根找到那些不可回收的,綠色是不可回收的,紫色是可以回收的,我們把它回收之后就變成空閑的了,這種算法相對比較簡單,在存活對象比較多的情況下效率比較高,它需要經歷兩次掃描,第一遍掃描是找到那些有用的,第二遍掃描是把那些沒用的找出來清理掉,這里會有兩個問題:一個是效率問題,標記和清除兩個過程的效率都不高,另一個是空間問題,標記清除之后會產生大量不連續的空間碎片,如果空間碎片太多會導致以后的程序在運行過程中需要分配較大對象的時候,無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集動作。

2. 復制算法

為了解決效率的問題,所以有了復制(Copying)算法的出現,它將可用的內存按容量划分為大小相等的兩塊,每次只使用其中的一塊,當這一塊的內存用完了,就將還存活着的對象賦值到另一塊上面,然后再把已使用過的內存空間一次清理掉,這樣使得每次都對整個半區進行內存回收,內存分配時也就不用考慮內存碎片等復雜情況,只需要移動堆頂的指針,這種適用於存活對象較少的情況,所以比較適合eden區,只掃描一次,效率提高了沒有碎片,但是會造成空間的浪費,將內存縮小為原來的一半,未免太高了一點,而且移動復制對象,需要調整對象的引用

在這里插入圖片描述

3. 標記 壓縮算法

標記壓縮就是把所有的東西整理的過程,清理的過程同時壓縮到頭上去。回收之前,有用的往前面走,將剩下的清理出來,但是標記壓縮算法依然有它的問題,我們都是通過GC Roots 找到那些不可回收的對象,然后把不可回收的往前挪,這個時候我們需要掃描兩次而且需要移動對象,第一遍掃描出有用的對象,第二遍進行移動,而且移動如果是多線程還需要進行同步,所以這個效率會低很多,但是它不會產生碎片,分配對象也不會產生內存減半。

在這里插入圖片描述

4. 總結

  • Mark-Sweep(標記清除): 標記為垃圾之后就給清理掉,別的空間還是固定的,效率還可以,就是容易產生碎片
  • Copying(拷貝): 將內存一分為二,只使用一半,如果垃圾太多了,拷貝有用的到另外一邊,剩下的清理就直接整個內存進行清理,效率比較高
  • Mark-Compact(標記壓縮): 將所有的對象湊在一起,把垃圾全部清理走,接下來剩下的這個空間還是連續的,在里面分配任何內容的時候直接往里面分配就行了

堆內存邏輯分區

JVM中的Hot Spot 用的是分代算法

在這里插入圖片描述
新生代分為:eden、survivor

eden(伊甸): 默認比例8:是我們剛剛新 new出來對象之后往里扔的那塊區域
survivor: 默認比例是1:是回收一次之后跑到這個區域,這里面由於裝的對象不同,所以采取的算法也不同

由於新生代存活對象特別少,死去對象特別多所以使用的算法是 復制算法

old 老年代:tenured(終身)
老年代活着的對象特別多適用於:標記清除和標記壓縮算法

一個對象從出生到消亡

在這里插入圖片描述
一個對象產生之后首先進行棧上分配,棧上如果分配不下會進入伊甸區,伊甸區經過一次垃圾回收之后進入surivivor區,survivor區在經過一次垃圾回收之后又進入另外一個survivor,與此同時伊甸區的某些對象也跟着進入另外一個survivot,什么時候年齡夠了就會進入old區,這是整個對象的一個邏輯上的移動過程。

那什么時候會在棧上分配,什么時候會在伊甸區分配?

1 棧上分配

棧上分配:

  • 線程私有小對象:小對象、線程私有的
  • 無逃逸:就在某一段代碼中使用,除了這段代碼就沒有人認識它了
  • 支持標量替換:意思是用普通的屬性、把普通的類型代替對象就叫標量替換

棧上分配會比在堆上分配快一點,如果在棧上分配不下,會優先進行本地分配,也就是 線程本地分配TLAB(Thread local Allocation Buffer): 在伊甸區很多線程都會往里面分配對象,但是分配對象的時候我們一定會進行空間的征用,誰搶到算誰的,多線程的同步,效率就會降低,所以設計了TLAB機制

  • 占用eden,默認為1%,在伊甸區取用百分之一的空間,這塊空間叫做線程獨有,分配對象的時候首先往線程獨有的這塊空間進行分配
  • 多線程的時候不用競爭eden就可以申請空間,提高效率

2 老年代

對象什么時候進入老年代?

回收了多少次進入老年代?

  • 超過 XX:MaxTenuringThreshold指定次數(YGC)
    1. Parallel Scavenge 15次進入老年代
    2. CMS 6次進入老年代
    3. G1 15次進入老年代

網上有說可以次數往上調大,這個是不可能的

動態年齡判斷

為了能夠適用不同程序的內存狀況,虛擬機並不是永遠的要求對象的年齡必須達到了MaxTenuringThreshold才能晉升老年代,如果在Surivivor空間中相同年齡所有對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象就可以直接進入老年代,無需等到MaxTenuringThreshold中要求的年齡。

兩個Survivor之間拷貝來拷貝去只要超過百分之50的時候把年齡最大的直接放入到old區,也就是不一定非得到15歲。

在s1里面有這么多對象拷貝到了s2里面超過百分之50的話,s1里面在加上伊甸區里面,整個一個對象一下子拷貝到s2里面,經過一次垃圾回收,過去之后,這個時候整個加起來對象已經超過s2的一半了,這里面年齡最大的一些對象直接進入老年區,這個就叫做動態年輕判斷

在這里插入圖片描述

大對象直接進入老年代 ,所謂的大對象是指,需要連續大量內存空間的Java對象,最典型的大對象就是那種很長的字符串以及數組,經常出現大對象容易導致內存還有不少空間的時候就提前觸發了垃圾收集來獲得足夠的連續內存空間

在這里插入圖片描述
start 先是new一個對象,然后在棧上進行分配,如果在棧上能夠分配,就分配到棧上,棧直接彈出,彈出結束,如果在棧上分配不下,判斷對象是否為大對象,如果是大對象,直接進入老年代,FGC后結束如果不是,進入線程本地分配(TLAB),不管怎么樣都會到伊甸區進行GC清除,如果清除完畢,直接結束,如果沒有清除完畢,進入S1,S1繼續GC清除,如果年齡到了進入old區,如果年齡不夠進入S2,然后S2再繼續GC的清除,要么年齡到了,要么動態年齡達到

MinorGC/YGC: 年輕代空間耗盡時觸發
MajorGC/FullGC: 在老年代無法繼續分配空間時觸發,新生代老年代同時進行回收

常見的垃圾回收器

新生代收集器: Serial、ParNew、Parallel Scavenge

老年代收集器: Serial Old、CMS、Parallel Old

新生代和老年代收集器: G1、ZGC、Shenandoah

每種垃圾回收器之間不是獨立操作的,下圖表示垃圾回收器之間有連線表示,可以協作使用:
在這里插入圖片描述

新生代垃圾收集器

1. Serial收集器

Serial 收集器是最基礎、歷史最悠久的收集器,是一個單線程工作的收集器,它的“單線程”的意義不是說他只會使用一個處理器或者一條收集線程去完成垃圾收集的工作,更重要的是強調在它進行垃圾收集的時候,會暫停其他所有工作線程,直到它收集結束

在這里插入圖片描述

根據上圖中我們可以知道,當Serial收集器運行的時候,會暫停所有線程,“Stop The World” ,等到GC完成后,應用線程繼續執行,就類似於 你有三個女朋友,他們同時讓你陪他們去逛街,你只能陪完其中一個才能去陪另外一個,陪其中一個的時候,其他女朋友就要等待,但是垃圾收集這項工作要比這種情況要復雜的多!

優勢: 因為使用的是單線程的方式,所以對於單個CPU來說,是其他類型收集器中效率最高的一個
缺點: 在用戶不可知、不可控的情況下,暫停所有線程,風險性和體驗感不好,讓人比較難接受

使用命令:可以開啟Serial 作為新生代收集器

-XX:+UserSerialGC #選擇Serial作為新生代垃圾收集器

2. ParNew收集器

ParNew收集器實質上是Serial收集器的多線程並行版本,除了同時使用多條線程進行垃圾收集器之外,其余的比如Serial收集器可用的控制參數、收集算法、Stop The Wrold 、對象分配規則等等都和Serial收集器完全一樣,在多核機器上,默認開啟的手機線程數和CPU數量一樣,但是可以通過參數進行修改

-XX:ParallelGCThreads #設置JVM垃圾收集的線程數

在這里插入圖片描述
ParNew收集器除了支持多線程並行收集之外,其他與Serial收集器相比並沒有太多創新之處,但它 卻是不少運行在服務端模式下的HotSpot虛擬機,尤其是JDK 7之前的遺留系統中首選的新生代收集 器,其中有一個與功能、性能無關但其實很重要的原因是:除了Serial收集器外,目前只有它能與CMS 收集器配合工作。

優點:隨着CPU的有效利用,對於GC時系統資源的有效利用有好處
缺點:同Serial一樣的毛病
使用場景:ParNew是許多運行在Server模式下的虛擬機中首選的新生代收集器,因為CMS只能與Serial 或者 ParNew 配合使用,在如今的多核環境下,首選的是多線程並行的ParNew,ParNew收集器是激活CMS后(使用-XX:+UseConcMarkSweepGC選項)的默認新生代收集器,也可以使用-XX:+/-UseParNewGC選項來強制指定或者禁用它

3. Parallel Scavenge收集器

Parallel Scavenge收集器也是一款新生代的收集器,它同樣是基於標記-復制算法那實現的收集器,也是能夠並行收集器的多線程收集器,Parallel Scavenge收集器關注點與其他收集器的不用處在於,CMS等收集器的關注點是盡可能地縮短垃圾收集時用戶線程的停頓時間,而Parallel Scavenge收集器的目標則是一個可控制的吞吐量,所謂的吞吐量就是處理器用於運行用戶代碼的時間與處理器總消耗的比值,如下圖所示:

在這里插入圖片描述
如果說虛擬機完成某個任務,用戶代碼加上垃圾收集總共耗費了100分鍾,其中垃圾收集花掉1分鍾,那么吞吐量就是99%。停頓時間越短就越適合需要與用戶交互或者需要保證服務響應質量的程序,良好的響應速度能提升用戶體驗。

垃圾收集器每100秒收集一次,每次停頓10秒,和垃圾收集器每50秒收集一次,每次停頓時間7秒,雖然后者停頓時間變短了,但是總體吞吐量變低了,CPU總體利用率變低了。

收集頻率 每次停頓時間 吞吐量
100秒收集一次 10秒 91%
每50秒收集一次 7秒 88%

可以通過 -XX:MaxGCPauseMillis來設置收集器盡可能在多長時間內完成內存回收,可以通過 -XX:GCTimeRatio來精確控制吞吐量。

如下是 Parallel 收集器和 Parallel Old 收集器結合進行垃圾收集的示意圖,在新生代,當用戶線程都執行到安全點時,所有線程暫停執行,ParNew 收集器以多線程,采用復制算法進行垃圾收集工作,收集完之后,用戶線程繼續開始執行;在老年代,當用戶線程都執行到安全點時,所有線程暫停執行,Parallel Old 收集器以多線程,采用標記整理算法進行垃圾收集工作。
在這里插入圖片描述

老年代垃圾收集器

1. Serial Old 收集器

Serial Old 是 Serial收集器的老年代版本,它同樣是一個單線程收集器,使用標記-整理算法,這個收集器的主要意義也是供客戶端模式下HotSpot虛擬機使用。如果在服務端一種是與Parallel Scavenge收集器搭配使用,另外一種是作為CMS 收集器發生失敗時的后備預案。

Serial收集器與Serial Old收集器的運行示意圖:

在這里插入圖片描述
適用場景: Client模式;單核服務器;與Parallel Scavenge收集器搭配;作為CMS收集器的后備方案,在並發收集發生Concurrent Mode Failure時使用

2. Parallel Old收集器

Parallel Old 是 Parallel Scavenge收集器的老年代版本,支持多線程並發收集,基於標記-整理算法實現,可以充分利用多核CPU的計算能力,慮Parallel Scavenge/Parallel Old收集器運行示意圖:

在這里插入圖片描述

2. CMS收集器

CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器。CMS收集器是基於標記-清楚算法實現的,這個收集器的運作過程比前面的幾個收集器更復雜一點,整個過程分為四個步驟:

1) 初始標記(CMS initial mark): 只是標記 GC Roots能夠直接關聯到的對象,速度很快

2) 並發標記(CMS concurrent mark): 從GC Roots 的直接關聯對象開始遍歷整個對象圖的過程,這個過程耗時較長但是不需要停頓用戶線程,可以和垃圾收集線程一起並發運行

3) 重新標記(CMS remark): 修正並發標記期間,因用戶程序繼續運作導致標記產生對象的標記記錄,這個階段的停頓時間會比初始標記階段稍長一些

4) 並發清除(CMS concurrent sweep): 清理刪除掉標記階段判斷的已經死亡的對象,由於不需要移動存活對象,這個階段也是可以與用戶線程同時並發的。

其中 初始標記、重新標記這兩個步驟仍然需要 “Stop The World” 暫停所有用戶線程,由於在整個過程中耗時最長的並發標記和並發清理階段中,垃圾收集器線程都可以與用戶線程一起工作,總體來說,CMS收集器的內存回收過程是和用戶線程一起並發執行的,如下圖所示:

在這里插入圖片描述
優點: CMS收集器是一款優秀的收集器,它主要體現為:並發收集、低停頓。

缺點:

CMS收集器對處理器資源非常敏感,在並發階段,雖然不會導致用戶線程停頓,但也會因為占用一部分線程導致應用程序變慢,降級總的吞吐量。CMS默認啟動回收線程數是(處理器核心數量+3)/4,也就是說如果處理器核心數大於等於四個,並發回收時垃圾收集線程只占用不超過25%的處理器運算資源,處理器資源會隨着CPU數量的增加而下降,但是當CPU數量不足四個的時候,CMS對用戶程序的影響就可能變的很大。
CMS收集器無法處理 “浮動垃圾” ,有可能出現 “Concurrent Mode Failure” 失敗進而導致另一次完全“Stop The World” 的Full GC 的產生,在CMS的並發標記和並發清理階段,用戶線程是還在繼續進行的,程序在運行自然就還會伴隨有新的垃圾對象不斷產生,但這一部分垃圾對象是出現在標記過程結束以后,CMS無法在當次收集中處理掉它們,只好留待下一次垃圾收集時再清理掉。這一部分的垃圾就稱為“浮動垃圾”
因為CMS是一款基於 “標記-清除”算法實現的收集器,因此收集結束時會有大量的空間碎片產生,空間碎片過多的時,將對給大對象帶來很大的麻煩,有可能不得不提前進行Full GC的操作,不過通過參數:-XX:+UseCMS-CompactAtFullCollection進行優化

新生代和老年代垃圾收集器

G1收集器

Garbage First (簡稱G1)收集器是垃圾收集器技術發展歷史上的里程碑式的成果,它開創了收集器面向局部收集的設計思路和基於Region的內存布局形式。

G1收集器是一款面向服務器端應用的垃圾收集器,在JDK9發布的時候成為服務端模式下的默認垃圾收集器,而CMS則淪為不被推薦使用的收集器

特點:

在G1收集器出現之前所有的其他收集器,目標范圍要么是新生代要么是老年代,要么就是Java堆,但是G1做了全面性,它可以面向堆內存任何部分來組成回收集進行回收,衡量標准不再是它屬於哪個分代,而是那塊內存中存放的垃圾數量最多,回收收益最大,這就是G1收集器的Mixed GC模式。而G1開創的基於Region的堆內存布局是它能夠實現這個目標的關鍵。

雖然G1仍然保留了新生代和老年代的概念,但新生代和老年代不再是固定的,他們都是一系列區域的動態集合,G1可以建立可預測的停頓時間模型,是因為它將Region作為單次回收最小單元

G1不再堅持固定大小以及固定數量的分代區域划分,而是把連續的Java堆划分為多個大小相等的獨立區域,每一個Region都可以根據需要,扮演新生代的Eden空間、Survivor空間或者老年代空間,收集器能夠對扮演不同的角色的Region采用不同的策略去處理。

Region中海油一類特殊的Humongous區域,專門用來存儲大對象。G1認為只要大小超過一個Region容量一半的對象即可判定為大對象。

G1收集器的運行過程:

  • 初始標記(Initial Marking): 標記GC Roots 能直接關聯到的對象,並且修改TAMS指針的值,讓下一階段用戶線程並發運行時,能正確在可用的Region中分配新對象,需要耗時較短的停頓線程,但是是借用Minor GC的時候同步完成的,所以在這個階段實際沒有額外的停頓

  • 並發標記(Concurrent Marking): 從GC Roots 開始對堆中對象進行可達性分析,遞歸掃描整個堆里面的對象圖,找出要回收的對象,這個階段耗時較長,但可以和用戶程序並發執行。

  • 最終標記(Final Marking): 對用戶線程做另一個短暫的暫停,用戶處理並發階段結束后仍遺留下來的最后那少量的SATB記錄

  • 篩選回收(Live Data Counting and Evacuation): 負責更新Region的統計數據,對各個Region的回收價值和成本進行排序,根據用戶鎖期望的停頓時間來制定回收計划,可以只有選擇任意多個Region構成回收集,然后把決定回收的那一部分Region的存活對象賦值到空的Region中,再清理整個Region的全部空間。

在這里插入圖片描述

總結

小勇你懂了嗎?小勇小勇,你別睡着了啊,我還沒講完呢!小勇醒醒啊!!!
小勇迷迷糊糊的說:怎么了,下班了嗎?
。。。。,下班啥,我講的GC你聽懂了嗎?
小勇:聽懂了,我明天就去面試,你講的太棒了!
。。。。。敷衍,算了我已經把東西都放在筆記里面了,你要是感興趣就可以來看看,今天就到這里了,我們上去吧

end....

我是牧小農,怕什么真理無窮,進一步有進一步的歡喜,大家加油!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM