Java垃圾收集器與內(nèi)存分配策略
- 程序計(jì)數(shù)器,虛擬機(jī)棧,本地方法棧3個(gè)區(qū)域的內(nèi)存隨線程而生,隨線程而滅,每一個(gè)棧楨分配多少內(nèi)存在類結(jié)構(gòu)確定下來時(shí)就已知的,因此具備確定性,不需要過多考慮回收的問題,方法/線程結(jié)束時(shí)內(nèi)存自然就跟著回收了
- Java堆,方法區(qū)一個(gè)接口多個(gè)實(shí)現(xiàn)類需要的內(nèi)存不一樣,一個(gè)方法的多個(gè)分支需要的內(nèi)存也不一樣.運(yùn)行時(shí)才知道會(huì)創(chuàng)建哪些對(duì)象,這部分內(nèi)存的分配和回收也是動(dòng)態(tài)的颖变。垃圾收集器所關(guān)注的是這部分內(nèi)存
對(duì)象已死嗎
<font color=red>垃圾收集器在對(duì)堆回收前,要確定哪些對(duì)象還存活,哪些對(duì)象已死去</font>
引用計(jì)數(shù)法
- 引用計(jì)數(shù)法
- 缺點(diǎn): 很難解決對(duì)象之間相互循環(huán)引用的問題,所以主流JVM沒有選用該方法管理內(nèi)存
可達(dá)性分析算法
- 基本思想: 通過一系列"GC Roots"的對(duì)象作為起點(diǎn),開始向下搜索,走過的路徑稱為引用鏈(Reference Chain),當(dāng)一個(gè)對(duì)象不可達(dá)時(shí),則證明該對(duì)象不可用
- 可達(dá)性分析算法
- 可作為GC Roots的對(duì)象包括
- 虛擬機(jī)棧(棧幀中的本地變量表)中引用的對(duì)象
- 本地方法棧中Native方法引用的對(duì)象
- 方法區(qū)中類靜態(tài)屬性引用的對(duì)象
- 方法區(qū)中常量引用的對(duì)象
- 對(duì)象的finallize()方法只會(huì)被系統(tǒng)自動(dòng)調(diào)用一次,下次回收時(shí)不會(huì)調(diào)用了鸵钝。該方法由JVM自動(dòng)建立的,低優(yōu)先級(jí)Finalizer線程執(zhí)行,但并不承諾會(huì)等待它運(yùn)行結(jié)束,以防止finallize存在死循環(huán)導(dǎo)致F-Queue隊(duì)列中的其它對(duì)象永久等待.
- finallize()運(yùn)行代價(jià)高昂,不確定性大,不鼓勵(lì)使用
- 枚舉根節(jié)點(diǎn)(GC Roots)
- 問題點(diǎn): 消耗很多時(shí)間
- 很多應(yīng)用方法區(qū)就有數(shù)百M(fèi),逐個(gè)檢查耗時(shí)
- GC停頓:
GC時(shí)必需停頓所有Java執(zhí)行線程(Stop the World), 防止分析過程中對(duì)象引用關(guān)系還在不斷變化
- 解決方案: 主流JVM(如HotSpot)使用準(zhǔn)確式GC,HotSpot使用一組OopMap的數(shù)據(jù)結(jié)構(gòu).
- 類加載完成時(shí): 把對(duì)象內(nèi)什么偏移量上是什么類型的數(shù)據(jù)計(jì)算出來
- JIT編譯時(shí): 在<font color=red>特定的位置(安全點(diǎn))</font>記錄下棧和寄存器中哪些位置是引用
- 如果為每條指令都生成OopMap,GC的空間成本會(huì)變得很高,
另外OopMap可能會(huì)導(dǎo)致引用關(guān)系變化,而GC時(shí)需要引用關(guān)系不能變化
解決方案是在<font color=red>特定的位置設(shè)置安全點(diǎn),和安全區(qū)域</font>,只有在安全點(diǎn)才能GC停頓. -
安全點(diǎn)的選定標(biāo)準(zhǔn):
是否具有讓程序長(zhǎng)時(shí)間執(zhí)行的特征
.即指令序列復(fù)用,例如:方法調(diào)用,循環(huán)跳轉(zhuǎn),異常跳轉(zhuǎn)等 - GC時(shí)如何讓所有線程都跑到最近的安全點(diǎn)上再停頓下來?
- 方案一: 搶占式中斷(已淘汰),GC時(shí)先把所有線程全部中斷, 如發(fā)現(xiàn)有線程中斷的地方不在安全點(diǎn)上,就恢復(fù)線程,讓它跑在安全點(diǎn)上
- 方案二:主動(dòng)式中斷, GC需要中斷線程時(shí),不直接對(duì)線程操作,僅僅簡(jiǎn)單設(shè)置一個(gè)標(biāo)志,各個(gè)線程執(zhí)行時(shí)主動(dòng)去輪詢(安全點(diǎn)+創(chuàng)建對(duì)象需要分配內(nèi)存的地方進(jìn)行輪詢)這個(gè)標(biāo)志,發(fā)現(xiàn)中斷標(biāo)志為true時(shí),自己就中斷掛起偶惠。
- 主動(dòng)式中斷
- 如果程序不執(zhí)行(Sleep,Blocked),則無法進(jìn)入安全點(diǎn)進(jìn)行GC停頓,這時(shí)候需要
安全區(qū)域
來解決. 安全區(qū)域是指在一段代碼片段中,引用關(guān)系不會(huì)發(fā)生變化.- 線程執(zhí)行到安全區(qū)域中的代碼時(shí), 標(biāo)記自己已進(jìn)入了Safe Region.
- JVM GC時(shí)不管標(biāo)記自己為Safe Region的線程
- 線程離開Safe Region時(shí),檢查系統(tǒng)是否已完成GC,如果沒有,則等待直到收到可以安全離開Safe Region的信號(hào)為止
- 安全區(qū)域
- 問題點(diǎn): 消耗很多時(shí)間
引用
引用 | 說明 | 舉例 |
---|---|---|
強(qiáng)引用 | 垃圾收集器<font color=red>永遠(yuǎn)不會(huì)回收</font>強(qiáng)引用的對(duì)象 | Object obj = new Object() |
軟引用 | <font color=red>OutOfMemory異常前</font>回收 | |
弱引用 | <font color=red>下次垃圾收集發(fā)生前</font>回收 | |
虛引用 | 唯一目的是對(duì)象被收集器回收時(shí)收到一個(gè)系統(tǒng)通知. 不會(huì)對(duì)對(duì)象生存時(shí)間構(gòu)成影響, 也無法通過虛引用獲得對(duì)象的實(shí)例 |
- |
垃圾收集算法
標(biāo)記-清除算法
- 兩個(gè)階段
- 標(biāo)記: 先按可達(dá)性算法標(biāo)記出所有需要回收的對(duì)象
- 回收: 標(biāo)記完成后統(tǒng)一回收,直接對(duì)可回收對(duì)象進(jìn)行清理
- 缺點(diǎn)
- 標(biāo)記回收兩個(gè)過程效率都不高
- 會(huì)產(chǎn)生大量不連續(xù)的內(nèi)存碎片
- 標(biāo)記-清除算法
標(biāo)記-整理算法
- Mark-Compact
- 用于<font color=red>回收老年代</font>
- | 標(biāo)記過程 | 回收過程 |
---|---|---|
標(biāo)記-清除算法 | 先按可達(dá)性算法標(biāo)記出所有需要回收的對(duì)象 | 直接對(duì)可回收對(duì)象進(jìn)行清理 |
標(biāo)記-整理算法 | 同上 | 讓所有存活的對(duì)象都向一端移動(dòng),然后直接清理掉端邊界以外的內(nèi)存 |
- 標(biāo)記整理算法
復(fù)制算法
- 將可用內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中一塊.當(dāng)一塊內(nèi)存用完,將存活對(duì)象復(fù)制到另一塊上,然后再把已使用過的內(nèi)存一次清理掉.
- 復(fù)制算法
- 解決了效率問題
- 用于<font color=red>回收新生代</font>
- 代價(jià)是內(nèi)存縮小了一半
- IBM研究表明新生代中98%的對(duì)象是朝生夕死的,所以不需要按1:1比例劃分內(nèi)存空間,而是將內(nèi)存劃分為一塊較大的Eden空間和2塊較小的Survivor空間,每次使用Eden和Survior-1.回收時(shí)將Eden和Survior-1復(fù)制到Survior-2,如果Survior-2空間不夠,則使用擔(dān)保內(nèi)存(要還的).
分代收集算法
垃圾收集器
- HotSpot JVM的垃圾收集器
- 發(fā)展歷程: Serial->Parallel->CMS->G1
注意:沒有萬能的收集器,只有最合適的
Serial 收集器
- 新生代收集器
- 單線程:進(jìn)行垃圾收集時(shí),必需暫停其它所有的工作線程(Stop The World).
即便是更加先進(jìn)的收集器,也只能不斷縮短用戶線程停頓時(shí)間,而不能完全消除
- 優(yōu)點(diǎn): 簡(jiǎn)單高效,沒有線程交互的干擾,專心做垃圾手機(jī)
- 適合運(yùn)行Client模式下的虛擬機(jī)(桌面應(yīng)用場(chǎng)景下分給JVM管理的內(nèi)存不大,停頓時(shí)間幾十ms-100多ms腕柜,可以接受)
- Serial+Serial Old收集器
ParNew 收集器
- 新生代收集器
- Serial收集器的多線程版本
- Server模式下JVM的首選新生代收集器(除了Serial外,只有它能與CMS配合工作)
- 多CPU下效果較好,1-2個(gè)CPU效果不一定比Serial好
- ParNew+Serial Old收集器
Parallel Scavenge 收集器
- 與ParNew一樣, 特點(diǎn)是它的關(guān)注點(diǎn)與其他收集器不同.
收集器 | 關(guān)注點(diǎn) | 適合 |
---|---|---|
CMS等其他收集器 | 盡可能縮短垃圾收集時(shí)用戶線程的停頓時(shí)間 | |
Parallel Scavenge | 達(dá)到可控制的吞吐量 | 停頓時(shí)間越短,越適合用戶交互程序|高吞吐量可高效利用CPU時(shí)間,盡快完成程序運(yùn)算任務(wù),適合后臺(tái)運(yùn)算而不需要太多交互的任務(wù) |
- 吞吐量=運(yùn)行用戶代碼時(shí)間/(運(yùn)行用戶代碼時(shí)間+垃圾收集時(shí)間)
JVM總共運(yùn)行100min,垃圾收集1min,吞吐量=99%
- GC自適應(yīng)調(diào)節(jié)策略:JVM根據(jù)系統(tǒng)運(yùn)行情況收集性能監(jiān)控信息,動(dòng)態(tài)調(diào)整參數(shù)以提供合適的停頓時(shí)間或最大的吞吐量, 是與ParNew的一個(gè)重要區(qū)別
Serial Old 收集器
- Serial 的老年代版本
Parallel Old 收集器
- Parallel的老年代版本
- Parallel Scavenge+Parallel Old
收集器 | 線程 | 年代 | 關(guān)注點(diǎn) |
---|---|---|---|
Serial | 單線程 | 新生代 | 關(guān)注用戶線程的停頓時(shí)間 |
Serial Old | 單線程 | 老年代 | 關(guān)注用戶線程的停頓時(shí)間 |
ParNew | 多線程 | 新生代 | 關(guān)注用戶線程的停頓時(shí)間 |
Parallel Scavenge | 多線程 | 新生代 | <font color=red>關(guān)注吞吐量</font> |
Parallel Old | 多線程 | 老年代 | <font color=red>關(guān)注吞吐量</font> |
CMS | 多線程 | 老年代 | 關(guān)注用戶線程的停頓時(shí)間 |
CMS收集器
- Concurrent Mark Sweep
- 多線程
- 老年代收集器
- 以獲取最短回收停頓時(shí)間為目標(biāo)的收集器
- 適用于互聯(lián)網(wǎng)站或B/S系統(tǒng)的服務(wù)端
- 基于標(biāo)記-清除,但更復(fù)雜,有4個(gè)步驟
|階段序號(hào)|階段|描述|速度|Stop The World|(非并行)|
|----|----|----|----|
|1|初始標(biāo)記|僅僅只是標(biāo)記一下能直接關(guān)聯(lián)到對(duì)象的GC Roots|很快|是|否|
|2|并發(fā)標(biāo)記|GC Roots Tracing的過程,標(biāo)記GC鏈中的對(duì)象|很快|否|是|
|3|重新標(biāo)記|修正并發(fā)標(biāo)記期間因用戶程序繼續(xù)運(yùn)作而導(dǎo)致標(biāo)記產(chǎn)生變動(dòng)的那部分對(duì)象的標(biāo)記記錄|初始標(biāo)記<重新標(biāo)記<并發(fā)標(biāo)記|是|是|
|4|并發(fā)清除||耗時(shí)較長(zhǎng)|否|是|
- CMS收集器
-
缺點(diǎn)
- 對(duì)CPU資源非常敏感,并發(fā)階段占用線程(CPU資源)而導(dǎo)致應(yīng)用程序變慢,總吞吐量降低
- 無法處理浮動(dòng)垃圾:并發(fā)清理階段用戶線程還在運(yùn)行,伴隨新垃圾產(chǎn)生,這部分垃圾在標(biāo)記過程之后,無法當(dāng)次集中處理,只好下次再處理
- "標(biāo)記-清除"算法導(dǎo)致了大量空間碎片
G1收集器
Garbage-First
收集器技術(shù)發(fā)展的最前沿成果之一告丢,正式商用Since JDK1.7
-
特點(diǎn)
- 并行與并發(fā): 能充分利用多CPU硬件優(yōu)勢(shì),其他收集器需要Stop The World的地方, CMS依然可以并發(fā)執(zhí)行
- 分代收集: 即可以收集新生代也可以收集老年代.無須和其他收集器配合
- 空間整合:整體基于"標(biāo)記-整理"算法,局部基于"整理算法",不會(huì)產(chǎn)生內(nèi)存碎片
- 可預(yù)測(cè)停頓: 能讓使用者明確在長(zhǎng)度為M ms的時(shí)間片段內(nèi),消耗在垃圾收集上的時(shí)間不超過N ms
G1將整個(gè)Java堆劃分為多個(gè)大小相等的獨(dú)立區(qū)域(Region),雖然還保留了新生代和老年代的概念,但不再物理隔離了晃洒,它們都是一部分Region的集合
可預(yù)測(cè)停頓是因?yàn)镚1通過跟蹤各Region里垃圾堆積的價(jià)值大小,維護(hù)優(yōu)先級(jí)列表,根據(jù)允許的收集時(shí)間,優(yōu)先回收價(jià)值最大的Region(Grabage-First的由來),從而避免了在整個(gè)Java堆中進(jìn)行全區(qū)域的垃圾收集
-
G1把內(nèi)存"化整為零"的思路實(shí)現(xiàn)并不簡(jiǎn)單,因?yàn)镽egion并不是孤立的,避免全堆掃描的解決思路如下:
- 每個(gè)Region有個(gè)Remembered Set與之對(duì)應(yīng)
- JVM發(fā)現(xiàn)程序在對(duì)Reference類型的數(shù)據(jù)進(jìn)行寫操作時(shí), 會(huì)產(chǎn)生一個(gè)Write Barrier暫時(shí)中斷寫操作
- 檢查Reference引用的對(duì)象是否處于不同Region中
- 是則通過CardTable將相關(guān)Reference記錄到被引用對(duì)象所屬Region的Remembered Set中
- 當(dāng)內(nèi)存回收時(shí),GC Roots的枚舉范圍中加入Remembered Set即可保證不對(duì)全堆掃描也不會(huì)有遺漏
不計(jì)算Remembered Set的操作, G1 運(yùn)作的大致步驟如下:
階段序號(hào) | 階段 | 描述 | 速度 | Stop The World | 并發(fā)執(zhí)行(非并行) |
---|---|---|---|---|---|
1 | 初始標(biāo)記 | 僅僅只是標(biāo)記一下能直接關(guān)聯(lián)到對(duì)象的GC Roots | 很快 | 是 | 否 |
2 | 并發(fā)標(biāo)記 | GC Roots Tracing的過程,標(biāo)記GC鏈中的對(duì)象 | 很快 | 否 | 是 |
3 | 最終標(biāo)記 | 修正并發(fā)標(biāo)記期間因用戶程序繼續(xù)運(yùn)作而導(dǎo)致標(biāo)記產(chǎn)生變動(dòng)的那部分對(duì)象的標(biāo)記記錄 | 初始標(biāo)記<重新標(biāo)記<并發(fā)標(biāo)記 | 是 | 可并行 |
4 | 篩選回收 | 對(duì)各Region回收價(jià)值和成本進(jìn)行排序,根據(jù)用戶所期望的GC停頓時(shí)間來指定回收計(jì)劃 | 耗時(shí)用戶可控制 | 是 | 是 |
- G1收集器
內(nèi)存分配與回收策略
- 分配策略
-
空間分配擔(dān)保
空間分配擔(dān)保