識別垃圾算法
- 引用計(jì)數(shù)法
- 可達(dá)性算法
清除垃圾算法
- 標(biāo)記清除算法
- 復(fù)制算法
- 標(biāo)記整理算法
- 分代回收
一、引用計(jì)數(shù)法
1.原理
統(tǒng)計(jì)每一個(gè)對象被引用的次數(shù),如果引用次數(shù)為0就釋放對象。能立即回收無用內(nèi)存。
2.實(shí)現(xiàn)
當(dāng)一個(gè)對象要重新賦值引用時(shí):
- 把新對象引用計(jì)數(shù)+1
- 老對象引用計(jì)數(shù)-1
- 賦值
偽代碼:

3.存在的問題
- 并發(fā)場景下,對引用計(jì)數(shù)的修改需要和對象指針的修改保證同步,往往需要加鎖或者復(fù)雜的無鎖算法
- 有時(shí)會(huì)引發(fā)連鎖式的回收
- 無法有效解決循環(huán)引用

注意:要先加,再減,否則如果剛好減到0的話就會(huì)被回收了。
二、可達(dá)性分析算法(Java使用)
Java 虛擬機(jī)中的垃圾回收器采用可達(dá)性分析來探索所有存活的對象
掃描堆中的對象,看是否能夠沿著 GC Root對象 為起點(diǎn)的引用鏈找到該對象,找不到,表示可以回收
哪些對象可以作為 GC Root ?

- System Class:啟動(dòng)類加載器加載的類(Object ,HashMap,String)
- Native Stack
- Thread:活動(dòng)線程(局部變量所引用的對象)
- Busy Monitor:被加鎖的對象

public class GCRootsTest {
public static void main(String[] args) throws InterruptedException, IOException {
List<Object> list1 = new ArrayList<>();
list1.add("a");
list1.add("b");
System.out.println(1);
System.in.read();
list1 = null;
System.out.println(2);
System.in.read();
System.out.println("end...");
}
}
hongcaixia@hongcaixiadeMacBook-Pro stringtable % jmap -dump:format=b,live,file=1.bin 66669
Dumping heap to /Users/hongcaixia/Documents/work/workspace/demo/target/classes/jvm/stringtable/1.bin ...
Heap dump file created
hongcaixia@hongcaixiadeMacBook-Pro stringtable % jmap -dump:format=b,live,file=2.bin 66669
Dumping heap to /Users/hongcaixia/Documents/work/workspace/demo/target/classes/jvm/stringtable/2.bin ...
Heap dump file created
三、復(fù)制算法

1.原理
把程序運(yùn)行的堆分成大小相同的兩半,一半為from空間,一半為to空間。利用from空間進(jìn)行分配,當(dāng)空間不足以分配對象的時(shí)候,觸發(fā)GC。GC會(huì)把存活的對象全部復(fù)制到to空間。復(fù)制完成以后,會(huì)把from和to互換。
2.特點(diǎn)
- 1.分配采用bump the pointer,每次都把top指針向后移動(dòng)即可。復(fù)制的存活對象多大,指針就移動(dòng)多大。
- 2.回收是否高效取決于存活對象的比例。存活對象越少,效率越高
- 3.無內(nèi)存碎片
- 4.需要浪費(fèi)一半內(nèi)存空間
- 5.需要停頓
- 6.實(shí)現(xiàn)簡單
在整理的過程 需要停頓業(yè)務(wù)線程,因?yàn)樵谡韺ο蟮倪^程,指針會(huì)發(fā)生改變。
3.對象位置發(fā)生變化,指向的引用維護(hù)方法
1??引入中間層

雖然在復(fù)制的過程中變得簡單,但是中間層的分配和回收并不容易做;而且每次訪問對象屬性都變成了再次訪問,性能的退化也是不能接受的。
2??使用forwarding指針

