JavaGuide知識點整理——JVM垃圾回收

本篇文章的基本脈絡(luò)


知識點脈絡(luò)

當(dāng)需要排查各種內(nèi)存溢出問題,當(dāng)垃圾收集稱為系統(tǒng)達到更高并發(fā)的瓶頸時软吐,我們就需要對這些自動化技術(shù)實施必要的監(jiān)控和調(diào)節(jié)。

揭開JVM內(nèi)存分配與回收的神秘面紗

java的自動內(nèi)存管理主要是針對對象內(nèi)存的回收和對象內(nèi)存的分配佃延。同時java自動內(nèi)存管理最核心的功能是堆內(nèi)存中對象的分配與回收褒搔。
java堆是垃圾收集器管理的主要區(qū)域,因此也被稱為GC堆恳啥。從垃圾回收的角度偏灿,由于現(xiàn)在收集器基本都采用分代垃圾收集算法,所以java堆還可以細分為新生代钝的,老年代菩混。再細致一點有:Eden空間,F(xiàn)rom Survivor扁藕,To Survivor空間等烟具。進一步劃分的目的是更好的回收內(nèi)存眉睹,或者更快地分配內(nèi)存。
堆空間的基本結(jié)構(gòu)如下:

堆的基本結(jié)構(gòu)

上圖所示的Eden區(qū),s0區(qū)亭敢,s1區(qū)都屬于新生代该园,Old Memory屬于老年代槽驶。
大部分情況下审洞,對象都會首先在Eden區(qū)分配,在一次新生代垃圾回收后痕支,如果對象還存活颁虐,則會進入s0或者s1,并且對象年齡加1.當(dāng)它的年齡增加到一定程度(默認15)就會晉升到老年代中卧须,對象晉升到老年代的年齡閾值可以通過參數(shù)-XX:MaxTenuringThreshold來設(shè)置另绩。這個值會在虛擬機運行過程中進行調(diào)整。但是HotSpot有個機制:遍歷所有對象時花嘶,按照年齡從小打大對其占用大小累積笋籽,當(dāng)累積的某個年齡大小超過了s區(qū)的一半時,去這個年齡和設(shè)置的默認年齡更小的那個值作為新的晉升年齡閾值椭员。
比如說 設(shè)置年齡10车海, S區(qū)內(nèi)存空間共10, 1歲的2,2歲的1,3歲的4隘击,4歲的0侍芝,5歲的2...這個時候HotSpot遍歷的時候 1歲的2 + 2歲的1 +3歲的4.發(fā)現(xiàn)到3歲超過了s區(qū)的一半,那么會把3和10去對比埋同,發(fā)現(xiàn)3更小州叠,會把晉升年齡閾值設(shè)置為3、
動態(tài)年齡計算的代碼如下:

uint ageTable::compute_tenuring_threshold(size_t survivor_capacity) {
//survivor_capacity是survivor空間的大小
size_t desired_survivor_size = (size_t)((((double)survivor_capacity)*TargetSurvivorRatio)/100);
size_t total = 0;
uint age = 1;
while (age < table_size) {
  //sizes數(shù)組是每個年齡段對象大小
  total += sizes[age];
  if (total > desired_survivor_size) {
      break;
  }
  age++;
}
uint result = age < MaxTenuringThreshold ? age : MaxTenuringThreshold;
...
}

經(jīng)過GC后莺禁,Eden區(qū)和From區(qū)應(yīng)該唄清空留量。這個時候From和To會交換覺得窄赋。也就是新的To變成了上次GC前的From哟冬,新的From就是上次GC前的To楼熄。不管怎么樣都會保證名為To的Survivor區(qū)域是空的、MinorGC會一直重復(fù)這樣的過程浩峡。在這個過程可岂,有可能當(dāng)Minor GC后,Survivor的From區(qū)域空間不夠翰灾,有一些還不達到進入老年代條件的實例放不下缕粹,則放不下的部分會提前進入老年代。下面我們用代碼測試一下:
參數(shù)設(shè)置如下

-verbose:gc
-Xmx200M
-Xms200M
-Xmn50M
-XX:+PrintGCDetails
-XX:TargetSurvivorRatio=60
-XX:+PrintTenuringDistribution
-XX:+PrintGCDateStamps
-XX:MaxTenuringThreshold=3
-XX:+UseConcMarkSweepGC
-XX:+UseParNewGC

示例代碼如下:

/*
* 本實例用于java GC以后纸淮,新生代survivor區(qū)域的變化平斩,以及晉升到老年代的時間和方式的測試代碼。需要自行分步注釋不需要的代碼進行反復(fù)測試對比
*
* 由于java的main函數(shù)以及其他基礎(chǔ)服務(wù)也會占用一些eden空間咽块,所以要提前空跑一次main函數(shù)绘面,來看看這部分占用。
*
* 自定義的代碼中侈沪,我們使用堆內(nèi)分配數(shù)組和棧內(nèi)分配數(shù)組的方式來分別模擬不可被GC的和可被GC的資源揭璃。
*
*
* */

