JVM垃圾回收

識別垃圾算法

  • 引用計(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
  • 賦值

偽代碼:


image.png

3.存在的問題

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

注意:要先加,再減,否則如果剛好減到0的話就會(huì)被回收了。

二、可達(dá)性分析算法(Java使用)

Java 虛擬機(jī)中的垃圾回收器采用可達(dá)性分析來探索所有存活的對象
掃描堆中的對象,看是否能夠沿著 GC Root對象 為起點(diǎn)的引用鏈找到該對象,找不到,表示可以回收

哪些對象可以作為 GC Root ?


image.png
  • System Class:啟動(dòng)類加載器加載的類(Object ,HashMap,String)
  • Native Stack
  • Thread:活動(dòng)線程(局部變量所引用的對象)
  • Busy Monitor:被加鎖的對象
image.png
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ù)制算法

復(fù)制算法.png

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??引入中間層

引入間接指針.png

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

2??使用forwarding指針

使用forwarding指針.png

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

提高空間利用率.png

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)記清除法

標(biāo)記清除法.png

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ù)雜,速度慢


image.png

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:顏色指針+讀屏障

垃圾回收器

  1. 串行單線程
    堆內(nèi)存較小,適合個(gè)人電腦
  2. 吞吐量優(yōu)先
    多線程
    堆內(nèi)存較大,多核 cpu
    讓單位時(shí)間內(nèi),STW 的時(shí)間最短 0.2 0.2 = 0.4,垃圾回收時(shí)間占比最低,這樣就吞吐量高
  3. 響應(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


image.png

沒有內(nèi)存碎片

新生代 Parallel Scavenge/ParNew和年老代Serial Old搭配

image.png

新生代Parallel Scavenge和老年代 Parallel Old

-XX:+UseParallelGC ~ -XX:+UseParallelOldGC(只要開啟一個(gè),另一個(gè)自動(dòng)開啟)


image.png

-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í)要掃描的對象)

image.png
  • 初始標(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ù)制 算法

image.png

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


image.png

image.png

第二階段: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%)

image.png

第三階段:MixGC過程

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


image.png

根據(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跨代引用問題

image.png
  • 卡表與 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

image.png

當(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);
    }
}
image.png

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

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

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容