1.A復(fù)制到to空間
2.因?yàn)锳指向著C,所以C也直接復(fù)制到to空間,修改C的引用,讓A指向C'
3.B復(fù)制到to空間,但是B的指針還是指向的from空間的C;
4.在第二步C復(fù)制到to空間時(shí),讓C指向新的C'地址(forwarding指針)。
5.B從C中的對象頭中拿到forwarding,指向新的C'。
4.提高空間利用率
將Eden空間分配成Eden,Survivor0和Survivor1區(qū)域。這樣Survivor空間的浪費(fèi)就可以減少了。
配置Survivor空間大小是JVM GC調(diào)參中的重要參數(shù)。
例如 -XX:SurvivorRatio=8 代表Eden:S0:S1=8:1:1

from:S1+Eden
to:S0
第一次:把s1+Eden一起經(jīng)過回收存活的放入S1;
from:S0+Eden
to:S1
第二次:把S0+Eden一起經(jīng)過回收存活的放入S0;
浪費(fèi)的空間就只有S0或者S1的大小。
四、標(biāo)記清除法

1.原理
使用鏈表管理所有的空閑區(qū)域。在Mark階段(標(biāo)在對象頭),將所有的存活對象識別出來,將不存活的對象所占用的內(nèi)存還給鏈表。
回收的這些對象所占用的內(nèi)存地址的起始和結(jié)束地址紀(jì)錄下來,放入空閑地址列表,下次再分配內(nèi)存時(shí),在空閑地址列表中找是否有足夠的空閑空間容納新對象,有則使用。
2.特點(diǎn)
- 1.分配和回收都要操作鏈表
分配要查詢鏈表哪個(gè)位置可以放得下這個(gè)對象,回收再將內(nèi)存還給鏈表 - 2.有內(nèi)存碎片
- 3.總體的內(nèi)存空間利用率較高
- 4.可以用很小的代價(jià)實(shí)現(xiàn)并發(fā)標(biāo)記和清除(在標(biāo)記的過程中對象指針不會(huì)發(fā)生變化,不需要停止業(yè)務(wù)線程)
- 5.速度快
五、標(biāo)記整理
沒有內(nèi)存碎片,利用率高,算法相對復(fù)雜,速度慢

1.找出需要回收的
2.把存活的對象放到回收的地方
分代算法:三色標(biāo)記+寫屏障
ZGC:顏色指針+讀屏障
新生代Serial和老年代Serial Old的組合
六、分代回收
1.新創(chuàng)建的對象嘗試放到eden,如果該對象比eden總量都大,那么直接放到老年代
2.eden沒有足夠的空間,觸發(fā)一次minorGC,將eden和from區(qū)的存活對象,移動(dòng)到to區(qū),對象年齡+1。然后將eden和from區(qū)進(jìn)行回收。最后 from區(qū)和to區(qū)互換3、如果to去沒有足夠的空間,那么將滿足條件的對象移入到老年代,對象的年齡達(dá)到了一定數(shù)值,6、15
4、移動(dòng)過程中老年代空間也不足了。需要回收老年代的mojorGC,往往回收老年代的時(shí)候需要將整個(gè)堆空間一并回收fullGC
新生代:Serial,ParNew,Parallel Scavenge
老年代:Serial Old,CMS,Parallel Old
即可在新生代,也可在老年代:G1,ZGC
分代回收:三色標(biāo)記+寫屏障
ZGC:顏色指針+讀屏障
垃圾回收器
- 串行單線程
堆內(nèi)存較小,適合個(gè)人電腦 - 吞吐量優(yōu)先
多線程
堆內(nèi)存較大,多核 cpu
讓單位時(shí)間內(nèi),STW 的時(shí)間最短 0.2 0.2 = 0.4,垃圾回收時(shí)間占比最低,這樣就吞吐量高 - 響應(yīng)時(shí)間優(yōu)先
多線程
堆內(nèi)存較大,多核 cpu
盡可能讓單次 STW 的時(shí)間最短 0.1 0.1 0.1 0.1 0.1 = 0.5
Serial+Serial Old
-XX:+UseSerialGC = Serial + SerialOld