public class JavaGcTest {

    public static void main(String[] args) throws InterruptedException {
        //空跑一次main函數(shù)來查看java服務(wù)本身占用的空間大小,我這里是占用了3M亭罪。所以40-3=37瘦馍,下面分配三個1M的數(shù)組和一個34M的垃圾數(shù)組。


        // 為了達到TargetSurvivorRatio(期望占用的Survivor區(qū)域的大杏σ邸)這個比例指定的值, 即5M*60%=3M(Desired survivor size)情组,
        // 這里用1M的數(shù)組的分配來達到Desired survivor size
        //說明: 5M為S區(qū)的From或To的大小,60%為TargetSurvivorRatio參數(shù)指定,可以更改參數(shù)獲取不同的效果箩祥。
        byte[] byte1m_1 = new byte[1 * 1024 * 1024];
        byte[] byte1m_2 = new byte[1 * 1024 * 1024];
        byte[] byte1m_3 = new byte[1 * 1024 * 1024];

        //使用函數(shù)方式來申請空間呻惕,函數(shù)運行完畢以后,就會變成垃圾等待回收滥比。此時應(yīng)保證eden的區(qū)域占用達到100%亚脆。可以通過調(diào)整傳入值來達到效果盲泛。
        makeGarbage(34);

        //再次申請一個數(shù)組濒持,因為eden已經(jīng)滿了,所以這里會觸發(fā)Minor GC
        byte[] byteArr = new byte[10*1024*1024];
        // 這次Minor Gc時, 三個1M的數(shù)組因為尚有引用寺滚,所以進入From區(qū)域(因為是第一次GC)age為1
        // 且由于From區(qū)已經(jīng)占用達到了60%(-XX:TargetSurvivorRatio=60), 所以會重新計算對象晉升的age柑营。
        // 計算方法見上文,計算出age:min(age, MaxTenuringThreshold) = 1村视,輸出中會有Desired survivor size 3145728 bytes, new threshold 1 (max 3)字樣
        //新的數(shù)組byteArr進入eden區(qū)域官套。


        //再次觸發(fā)垃圾回收,證明三個1M的數(shù)組會因為其第二次回收后age為2,大于上一次計算出的new threshold 1奶赔,所以進入老年代惋嚎。
        //而byteArr因為超過survivor的單個區(qū)域,直接進入了老年代站刑。
        makeGarbage(34);
    }
    private static void makeGarbage(int size){
        byte[] byteArrTemp = new byte[size * 1024 * 1024];
    }
}

注意如下輸出結(jié)果匯總老年代的信息為concurrent mark-sweep generation另伍、另外還列出了某次GC后是否重新生成了threshold。以及各個年齡占用空間大小绞旅。

