前言
在一個風和日麗的中午,和同事小勇一起走在公司樓下的小公園里面,看到很多的小姐姐,心想什么時候能夠和這些小姐姐一起討論人生呀,美滋滋,嘿嘿嘿,
收起你的哈喇子好不好,小勇總是在這個時候發出聲音,挺讓人喜(fu)歡(ck)的,
小勇:小農,現在不是推崇垃圾分類嗎,你說到底什么是垃圾?小勇總是在我和他散步的時候,問這么讓人深思的問題!
我:什么是垃圾啊,你不就是垃圾嗎?
小勇:去你大爺的,正經的,
我:小勇啊,答應我以后散步的時候我們討論點輕松點的問題好嘛?垃圾是啥,垃圾就是沒有參考的物件就是垃圾啊
小勇:,,,,,我們還是去午休吧
我:別啊,都講到這里了,給你普及一下,你難道不想以后你的簡歷上出現——熟悉GC常用演算法,熟悉常見垃圾收集器,具有實際JVM調優實戰經驗嗎?保證讓你豁然開朗,等你以后去面試的時候,給面試官講這些保證妥妥的,
小勇:你這么說我倒是有點興趣,但是如果講不明白,那你就浪費了我時間了,晚飯就你請吧,
我是沒問題,但是我的三個粉絲不會答應你的
小勇:你沒問題就行了,請開始你的表演吧~
什么是垃圾
什么是垃圾,就是沒有任何參考指向的一個物件或者多個物件(回圈參考),但是他們卻依然占據著記憶體空間,
GC是一種自動的存盤管理機制,當一些被占用的記憶體不再需要時,就應該予以釋放,這種存盤資源管理,稱為垃圾回收,
就像我們的衣柜一樣,我們里面可能存放這很多衣服,有可能幾個月或者幾年都不會穿過一次,但是這些我們不穿的衣服一直霸占著我們的衣柜(記憶體),我們把這些不會穿的衣服扔掉的或者捐贈出去,這樣我們就可以放更多可以穿的衣服,這個就類似于“垃圾回收”,
在GC里面,只分為可回收和不可回收,如下圖所示:

1.1 Java 和 C++ 垃圾回收的區別
Java是你只管扔垃圾就可以,Java會自動幫你處理,而C++要手動處理,但是容易造成一個問題就是忘記回識訓者回收多次
-
java
- GC處理垃圾
- 開發效率高,執行效率低
-
C++
- 手工處理垃圾
- 忘記回收,會導致記憶體泄漏
- 回收多次,非法訪問
- 開發效率地,執行效率高
怎么找垃圾?
上面我們知道了什么是垃圾,那么我們如何去找到垃圾呢?
在堆里面存放這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)- Parallel Scavenge 15次進入老年代
- CMS 6次進入老年代
- 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…
我是牧小農,怕什么真理無窮,進一步有進一步的歡喜,大家加油!
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/272282.html
標籤:其他