沒有內(nèi)存碎片
新生代 Parallel Scavenge/ParNew和年老代Serial Old搭配

新生代Parallel Scavenge和老年代 Parallel Old
-XX:+UseParallelGC ~ -XX:+UseParallelOldGC(只要開啟一個(gè),另一個(gè)自動(dòng)開啟)

-XX:ParallelGCThreads=n 控制垃圾回收的線程數(shù)
-XX:+UseAdaptiveSizePolicy 采用自適應(yīng)大小調(diào)整策略(新生代大?。?br>
-XX:GCTimeRatio=ratio 調(diào)整吞吐量,垃圾回收時(shí)間和總時(shí)間的占比(達(dá)不到目標(biāo)則調(diào)整堆空間大小)
-XX:MaxGCPauseMillis=ms 最大暫停毫秒數(shù)(默認(rèn)200毫秒)
七、CMS
-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld(并發(fā)失敗退化為SerialOld)
(并發(fā):用戶線程和垃圾回收線程可以一起執(zhí)行)
響應(yīng)時(shí)間優(yōu)先:
-XX:ParallelGCThreads=n(并行的線程數(shù)) ~ -XX:ConcGCThreads=threads(并發(fā)線程數(shù))
-XX:CMSInitiatingOccupancyFraction=percent(執(zhí)行垃圾回收的內(nèi)存占比,需要預(yù)留空間給浮動(dòng)垃圾)
-XX:+CMSScavengeBeforeRemark(在重新標(biāo)記之前對新生代進(jìn)行一次垃圾回收,減少重新標(biāo)記時(shí)要掃描的對象)

- 初始標(biāo)記:stop-the-world,標(biāo)記GCRoots直接關(guān)聯(lián)的對象
- 并發(fā)標(biāo)記:并發(fā)追溯標(biāo)記,程序不會(huì)停頓
- 重新標(biāo)記:暫停虛擬機(jī),掃描CMS堆中的剩余對象
- 并發(fā)清理:清理垃圾對象,程序不會(huì)停頓
- 并發(fā)重置:重置CMS收集器的數(shù)據(jù)結(jié)構(gòu)
Promotion Fail:
當(dāng)年輕代進(jìn)行minor gc時(shí),把eden和from放到to區(qū)的時(shí)候,to區(qū)不夠用了,需要把存活的對象移動(dòng)至老年代,當(dāng)老年代沒有足夠的空間或者有足夠的空間但是太碎片化(標(biāo)記-清除算法)時(shí),就會(huì)發(fā)生Promotion Fail。
此時(shí),會(huì)將CMS降級為Serial Old。執(zhí)行full gc
解決辦法:當(dāng)標(biāo)記清除了一定次數(shù)之后,把老年代進(jìn)行整理。調(diào)大老年代/調(diào)大新生代。
標(biāo)記算法:三色標(biāo)記法
- 黑:已經(jīng)完成標(biāo)記
- 灰:標(biāo)記了一部分(類中某些成員還未標(biāo)記)
- 白:未標(biāo)記
由于GC線程和業(yè)務(wù)線程同時(shí)執(zhí)行,就會(huì)導(dǎo)致漏標(biāo)和錯(cuò)標(biāo)。
漏標(biāo):再重新標(biāo)記
錯(cuò)標(biāo):Incremental Update
當(dāng)已經(jīng)標(biāo)記完的對象又被某個(gè)線程重新指向的時(shí)候,將黑色換成灰色。
八、G1
-XX:+UseG1GC
適用場景
- 同時(shí)注重吞吐量(Throughput)和低延遲(Low latency),默認(rèn)的暫停目標(biāo)是 200 ms
- 超大堆內(nèi)存,會(huì)將堆劃分為多個(gè)大小相等的 Region
- 整體上是 標(biāo)記+整理 算法,兩個(gè)區(qū)域之間是 復(fù)制 算法

1、G1垃圾收集器將整個(gè) JVM 內(nèi)存分為多個(gè)大小相等的region,年輕代和老年代邏輯分區(qū) 。
2、G1 是 Java9 以后的默認(rèn)垃圾回收器
3、G1 在整體上使用標(biāo)記整理算法,局部使用復(fù)制算法
4、G1 的每個(gè) Region 大小在 1-32M 之間,可以通過
-XX:G1HeapRegionSize=n指定區(qū)大小。5、總的 Region 個(gè)數(shù)最大可以存在 2048 個(gè),即heap最大能夠達(dá)到32M*2048=64G
6、0.5<obj<1,那么放到old區(qū),old標(biāo)記為H
1<obj<n,連續(xù)的n個(gè)region,作為H
邏輯分區(qū),三色標(biāo)記+寫屏障
借助SATB算法,snapshot at the begins
第一階段:YoungGC的過程:
會(huì) STW


第二階段:YoungGC+concurrent mark
在 Young GC 時(shí)會(huì)進(jìn)行 GC Root 的初始標(biāo)記
老年代占用堆空間比例達(dá)到閾值時(shí),進(jìn)行并發(fā)標(biāo)記(不會(huì) STW),由下面的 JVM 參數(shù)決定-XX:InitiatingHeapOccupancyPercent=percent (默認(rèn)45%)

第三階段:MixGC過程
會(huì)對 E、S、O 進(jìn)行全面垃圾回收
最終標(biāo)記(Remark)會(huì) STW
拷貝存活(Evacuation)會(huì) STW
-XX:MaxGCPauseMillis=ms

根據(jù)最大暫停時(shí)間有選擇的回收
- 初始標(biāo)記:標(biāo)記出GCRoot對象,以及GCRoot所在的Region(RootRegion)
- Root Region Scanning:掃表整個(gè)old的Region(查看root region的rset是否有引用)
- 并發(fā)標(biāo)記:并發(fā)追溯標(biāo)記,進(jìn)行GCRootsTracing的過程(只標(biāo)記gcroot中的rset這部分)
- 最終標(biāo)記:修正并發(fā)標(biāo)記期間,因程序運(yùn)行導(dǎo)致標(biāo)記發(fā)生變化的那一部分對象(SATB算法)
- 清理回收:根據(jù)時(shí)間來進(jìn)行價(jià)值最大化的回收,重置rset
1.標(biāo)記gcroot和gc root 所在的region
2.掃描gc root region和rset中的root
3.對rset進(jìn)行標(biāo)記
4.針對漏標(biāo),錯(cuò)標(biāo),使用SATB算法重新標(biāo)記
5.回收,重置rset
G1相關(guān)的參數(shù)配置:
- -XX:+UseG1GC :設(shè)置使用 G1 垃圾回收器
- -XX:MaxGCPauseMillis=n :最大 GC 停頓時(shí)間,毫秒值
- -XX:InitatingHeapOccupancyPercent=n:當(dāng)堆空間占用到 n 兆時(shí)就觸發(fā) GC(45)
- -XX:GoncGCThreads=n:并發(fā) GC 使用的線程數(shù)
- -XX:G1ReserverPercent=n:設(shè)置作為空閑空間的預(yù)留內(nèi)存百分比(10%)
-XX:G1HeapRegionSize=size(設(shè)置每個(gè)region大小)
YoungGC跨代引用問題

- 卡表與 Remembered Set
- 在引用變更時(shí)通過 post-write barrier + dirty card queue(當(dāng)有引用新生代時(shí)標(biāo)記為臟card,減少掃描范圍)
- concurrent refinement threads 更新 Remembered Set
Remark
pre-write barrier + satb_mark_queue

當(dāng)引用發(fā)生改變時(shí),加入寫屏障,把發(fā)生了引用的對象加入到隊(duì)列中,將對象的顏色改為灰色,重新標(biāo)記階段會(huì)把隊(duì)列中的對象再標(biāo)記一次。
優(yōu)化點(diǎn)1:JDK 8u20 字符串去重
- 優(yōu)點(diǎn):節(jié)省大量內(nèi)存
- 缺點(diǎn):略微多占用了 cpu 時(shí)間,新生代回收時(shí)間略微增加
-XX:+UseStringDeduplication
String s1 = new String("hello"); // char[]{'h','e','l','l','o'}
String s2 = new String("hello"); // char[]{'h','e','l','l','o'}
- 將所有新分配的字符串放入一個(gè)隊(duì)列
- 當(dāng)新生代回收時(shí),G1并發(fā)檢查是否有字符串重復(fù)
- 如果它們值一樣,讓它們引用同一個(gè) char[]
- 注意,與 String.intern() 不一樣
- String.intern() 關(guān)注的是字符串對象
- 而字符串去重關(guān)注的是 char[]
- 在 JVM 內(nèi)部,使用了不同的字符串表
優(yōu)化點(diǎn)2:DK 8u40 并發(fā)標(biāo)記類卸載
所有對象都經(jīng)過并發(fā)標(biāo)記后,就能知道哪些類不再被使用,當(dāng)一個(gè)類加載器的所有類都不再使用,則卸載它所加載的所有類
-XX:+ClassUnloadingWithConcurrentMark 默認(rèn)啟用
優(yōu)化點(diǎn)3:JDK 8u60 回收巨型對象
- 一個(gè)對象大于 region 的一半時(shí),稱之為巨型對象
- G1 不會(huì)對巨型對象進(jìn)行拷貝
- 回收時(shí)被優(yōu)先考慮
- G1 會(huì)跟蹤老年代所有 incoming 引用,這樣老年代 incoming 引用為0 的巨型對象就可以在新生代垃圾回收時(shí)處理掉
優(yōu)化點(diǎn)4: JDK 9 并發(fā)標(biāo)記起始時(shí)間的調(diào)整
- 并發(fā)標(biāo)記必須在堆空間占滿前完成,否則退化為 FullGC
- JDK 9 之前需要使用 -XX:InitiatingHeapOccupancyPercent
- JDK 9 可以動(dòng)態(tài)調(diào)整
- -XX:InitiatingHeapOccupancyPercent 用來設(shè)置初始值
- 進(jìn)行數(shù)據(jù)采樣并動(dòng)態(tài)調(diào)整
- 總會(huì)添加一個(gè)安全的空檔空間
九、ZGC
著色指針+讀屏障
十、JVM相關(guān)參數(shù)
| 含義 | 參數(shù) |
|---|---|
| 堆初始大小 | -Xms |
| 堆最大大小 | -Xmx 或 -XX:MaxHeapSize=size |
| 新生代大小 | -Xmn 或 (-XX:NewSize=size + -XX:MaxNewSize=size ) |
| 幸存區(qū)比例(動(dòng)態(tài)) | -XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy |
| 幸存區(qū)比例 | -XX:SurvivorRatio=ratio |
| 晉升閾值 | -XX:MaxTenuringThreshold=threshold |
| 晉升詳情 | -XX:+PrintTenuringDistribution |
| GC詳情 | -XX:+PrintGCDetails -verbose:gc |
| FullGC 前 MinorGC | -XX:+ScavengeBeforeFullGC |
/**
* 演示內(nèi)存的分配策略
*/
public class Demo1 {
private static final int _512KB = 512 * 1024;
private static final int _1MB = 1024 * 1024;
private static final int _6MB = 6 * 1024 * 1024;
private static final int _7MB = 7 * 1024 * 1024;
private static final int _8MB = 8 * 1024 * 1024;
// -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc -XX:-ScavengeBeforeFullGC
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
ArrayList<byte[]> list = new ArrayList<>();
list.add(new byte[_8MB]);
list.add(new byte[_8MB]);
}).start();
System.out.println("sleep....");
Thread.sleep(1000L);
}
}

線程內(nèi)的oom不會(huì)導(dǎo)致整個(gè)進(jìn)程結(jié)束。