2021-07-01T10:41:32.257+0800: [GC (Allocation Failure) 2021-07-01T10:41:32.257+0800: [ParNew
Desired survivor size 3145728 bytes, new threshold 1 (max 3)
- age   1:    3739264 bytes,    3739264 total
: 40345K->3674K(46080K), 0.0014584 secs] 40345K->3674K(199680K), 0.0015063 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2021-07-01T10:41:32.259+0800: [GC (Allocation Failure) 2021-07-01T10:41:32.259+0800: [ParNew
Desired survivor size 3145728 bytes, new threshold 3 (max 3)
: 13914K->0K(46080K), 0.0046596 secs] 13914K->13895K(199680K), 0.0046873 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
 par new generation   total 46080K, used 35225K [0x05000000, 0x08200000, 0x08200000)
  eden space 40960K,  86% used [0x05000000, 0x072667f0, 0x07800000)
  from space 5120K,   0% used [0x07800000, 0x07800000, 0x07d00000)
  to   space 5120K,   0% used [0x07d00000, 0x07d00000, 0x08200000)
 concurrent mark-sweep generation total 153600K, used 13895K [0x08200000, 0x11800000, 0x11800000)
 Metaspace       used 153K, capacity 2280K, committed 2368K, reserved 4480K

對象優(yōu)先在Eden區(qū)分配

目前主流的垃圾收集器都會采用分代回收算法摆尝, 因此需要將堆內(nèi)存分為新生代和老年代。這樣我們就可以根據(jù)各個年代的特點選擇合適的垃圾收集算法因悲。
大多數(shù)情況下堕汞,對象在新生代中Eden區(qū)分配。當(dāng)Eden區(qū)沒有足夠空間進行分配時晃琳,虛擬機將發(fā)起一次Minor GC臼朗,下面我們實際測試一下:

jvm打印GC日志的命令如下-XX:+PrintGCDetails

兩個對象都沒有實例化的內(nèi)存情況

代碼如下:

    public static void main(String[] args) throws Exception {
        byte[] allocation1, allocation2;
        allocation1 = new byte[20000*1024];
        // allocation2 = new byte[20000*1024];
    }

實例化一個后運行結(jié)果如下:


allocation1占用Eden空間

從圖中可以看出Eden區(qū)占用百分之九十多了,肯定裝不下實例化后的allocation2了蝎土,現(xiàn)在我們打開注釋的代碼再次運行:


老年代被占用

簡單解釋一下為什么會出現(xiàn)這種情況:因為給allocation2分配內(nèi)存的時候eden區(qū)幾乎被分配完了视哑,我們剛剛講了當(dāng)Eden區(qū)沒有足夠空間進行分配時,虛擬機將發(fā)起一次Minor GC誊涯, GC期間虛擬機又發(fā)現(xiàn)allocation1無法存入Survivor空間挡毅,所以只好通過分配擔(dān)保機制把新生代的對象提前轉(zhuǎn)移到老年代中去,老年代上的空間足夠存放allocation1暴构,所以不會出現(xiàn)full GC跪呈。執(zhí)行Minor GC后,后面分配的對象如果能存進Eden區(qū)還是會在eden區(qū)分配內(nèi)存取逾『穆蹋可以執(zhí)行下面的代碼驗證:

至此占用eden百分之10

打開最后一個對象占用eden百分之15

由此說明新對象分配在eden區(qū)。

大對象直接進入老年代

大對象就是需要大量連續(xù)內(nèi)存空間的對象(比如字符串砾隅,數(shù)組)

為什么這么做呢误阻?
為了避免為大對象分配內(nèi)存時由于分配擔(dān)保機制帶來的復(fù)制而降低效率。

長期存活的對象將進入老年代

既然虛擬機采用了分代收集的思想來管理內(nèi)存晴埂,那么內(nèi)存回收時就必須能識別哪些對象應(yīng)該放在新生代究反,哪些對象應(yīng)該放在老年代。為了做到這一點儒洛,虛擬機給每個對象一個對象年齡計數(shù)器精耐。
如果對象在Eden出生并經(jīng)過一次Minor GC后仍然能夠存活,并且能被Survivor容納的話琅锻,將被移動到Survivor空間中卦停,并將對象年齡設(shè)置為1向胡,對象在Survivor中每熬過一次Minor GC,年齡就增長一歲惊完。當(dāng)它的年齡增加到一定程度(默認15歲)就會晉升到老年代中僵芹,對象晉升到老年代的年齡閾值,可以通過參數(shù)-XX:MaxTenuringThreshold來設(shè)置专执。

動態(tài)對象年齡判斷

大部分情況對象都會首先在Eden區(qū)域分配,在一次新生代垃圾回收之后郁油,如果對象還存活本股,則會進入S0或者S1。并且對象的年齡還會加1.當(dāng)年齡到大設(shè)定的閾值會晉升到老年代桐腌。
但是HotSpot還有一個機制:HotSpot遍歷所有對象時拄显,按照年齡從小到大對其大小進行累積。當(dāng)累積的某個年齡大小超過Survivor區(qū)的一半時案站,取這個年齡和默認閾值較小的那個作為晉升年齡的閾值躬审。代碼如下:

uint ageTable::compute_tenuring_threshold(size_t survivor_capacity) {
//survivor_capacity是survivor空間的大小
size_t desired_survivor_size = (size_t)((((double)survivor_capacity)*TargetSurvivorRatio)/100);
size_t total = 0;
uint age = 1;
while (age < table_size) {
//sizes數(shù)組是每個年齡段對象大小
total += sizes[age];
if (total > desired_survivor_size) {
   break;
}
age++;
}
uint result = age < MaxTenuringThreshold ? age : MaxTenuringThreshold;
...
}

還有一點:默認晉升年齡并不都是15,區(qū)分垃圾收集器的蟆盐。CMS默認的就是6.

主要進行GC的區(qū)域

針對HotSpot VM的實現(xiàn)承边,它里面的GC其實準確的分類只有兩種:

  • 部分收集(Partial GC):
    • 新生代收集(Minor GC/Young GC):只針對新生代進行垃圾收集。
    • 老年代收集(Major GC/Old GC):只對老年代進行垃圾收集石挂。需要注意的是Major GC在有的語境中也用于指代 整堆收集
    • 混合收集(Mixed GC):對整個新生代和部分老年代進行垃圾收集博助。
  • 整堆收集(Full GC):收集整個java堆和方法區(qū)。

空間分配擔(dān)保

空間分配擔(dān)保是為了確保在Minor GC之前老年代本身還有容納新生代所有對象的剩余空間痹愚。

JDK6.24之前富岳,在發(fā)生Minor GC之前,虛擬機必須先檢查老年代最大可用的連續(xù)空間是否大于新生代所有對象總空間拯腮。如果這個條件成立窖式,那這一次Minor GC可以確保是安全的。如果不成立动壤,則虛擬機會先查看-XX:HandlePromotionFailure 參數(shù)的設(shè)置值是否允許擔(dān)保失敗萝喘。如果允許,那么會繼續(xù)檢查老年代最大可用的連續(xù)空間是否大于歷次晉升到老年代對象的平均大小琼懊,如果大于蜒灰,將會嘗試一次Minor GC。盡管這次Minor GC是有風(fēng)險的肩碟。如果小于或者XX:HandlePromotionFailure設(shè)置不允許冒險强窖,那這時就要改為進行一次Full GC。
JDK6.24以后的會澤變?yōu)橹灰夏甏倪B續(xù)空間大于新生代對象總大小或者歷次晉升的平均大小削祈,就會進行MinorGC,否則將進行Full GC翅溺。

對象已經(jīng)死亡

堆中幾乎放著所有的對象實例脑漫,對堆垃圾回收前的第一步就是要判斷哪些對象已經(jīng)死亡(即不能再被任何途徑使用的對象)。

引用計數(shù)法

給對象中添加一個引用計數(shù)器咙崎,每當(dāng)有一個地方引用它优幸,計數(shù)器就加1.當(dāng)引用失效,計數(shù)器就減1.任何時候計數(shù)器為0的對象就是不可能再被使用的褪猛。
這個方法實現(xiàn)簡單网杆,效率高,但是目前主流的虛擬機中并沒有選擇這個算法來管理內(nèi)存伊滋,其主要原因就是它很難結(jié)果循環(huán)引用的問題碳却。所謂對象的相互引用問題,就是兩個對象互相引用笑旺,除此之外再無其他引用昼浦。因為互相引用導(dǎo)致計數(shù)器不為0.于是引用計數(shù)法無法通知GC回收他們。代碼如下:

public class ReferenceCountingGc {
    Object instance = null;
    public static void main(String[] args) {
        ReferenceCountingGc objA = new ReferenceCountingGc();
        ReferenceCountingGc objB = new ReferenceCountingGc();
        objA.instance = objB;
        objB.instance = objA;
        objA = null;
        objB = null;

    }
}

可達性分析算法

這個算法的基本思想就是通過一系列的稱為“GC Roots”的對象作為起點筒主,從這些節(jié)點開始向下搜索关噪,節(jié)點所走過的路徑稱為引用鏈,當(dāng)一個對象到GC Roots沒有任何引用鏈相連的話乌妙,證明此對象是不可用的使兔,需要被回收。

下圖中的Object6-Object10雖然有引用關(guān)系藤韵,但是他們到GC Roots不可達火诸,因為是需要被回收的對象。


image.png

哪些對象可以作為GC Roots呢荠察?

  • 虛擬機棧(棧幀中的本地變量表)中引用的對象
  • 本地方法棧(Native方法)中引用的對象
  • 方法區(qū)中類靜態(tài)屬性引用的變量
  • 方法區(qū)中常量引用的對象
  • 所有被同步鎖持有的對象

對象可以被回收置蜀,就代表一定會被回收么?
即使在可達性分析法中不可達的對象悉盆,也并非是"非死不可"的盯荤,這時候它們暫時處于"緩刑階段"。要真正宣告一個對象死亡焕盟。至少要經(jīng)歷兩次標(biāo)記過程:可達性分析法中不可達的對象被第一次標(biāo)記并且進行一次篩選秋秤。篩選的條件是此對象是否有必要執(zhí)行finalize方法,當(dāng)對象沒有覆蓋finalize方法或者finalize方法已經(jīng)被虛擬機調(diào)用過時脚翘,虛擬機將這兩種情況視為沒有必要執(zhí)行灼卢。
被判定為需要執(zhí)行的對象將會被放在一個隊列中進行二次標(biāo)記,除非這個對象與引用鏈上的任何一個對象建立關(guān)聯(lián)来农,否則就會被真的回收鞋真。

再談引用

無論是否通過引用計數(shù)法判斷對象引用數(shù)量,還是通過可達性分析法判斷對象的引用鏈是否可達沃于,判斷對象的存活都與"引用"有關(guān)涩咖。
JDK1.2之前海诲,java中引用的定義很傳統(tǒng):如果reference類型的數(shù)據(jù)存儲的數(shù)值代表的是另一塊內(nèi)存的起始地址,就稱這塊內(nèi)存代表一個引用檩互。
JDK1.2之后特幔,java對引用的概念進行了擴充,將引用分為強引用闸昨,軟引用蚯斯,弱引用,虛引用四種(引用強度逐漸減弱)

強引用
以前我們使用的大部分引用實際上都是強引用饵较。這是使用最普遍的引用拍嵌。如果一個對象具有強引用,那么類似于必不可少的生活用品告抄。垃圾回收器絕對不會回收它撰茎。當(dāng)內(nèi)存空間不足的時候嵌牺,java虛擬機寧愿拋出OOM錯誤使程序異常終止打洼,也不會靠隨意回收具有強引用的對象來解決內(nèi)存不足的問題。

軟引用
如果一個對象只具有軟引用逆粹,那就類似于可有可無的生活用品募疮。如果內(nèi)存空間足夠的話,垃圾回收器就不會回收它僻弹,如果內(nèi)存空間不足了阿浓,就會回收這些對象的內(nèi)存,只要垃圾回收器沒有回收它蹋绽,該對象就可以被程序使用芭毙。軟引用可以用來實現(xiàn)內(nèi)存敏感的高速緩存。
軟引用可以和一個引用隊列聯(lián)合使用卸耘。如果軟引用所引用的對象被垃圾回收退敦,java虛擬機就會把這個軟引用加入到與之關(guān)聯(lián)的引用隊列中。

弱引用
如果一個對象只具有弱引用蚣抗,也類似于可有可無的生活用品侈百。弱引用與軟引用的區(qū)別在于:只有弱引用的對象擁有更短暫的生命周期。在垃圾回收器掃描他所管轄的內(nèi)存區(qū)域時翰铡,一旦發(fā)現(xiàn)了只有弱引用的對象钝域,不管當(dāng)前內(nèi)存是否足夠,都會回收它锭魔,不過由于垃圾回收器是一個優(yōu)先級很低的線程例证,因此不一定會很快發(fā)現(xiàn)那些只有弱引用的對象。

虛引用
虛引用顧名思義形同虛設(shè)迷捧,并不會決定對象的聲明周期战虏,如果一個對象只有虛引用拣宰,那么就和沒有引用一樣。任何時候都可能被垃圾回收烦感。
虛引用主要用來跟蹤對象被垃圾回收的活動巡社。

虛引用和軟引用和弱引用的區(qū)別:虛引用必須和引用隊列聯(lián)合使用。當(dāng)垃圾回收器準備回收一個對象的時候手趣,如果發(fā)現(xiàn)它還有虛引用晌该,就會在回收對象之前把這個虛引用加入到與之關(guān)聯(lián)的引用隊列中。程序可以判斷引用隊列中是否已經(jīng)加入了虛引用绿渣。來了解被引用的對象是否將要被垃圾回收朝群。程序如果發(fā)現(xiàn)某個虛引用已經(jīng)被加入到引用隊列,那么就可以在所引用的對象的內(nèi)存被回收之前采取必要的行動中符。

特別注意的是姜胖,在程序設(shè)計中一般很少使用弱引用和虛引用,使用軟引用的情況比較多淀散,這是因為軟引用可以加速JVM對垃圾內(nèi)存的回收速度右莱,可以維護系統(tǒng)的運行安全,防止內(nèi)存溢出等問題的產(chǎn)生档插。

如何判斷一個常量是廢棄常量慢蜓?

運行時常量池主要回收的是廢棄的常量,那么我們?nèi)绾闻袛嘁粋€常量是廢棄常量呢郭膛?
JDK1.7之前運行時常量池邏輯包含字符串常量池存在方法區(qū)晨抡,此時HotSpot虛擬機對方法區(qū)的實現(xiàn)為永久代
JDK1.7字符串常量池被從方法區(qū)拿到了堆中,這里沒有提到運行時常量池则剃。也就是說字符串常量池被單獨拿到堆中耘柱,運行時常量池剩下的東西還是在方法區(qū)中,也就是永久代棍现。
JDK1.8HotSpot移除了永久代调煎,用元空間取而代之。這時候字符串常量池還是在堆中轴咱,運行時常量池還是在方法區(qū)汛蝙。只不過是從永久代變成了元空間。

假如在字符串常量池中存在字符串"abc",如果當(dāng)前沒有任何String對象引用該字符串常量的話朴肺,說明常量"abc"是廢棄常量窖剑,如果這時候發(fā)生內(nèi)存回收且有必要的話,"abc"就會被系統(tǒng)清理出常量池了戈稿。

如何判斷一個類是無用的類

方法區(qū)主要回收的是無用的類西土,那么如何判斷一個類是無用的類呢?
判定一個常量是否廢棄比較簡單鞍盗,而判斷一個類是否是無用的類的條件則相對苛刻許多需了,類需要同時滿足下面三個條件才算是無用的類:

  • 該類所有的實例都已經(jīng)被回收跳昼,也就是java堆中不存在該類的任何實例。
  • 加載該類的ClassLoader已經(jīng)被回收
  • 該類對應(yīng)的java.lang.Class對象沒有在任何地方被引用肋乍,無法在任何地方通過反射訪問該類的方法鹅颊。

虛擬機對可以滿足上述三個條件的無用類進行回收,這里說的僅僅是可以墓造,而不是和對象一樣不使用了就必然被回收堪伍。

垃圾收集算法

標(biāo)記-清除算法

該算法分為標(biāo)記和清除兩個階段:首先標(biāo)記出所有不需要回收的對象,在標(biāo)記完成后統(tǒng)一回收掉所有沒有被標(biāo)記的對象觅闽。它是最基礎(chǔ)的收集算法帝雇,后續(xù)的算法都是對其不足進行改進得到的。這種垃圾收集算法會帶來兩個明顯的問題:

  1. 效率問題
  2. 空間問題(標(biāo)記清除后悔產(chǎn)生大量不連續(xù)的碎片)
    標(biāo)記清除

標(biāo)記-復(fù)制算法

為了解決效率問題蛉拙,標(biāo)記-復(fù)制算法出現(xiàn)了尸闸,它可以將內(nèi)存分為大小相同的兩塊,每次使用其中一塊孕锄,當(dāng)這一塊內(nèi)存用完了以后吮廉,就將還存活的對象復(fù)制到另一塊去,然后再把使用的空間一次性清理掉硫惕。這樣每次的內(nèi)存回收都是對內(nèi)存區(qū)間的一半進行回收茧痕。


標(biāo)記-復(fù)制

標(biāo)記-整理算法

根據(jù)老年代的特點提出的一種標(biāo)記算法野来,標(biāo)記過程和標(biāo)記-清除算法一樣恼除。但是后續(xù)步驟不是直接對可回收對象回收。而是讓所有存活的對象向一端移動曼氛,然后直接清理掉端邊界以外的內(nèi)存豁辉。


標(biāo)記-整理

分代收集算法

當(dāng)前虛擬機的垃圾收集都采用分代收集算法。這種算法沒有什么新的思想舀患,只是根據(jù)對象存活周期的不同將內(nèi)存分為幾個塊徽级。一般java將堆分為新生代和老年代。這樣我們就可以根據(jù)各個年代的特點選擇合適的垃圾收集算法聊浅。
比如在新生代中餐抢,每次收集都會有大量對象死去,所以可以采用標(biāo)記-復(fù)制算法低匙。只需要付出少了對象的復(fù)制成本就可以完成每次的垃圾收集旷痕。而老年代的對象存活幾率是比較高的,而且沒有額外的空間對他們進行分配擔(dān)保顽冶,所以我們必須選擇標(biāo)記-清除或者標(biāo)記-整理算法進行垃圾回收欺抗。

垃圾收集器

如果說收集算法是內(nèi)存回收的方法論,那么垃圾收集器就是內(nèi)存回收的具體實現(xiàn)强重。
雖然我們對各個收集器進行比較绞呈,但是并不是要挑選出一個最好的收集器贸人。因為直到現(xiàn)在為止還沒有最好的垃圾收集器出現(xiàn),更沒有萬能的垃圾收集器佃声。我們能做到的就是根據(jù)具體的應(yīng)用場景選擇合適自己的垃圾收集器艺智。試想一下:如果有一種任何場景下都適用的完美收集器存在,那么HotSpot虛擬機就不會實現(xiàn)那么多不同的垃圾收集器了圾亏。

Serial收集器

Serial串行收集器是最基本力惯,即使最悠久的垃圾收集器了。這是一個單線程的收集器召嘶。它的“單線程”的意義不僅僅意味著它只會適用一條垃圾收集線程去完成垃圾收集工作父晶,更重要的是它在進行垃圾收集工作的時候必須暫停其它所有線程(Stop The World),直到它收集結(jié)束弄跌。

Serial收集器新生代采用標(biāo)記-復(fù)制算法甲喝,老年代采用標(biāo)記-整理算法。

image.png

虛擬機的設(shè)計者們當(dāng)然知道STW會帶來不良的用戶體驗铛只。所以在后續(xù)的垃圾收集器設(shè)計中停頓時間在不斷縮短(仍然還有停頓埠胖。目前是沒有不會停頓的)。
但是Serial收集器有一個優(yōu)于其他收集器的地方:它簡單而高效(與其他收集器的單線程相比)淳玩。Serial收集器由于沒有線程交互的開銷直撤,自然可以獲得很高的單線程收集效率。Serial收集器對于運行在Client模式下的虛擬機來說是個不錯的選擇蜕着。

ParNew收集器

ParNew收集器其實就是Serial收集器的多線程版本谋竖,除了使用多線程進行垃圾收集之外,其余行為(控制參數(shù)承匣,收集算法蓖乘,回收策略等)都是Serial收集器完全一樣。

image.png

他是許多運行在Server模式下的虛擬機首要選擇韧骗,除了Serial手機七萬嘉抒,只有它能和CMS收集器(真正意義上的并發(fā)收集器)配合工作。

并行和并發(fā)概念補充:

  • 并行:指多條垃圾收集線程并行工作袍暴,但是此用戶仍然處于等待狀態(tài)些侍。
  • 并發(fā):指用戶線程與垃圾收集線程同時執(zhí)行(不一定是并行,可能是交替執(zhí)行)政模,用戶程序在繼續(xù)運行岗宣,而垃圾收集器運行在另一個CPU上。

Parallel Scavenge收集器

Parallel Scavenge收集器也是標(biāo)記-復(fù)制算法的多線程收集器览徒,看上去和ParNew一樣狈定,但是他有個特別的地方:
Parallel Scavenge收集器關(guān)注點是吞吐量(高效率的利用CPU)。CMS等垃圾收集器的關(guān)注點更多是用戶線程的停頓時間(提高用戶體驗)。所謂吞吐量就是CPU中用于運行用戶代碼的時間和CPU總消耗時間的比值纽什。Parallel Scavenge收集器提供了很多參數(shù)供用戶找到最合適的停頓時間或者最大吞吐量措嵌。如果對于收集器不了解的手動優(yōu)化存在困難的時候,用Parallel Scavenge收集器配合自適應(yīng)調(diào)節(jié)策略芦缰,把內(nèi)存管理優(yōu)化交給虛擬機完成也是一個不錯的選擇企巢。

Parallel Scavenge新生代采用標(biāo)記-復(fù)制算法,老年代采用標(biāo)記-整理算法

這是JDK8默認的收集器让蕾。我們可以用指令查看:

java -XX:+PrintCommandLineFlags -version

JDK8默認Parallel Scavenge_old

Serial Old 收集器

Serial 收集器的老年代版本浪规,它同樣是一個單線程收集器。主要有兩大用途:一種用途是JDK1.5及其以前的版本中和Parllel Scavenge搭配使用探孝。另一種是作為CMS的后備方案笋婿。

Parallel Old收集器

Parallel Scavenge收集器的老年代版本。使用多線程和標(biāo)記-整理算法顿颅。在注重吞吐量和CPU資源的場合缸濒,都可以有限考慮Parallel Scavenge收集器 和 Parallel Old收集器.

CMS收集器

CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標(biāo)的收集器。它非常符合在注重用戶體驗的應(yīng)用上使用粱腻。

CMS收集器是HotSpot虛擬機第一款真正意義上的并發(fā)收集器庇配,它第一次實現(xiàn)了讓垃圾收集線程和用戶線程(基本上)同時工作。
從名字上Mark Sweep這兩個此可以看出CMS收集器是一種標(biāo)記-清除算法實現(xiàn)的绍些。它的運作過程比前幾種垃圾收集器更復(fù)雜一點捞慌。整個過程分為四種:

  • 初始標(biāo)記:暫停所有其他線程,記錄下直接與root相連的對象柬批,速度很快啸澡。
  • 并發(fā)標(biāo)記:同時開啟GC和用戶線程,用一個閉包結(jié)構(gòu)去記錄可達對象萝快。但是在這個階段結(jié)束锻霎,這個閉包結(jié)構(gòu)并不能保證包含當(dāng)前所有的可達對象著角。因為用戶線程可能會不斷的更新引用域揪漩。所以GC線程無法保證可達性分析的實時性。所以這個算法里會跟蹤記錄這些發(fā)生引用更新的地方吏口。
  • 重新標(biāo)記:重新標(biāo)記階段就是為了修正并發(fā)標(biāo)記期間因為用戶程序運行而導(dǎo)致標(biāo)記產(chǎn)生變動的那一部分對象的標(biāo)記記錄奄容。這個階段的停頓比初始標(biāo)記長,遠遠比并發(fā)標(biāo)記短产徊。
  • 并發(fā)清楚:開啟用戶線程昂勒,通知GC線程開始對未標(biāo)記的區(qū)域做清掃。
    CMS線程圖

    CMS是主要優(yōu)點:并發(fā)收集舟铜,低停頓戈盈。
    但是也有下面三個明顯的缺點:
  • 對CPU資源敏感
  • 無法處理浮動垃圾
  • 它使用的回收算法標(biāo)記-清除會導(dǎo)致收集結(jié)束時有大量的空間碎片產(chǎn)生

G1收集器

G1是一款面向服務(wù)器的垃圾收集器。主要針對配備多顆處理器以及大容量內(nèi)存的機器。以極高概率滿足GC停頓時間要求的同時塘娶,還具備高吞吐量性能特征归斤。
被視為JDK1.7中HotSpot虛擬機的一個重要進化特征,它具備以下特點:

  • 并行與并發(fā):G1能充分利用CPU刁岸,多核環(huán)境下的硬件優(yōu)勢脏里,使用多個CPU(CPU或者CPU核心)來縮短STW停頓時間。部分其他收集器原本需要停頓java線程執(zhí)行GC操作虹曙,G1收集器仍然可以通過并發(fā)的方式讓java線程繼續(xù)執(zhí)行迫横。
  • 分代收集:雖然G1可以不需要其他收集器配合就能獨立管理整個GC堆,但是還是保留了分代的概念酝碳。
  • 空間整合:與CMS的標(biāo)記-清理不同矾踱,C1從整體上看是基于標(biāo)記-整理算法實現(xiàn)的收集器,從局部上看是基于標(biāo)記-復(fù)制算法實現(xiàn)的疏哗。
  • 可預(yù)測的停頓:這個G1相比于CMS的另一個大優(yōu)勢介返,降低停頓時間是G1和CMS的共同關(guān)注點,但是G1除了追求低停頓之外沃斤,還能建立可預(yù)測的停頓時間模型圣蝎,能讓使用者明確指定在一個長度M毫秒的時間段內(nèi)。

G1收集器運作大致分為下面幾個步驟:

  • 初始標(biāo)記
  • 并發(fā)標(biāo)記
  • 最終標(biāo)記
  • 篩選回收

G1收集器在后臺維護了一個優(yōu)先列表衡瓶,每次根據(jù)允許的時間優(yōu)先回收價值最大的Region徘公,這種使用Region劃分內(nèi)存空間以及優(yōu)先級的區(qū)域回收方式,保證了G1收集器在有限的時間內(nèi)盡可能高的收集率(把內(nèi)存化整為零)哮针。
比如一個Region預(yù)計100ms回收20M垃圾关面,另一個Region預(yù)計10ms回收100M垃圾,那么10ms100M的這個就會優(yōu)先回收十厢。

ZGC收集器

ZGC(The Z Garbage Collector)是JDK11推出的一款實驗性的低延遲垃圾回收器等太。設(shè)計目標(biāo)如下:

  • 停頓時間不超過10ms
  • 停頓時間不會隨著堆大小或者活躍對象的大小而增加
  • 支持8MB-4TB級別的堆

從設(shè)計目標(biāo)上看ZGC適用于大內(nèi)存低延服務(wù)的內(nèi)存管理。其實在極度追求用戶體驗的情況下蛮放,不管是CMS還是G1都會有所不足缩抡。感興趣的可以去看下美團團隊分享的關(guān)于ZGC的介紹,內(nèi)容比較多包颁,我就不搬運了~附上鏈接:新一代垃圾回收器ZGC的探索與實踐

本篇筆記就記到這里瞻想,如果稍微幫到你了記得點個喜歡點個關(guān)注,也祝大家工作順順利利娩嚼,每天進步一點點~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蘑险,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子岳悟,更是在濱河造成了極大的恐慌佃迄,老刑警劉巖泼差,帶你破解...
    沈念sama閱讀 218,546評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異呵俏,居然都是意外死亡拴驮,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,224評論 3 395
  • 文/潘曉璐 我一進店門柴信,熙熙樓的掌柜王于貴愁眉苦臉地迎上來套啤,“玉大人,你說我怎么就攤上這事随常∏甭伲” “怎么了?”我有些...
    開封第一講書人閱讀 164,911評論 0 354
  • 文/不壞的土叔 我叫張陵绪氛,是天一觀的道長唆鸡。 經(jīng)常有香客問我,道長枣察,這世上最難降的妖魔是什么争占? 我笑而不...
    開封第一講書人閱讀 58,737評論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮序目,結(jié)果婚禮上臂痕,老公的妹妹穿的比我還像新娘。我一直安慰自己猿涨,他們只是感情好握童,可當(dāng)我...
    茶點故事閱讀 67,753評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著叛赚,像睡著了一般澡绩。 火紅的嫁衣襯著肌膚如雪揣非。 梳的紋絲不亂的頭發(fā)上丁稀,一...
    開封第一講書人閱讀 51,598評論 1 305
  • 那天,我揣著相機與錄音告材,去河邊找鬼事镣。 笑死步鉴,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的蛮浑。 我是一名探鬼主播唠叛,決...
    沈念sama閱讀 40,338評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼沮稚!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起册舞,我...
    開封第一講書人閱讀 39,249評論 0 276
  • 序言:老撾萬榮一對情侶失蹤蕴掏,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體盛杰,經(jīng)...
    沈念sama閱讀 45,696評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡挽荡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,888評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了即供。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片定拟。...
    茶點故事閱讀 40,013評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖逗嫡,靈堂內(nèi)的尸體忽然破棺而出青自,到底是詐尸還是另有隱情,我是刑警寧澤驱证,帶...
    沈念sama閱讀 35,731評論 5 346
  • 正文 年R本政府宣布延窜,位于F島的核電站,受9級特大地震影響抹锄,放射性物質(zhì)發(fā)生泄漏逆瑞。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,348評論 3 330
  • 文/蒙蒙 一伙单、第九天 我趴在偏房一處隱蔽的房頂上張望获高。 院中可真熱鬧,春花似錦吻育、人聲如沸谋减。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,929評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽出爹。三九已至,卻和暖如春缎除,著一層夾襖步出監(jiān)牢的瞬間严就,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,048評論 1 270
  • 我被黑心中介騙來泰國打工器罐, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留梢为,地道東北人。 一個月前我還...
    沈念sama閱讀 48,203評論 3 370
  • 正文 我出身青樓轰坊,卻偏偏與公主長得像铸董,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子肴沫,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,960評論 2 355

推薦閱讀更多精彩內(nèi)容