日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

JVM2:垃圾收集器与内存分配策略

發(fā)布時(shí)間:2024/1/8 编程问答 26 豆豆
生活随笔 收集整理的這篇文章主要介紹了 JVM2:垃圾收集器与内存分配策略 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

垃圾收集器與內(nèi)存分配策略

文章目錄

  • 垃圾收集器與內(nèi)存分配策略
    • 對(duì)象回收
      • 引用計(jì)數(shù)算法
      • 可達(dá)性分析算法
      • 四種引用類型
      • 生存與死亡
      • 回收方法區(qū)
    • 垃圾收集算法
      • 標(biāo)記清除法
      • 復(fù)制算法
      • 標(biāo)記-整理算法
    • HotSpot的算法細(xì)節(jié)實(shí)現(xiàn)
      • 根節(jié)點(diǎn)枚舉
      • 安全點(diǎn)
      • 安全區(qū)域
      • 記憶集
      • 寫屏障
      • 并發(fā)的可達(dá)性分析
    • 經(jīng)典的垃圾收集器
      • Serial收集器
      • ParNew收集器
      • Parallel Scavenge收集器
      • Serial Old收集器
      • Parallel Old收集器
      • CMS收集器
      • Garbage First收集器(G1收集器)

對(duì)象回收

引用計(jì)數(shù)算法

在對(duì)象中添加一個(gè)引用計(jì)數(shù)器,每當(dāng)有一個(gè)地方引用它時(shí),計(jì)數(shù)器值就加一;當(dāng)引用失效時(shí),計(jì)數(shù)器值就減一;任何時(shí)刻計(jì)數(shù)器為零的對(duì)象就是不可能再被使用的。

存在問題

單純的引用計(jì)數(shù)就很難解決對(duì)象之間相互循環(huán)引用的問題。

可達(dá)性分析算法

這個(gè)算法的基本思路就是通過一系列稱為“GC Roots”的根對(duì)象作為起始節(jié)點(diǎn)集,從這些節(jié)點(diǎn)開始,根據(jù)引用關(guān)系向下搜索,搜索過程所走過的路徑稱為“引用鏈”(Reference Chain),如果某個(gè)對(duì)象到GC Roots間沒有任何引用鏈相連,或者用圖論的話來說就是從GC Roots到這個(gè)對(duì)象不可達(dá)時(shí),則證明此對(duì)象是不可能再被使用的。

可作為GC Roots的對(duì)象包括以下幾種:

  • 在虛擬機(jī)棧(棧幀中的本地變量表)中引用的對(duì)象,譬如各個(gè)線程被調(diào)用的方法堆棧中使用到的參數(shù)、局部變量、臨時(shí)變量等。
  • 在方法區(qū)中類靜態(tài)屬性引用的對(duì)象,譬如Java類的引用類型靜態(tài)變量。
  • 在方法區(qū)中常量引用的對(duì)象,譬如字符串常量池(String Table)里的引用。
  • 在本地方法棧中JNI(即通常所說的Native方法)引用的對(duì)象。
  • Java虛擬機(jī)內(nèi)部的引用,如基本數(shù)據(jù)類型對(duì)應(yīng)的Class對(duì)象,一些常駐的異常對(duì)象(比如NullPointExcepiton、OutOfMemoryError)等,還有系統(tǒng)類加載器。
  • 所有被同步鎖(synchronized關(guān)鍵字)持有的對(duì)象。
  • 反映Java虛擬機(jī)內(nèi)部情況的JMXBean、JVMTI中注冊(cè)的回調(diào)、本地代碼緩存等。
  • 四種引用類型

  • 強(qiáng)引用是最傳統(tǒng)的“引用”的定義,是指在程序代碼之中普遍存在的引用賦值,即類似“Objectobj=new Object()”這種引用關(guān)系。無論任何情況下,只要強(qiáng)引用關(guān)系還存在,垃圾收集器就永遠(yuǎn)不會(huì)回收掉被引用的對(duì)象。
  • 軟引用是用來描述一些還有用,但非必須的對(duì)象。只被軟引用關(guān)聯(lián)著的對(duì)象,在系統(tǒng)將要發(fā)生內(nèi)存溢出異常前,會(huì)把這些對(duì)象列進(jìn)回收范圍之中進(jìn)行第二次回收,如果這次回收還沒有足夠的內(nèi)存,才會(huì)拋出內(nèi)存溢出異常。在JDK 1.2版之后提供了SoftReference類來實(shí)現(xiàn)軟引用。
  • 弱引用也是用來描述那些非必須對(duì)象,但是它的強(qiáng)度比軟引用更弱一些,被弱引用關(guān)聯(lián)的對(duì)象只能生存到下一次垃圾收集發(fā)生為止。當(dāng)垃圾收集器開始工作,無論當(dāng)前內(nèi)存是否足夠,都會(huì)回收掉只被弱引用關(guān)聯(lián)的對(duì)象。在JDK 1.2版之后提供了WeakReference類來實(shí)現(xiàn)弱引用。
  • 虛引用也稱為“幽靈引用”或者“幻影引用”,它是最弱的一種引用關(guān)系。一個(gè)對(duì)象是否有虛引用的存在,完全不會(huì)對(duì)其生存時(shí)間構(gòu)成影響,也無法通過虛引用來取得一個(gè)對(duì)象實(shí)例。為一個(gè)對(duì)象設(shè)置虛引用關(guān)聯(lián)的唯一目的只是為了能在這個(gè)對(duì)象被收集器回收時(shí)收到一個(gè)系統(tǒng)通知。在JDK 1.2版之后提供了PhantomReference類來實(shí)現(xiàn)虛引用。
  • 生存與死亡

    即使在可達(dá)性分析算法中判定為不可達(dá)的對(duì)象,也不是“非死不可”的,這時(shí)候它們暫時(shí)還處于“緩刑”階段,**要真正宣告一個(gè)對(duì)象死亡,至少要經(jīng)歷兩次標(biāo)記過程:**如果對(duì)象在進(jìn)行可達(dá)性分析后發(fā)現(xiàn)沒有與GC Roots相連接的引用鏈,那它將會(huì)被第一次標(biāo)記,隨后進(jìn)行一次篩選,篩選的條件是此對(duì)象是否有必要執(zhí)行finalize()方法。假如對(duì)象沒有覆蓋finalize()方法,或者finalize()方法已經(jīng)被虛擬機(jī)調(diào)用過,那么虛擬機(jī)將這兩種情況都視為“沒有必要執(zhí)行”。

    **如果這個(gè)對(duì)象被判定為確有必要執(zhí)行finalize()方法,那么該對(duì)象將會(huì)被放置在一個(gè)名為F-Queue的隊(duì)列之中,**并在稍后由一條由虛擬機(jī)自動(dòng)建立的、低調(diào)度優(yōu)先級(jí)的Finalizer線程去執(zhí)行它們的finalize()方法。finalize()方法是對(duì)象逃脫死亡命運(yùn)的最后一次機(jī)會(huì),稍后收集器將對(duì)F-Queue中的對(duì)象進(jìn)行第二次小規(guī)模的標(biāo)記,==如果對(duì)象要在finalize()中成功拯救自己——只要重新與引用鏈上的任何一個(gè)對(duì)象建立關(guān)聯(lián)即可,譬如把自己(this關(guān)鍵字)賦值給某個(gè)類變量或者對(duì)象的成員變量,那在第二次標(biāo)記時(shí)它將被移出“即將回收”的集合;如果對(duì)象這時(shí)候還沒有逃脫,那基本上它就真的要被回收了。==從代碼清單中我們可以看到一個(gè)對(duì)象的finalize()被執(zhí)行,但是它仍然可以存活。

    /*** 此代碼演示了兩點(diǎn):* 1.對(duì)象可以在被GC時(shí)自我拯救。* 2.這種自救的機(jī)會(huì)只有一次,因?yàn)橐粋€(gè)對(duì)象的finalize()方法最多只會(huì)被系統(tǒng)自動(dòng)調(diào)用一次* @author: Mr.O* @create: 2020-11-01 15:00**/ public class FinalizeEscapeGC {public static FinalizeEscapeGC SAVE_HOOK = null;public void isAlive() {System.out.println("yes, i am still alive :)");}@Overrideprotected void finalize() throws Throwable {super.finalize();System.out.println("finalize method executed!");FinalizeEscapeGC.SAVE_HOOK = this;}public static void main(String[] args) throws Throwable {SAVE_HOOK = new FinalizeEscapeGC();//對(duì)象第一次成功拯救自己SAVE_HOOK = null;System.gc();// 因?yàn)镕inalizer方法優(yōu)先級(jí)很低,暫停0.5秒,以等待它Thread.sleep(500);if (SAVE_HOOK != null) {SAVE_HOOK.isAlive();} else {System.out.println("no, i am dead :(");}// 下面這段代碼與上面的完全相同,但是這次自救卻失敗了SAVE_HOOK = null;System.gc();// 因?yàn)镕inalizer方法優(yōu)先級(jí)很低,暫停0.5秒,以等待它Thread.sleep(500);if (SAVE_HOOK != null) {SAVE_HOOK.isAlive();} else {System.out.println("no, i am dead :(");}} }

    從代碼清單3-2的運(yùn)行結(jié)果可以看到,SAVE_HOOK對(duì)象的finalize()方法確實(shí)被垃圾收集器觸發(fā)過,并且在被收集前成功逃脫了

    另外一個(gè)值得注意的地方就是,代碼中有兩段完全一樣的代碼片段,執(zhí)行結(jié)果卻是一次逃脫成功,一次失敗了。這是因?yàn)槿魏我粋€(gè)對(duì)象的finalize()方法都只會(huì)被系統(tǒng)自動(dòng)調(diào)用一次,如果對(duì)象面臨下一次回收,它的finalize()方法不會(huì)被再次執(zhí)行,因此第二段代碼的自救行動(dòng)失敗了。

    回收方法區(qū)

    方法區(qū)的垃圾收集主要回收兩部分內(nèi)容:廢棄的常量和不再使用的類型?;厥諒U棄常量與回收J(rèn)ava堆中的對(duì)象非常類似。舉個(gè)常量池中字面量回收的例子,假如一個(gè)字符串“java”曾經(jīng)進(jìn)入常量池中,但是當(dāng)前系統(tǒng)又沒有任何一個(gè)字符串對(duì)象的值是“java”,換句話說,已經(jīng)沒有任何字符串對(duì)象引用常量池中的“java”常量,且虛擬機(jī)中也沒有其他地方引用這個(gè)字面量。如果在這時(shí)發(fā)生內(nèi)存回收,而且垃圾收集器判斷確有必要的話,這個(gè)“java”常量就將會(huì)被系統(tǒng)清理出常量池。常量池中其他類(接口)、方法、字段的符號(hào)引用也與此類似。


    判定一個(gè)常量是否“廢棄”還是相對(duì)簡單,而要判定一個(gè)類型是否屬于“不再被使用的類”的條件就比較苛刻了。需要同時(shí)滿足下面三個(gè)條件:

  • 該類所有的實(shí)例都已經(jīng)被回收,也就是Java堆中不存在該類及其任何派生子類的實(shí)例。
  • 加載該類的類加載器已經(jīng)被回收,這個(gè)條件除非是經(jīng)過精心設(shè)計(jì)的可替換類加載器的場景,如OSGi、JSP的重加載等,否則通常是很難達(dá)成的。
  • 該類對(duì)應(yīng)的java.lang.Class對(duì)象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。
  • HotSpot虛擬機(jī)提供了-Xnoclassgc參數(shù)進(jìn)行控制,還可以使用-verbose:class以及-XX:+TraceClass-Loading、-XX:+TraceClassUnLoading查看類加載和卸載信息

    垃圾收集算法

  • 部分收集(Partial GC):指目標(biāo)不是完整收集整個(gè)Java堆的垃圾收集,其中又分為:
  • ■新生代收集(Minor GC/Young GC):指目標(biāo)只是新生代的垃圾收集。

    ■老年代收集(Major GC/Old GC):指目標(biāo)只是老年代的垃圾收集。目前只有CMS收集器會(huì)有單獨(dú)收集老年代的行為。另外請(qǐng)注意“Major GC”這個(gè)說法現(xiàn)在有點(diǎn)混淆,在不同資料上常有不同所指,讀者需按上下文區(qū)分到底是指老年代的收集還是整堆收集。

    ■混合收集(Mixed GC):指目標(biāo)是收集整個(gè)新生代以及部分老年代的垃圾收集。目前只有G1收集器會(huì)有這種行為。

  • 整堆收集(Full GC):收集整個(gè)Java堆和方法區(qū)的垃圾收集。
  • 標(biāo)記清除法

    算法分為“標(biāo)記”和“清除”兩個(gè)階段:首先標(biāo)記出所有需要回收的對(duì)象,在標(biāo)記完成后,統(tǒng)一回收掉所有被標(biāo)記的對(duì)象,也可以反過來,標(biāo)記存活的對(duì)象,統(tǒng)一回收所有未被標(biāo)記的對(duì)象。

    它的主要缺點(diǎn)有兩個(gè):

  • 第一個(gè)是執(zhí)行效率不穩(wěn)定,如果Java堆中包含大量對(duì)象,而且其中大部分是需要被回收的,這時(shí)必須進(jìn)行大量標(biāo)記和清除的動(dòng)作,導(dǎo)致標(biāo)記和清除兩個(gè)過程的執(zhí)行效率都隨對(duì)象數(shù)量增長而降低;
  • 第二個(gè)是內(nèi)存空間的碎片化問題,標(biāo)記、清除之后會(huì)產(chǎn)生大量不連續(xù)的內(nèi)存碎片,空間碎片太多可能會(huì)導(dǎo)致當(dāng)以后在程序運(yùn)行過程中需要分配較大對(duì)象時(shí)無法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾收集動(dòng)作。
  • 復(fù)制算法

    它將可用內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當(dāng)這一塊的內(nèi)存用完了,就將還存活著的對(duì)象復(fù)制到另外一塊上面,然后再把已使用過的內(nèi)存空間一次清理掉。如果內(nèi)存中多數(shù)對(duì)象都是存活的,這種算法將會(huì)產(chǎn)生大量的內(nèi)存間復(fù)制的開銷,但對(duì)于多數(shù)對(duì)象都是可回收的情況,算法需要復(fù)制的就是占少數(shù)的存活對(duì)象,而且每次都是針對(duì)整個(gè)半?yún)^(qū)進(jìn)行內(nèi)存回收,分配內(nèi)存時(shí)也就不用考慮有空間碎片的復(fù)雜情況,只要移動(dòng)堆頂指針,按順序分配即可。這樣實(shí)現(xiàn)簡單,運(yùn)行高效,不過其缺陷也顯而易見,這種復(fù)制回收算法的代價(jià)是將可用內(nèi)存縮小為了原來的一半,空間浪費(fèi)未免太多了一點(diǎn)。

    HotSpot虛擬機(jī)默認(rèn)Eden和Survivor的大小比例是8∶1

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

    標(biāo)記過程仍然與“標(biāo)記-清除”算法一樣,但后續(xù)步驟不是直接對(duì)可回收對(duì)象進(jìn)行清理,而是讓所有存活的對(duì)象都向內(nèi)存空間一端移動(dòng),然后直接清理掉邊界以外的內(nèi)存,“標(biāo)記-整理”算法的示意圖如圖

    如果移動(dòng)存活對(duì)象,尤其是在老年代這種每次回收都有大量對(duì)象存活區(qū)域,移動(dòng)存活對(duì)象并更新所有引用這些對(duì)象的地方將會(huì)是一種極為負(fù)重的操作,而且這種對(duì)象移動(dòng)操作必須全程暫停用戶應(yīng)用程序才能進(jìn)行,這就更加讓使用者不得不小心翼翼地權(quán)衡其弊端了,像這樣的停頓被最初的虛擬機(jī)設(shè)計(jì)者形象地描述為“Stop The World”。

    HotSpot的算法細(xì)節(jié)實(shí)現(xiàn)

    根節(jié)點(diǎn)枚舉

    我們以可達(dá)性分析算法中從GC Roots集合找引用鏈這個(gè)操作作為介紹虛擬機(jī)高效實(shí)現(xiàn)的第一個(gè)例子。固定可作為GC Roots的節(jié)點(diǎn)主要在全局性的引用(例如常量或類靜態(tài)屬性)與執(zhí)行上下文(例如棧幀中的本地變量表)中,盡管目標(biāo)明確,但查找過程要做到高效并非一件容易的事情,現(xiàn)在Java應(yīng)用越做越龐大,光是方法區(qū)的大小就常有數(shù)百上千兆,里面的類、常量等更是恒河沙數(shù),若要逐個(gè)檢查以這里為起源的引用肯定得消耗不少時(shí)間。

    迄今為止,所有收集器在根節(jié)點(diǎn)枚舉這一步驟時(shí)都是必須暫停用戶線程的,因此毫無疑問根節(jié)點(diǎn)枚舉與之前提及的整理內(nèi)存碎片一樣會(huì)面臨相似的“Stop The World”的困擾。

    即使是號(hào)稱停頓時(shí)間可控,或者(幾乎)不會(huì)發(fā)生停頓的CMS、G1、ZGC等收集器,枚舉根節(jié)點(diǎn)時(shí)也是必須要停頓的。

    **當(dāng)用戶線程停頓下來之后,其實(shí)并不需要一個(gè)不漏地檢查完所有執(zhí)行上下文和全局的引用位置,虛擬機(jī)應(yīng)當(dāng)是有辦法直接得到哪些地方存放著對(duì)象引用的。在HotSpot的解決方案里,是使用一組稱為OopMap的數(shù)據(jù)結(jié)構(gòu)來達(dá)到這個(gè)目的。**一旦類加載動(dòng)作完成的時(shí)候,HotSpot就會(huì)把對(duì)象內(nèi)什么偏移量上是什么類型的數(shù)據(jù)計(jì)算出來,在即時(shí)編譯過程中,也會(huì)在特定的位置記錄下棧里和寄存器里哪些位置是引用。這樣收集器在掃描時(shí)就可以直接得知這些信息了,并不需要真正一個(gè)不漏地從方法區(qū)等GC Roots開始查找。

    安全點(diǎn)

    實(shí)際上HotSpot也的確沒有為每條指令都生成OopMap,前面已經(jīng)提到,只是在“特定的位置”記錄了這些信息,這些位置被稱為安全點(diǎn)(Safepoint)。有了安全點(diǎn)的設(shè)定,也就決定了用戶程序執(zhí)行時(shí)并非在代碼指令流的任意位置都能夠停頓下來開始垃圾收集,而是強(qiáng)制要求必須執(zhí)行到達(dá)安全點(diǎn)后才能夠暫停。

    安全點(diǎn)的選定既不能太少以至于讓收集器等待時(shí)間過長,也不能太過頻繁以至于過分增大運(yùn)行時(shí)的內(nèi)存負(fù)荷。

    安全區(qū)域

    安全區(qū)域是指能夠確保在某一段代碼片段之中,引用關(guān)系不會(huì)發(fā)生變化,因此,在這個(gè)區(qū)域中任意地方開始垃圾收集都是安全的。我們也可以把安全區(qū)域看作被擴(kuò)展拉伸了的安全點(diǎn)。

    記憶集

    提到了為解決對(duì)象跨代引用所帶來的問題,垃圾收集器在新生代中建立了名為記憶集(Remembered Set)的數(shù)據(jù)結(jié)構(gòu),用以避免把整個(gè)老年代加進(jìn)GC Roots掃描范圍。事實(shí)上并不只是新生代、老年代之間才有跨代引用的問題,所有涉及部分區(qū)域收集(Partial GC)行為的垃圾收集器,典型的如G1、ZGC和Shenandoah收集器,都會(huì)面臨相同的問題

    寫屏障

    我們已經(jīng)解決了如何使用記憶集來縮減GC Roots掃描范圍的問題,但還沒有解決卡表元素如何維護(hù)的問題,例如它們何時(shí)變臟、誰來把它們變臟等。

    卡表元素何時(shí)變臟的答案是很明確的——有其他分代區(qū)域中對(duì)象引用了本區(qū)域?qū)ο髸r(shí),其對(duì)應(yīng)的卡表元素就應(yīng)該變臟,變臟時(shí)間點(diǎn)原則上應(yīng)該發(fā)生在引用類型字段賦值的那一刻。

    應(yīng)用寫屏障后,虛擬機(jī)就會(huì)為所有賦值操作生成相應(yīng)的指令,一旦收集器在寫屏障中增加了更新卡表操作,無論更新的是不是老年代對(duì)新生代對(duì)象的引用,每次只要對(duì)引用進(jìn)行更新,就會(huì)產(chǎn)生額外的開銷,不過這個(gè)開銷與Minor GC時(shí)掃描整個(gè)老年代的代價(jià)相比還是低得多的。

    在JDK 7之后,HotSpot虛擬機(jī)增加了一個(gè)新的參數(shù)-XX:+UseCondCardMark,用來決定是否開啟卡表更新的條件判斷。開啟會(huì)增加一次額外判斷的開銷,但能夠避免偽共享問題,兩者各有性能損耗,是否打開要根據(jù)應(yīng)用實(shí)際運(yùn)行情況來進(jìn)行測試權(quán)衡。

    并發(fā)的可達(dá)性分析

    當(dāng)前主流編程語言的垃圾收集器基本上都是依靠可達(dá)性分析算法來判定對(duì)象是否存活的,可達(dá)性分析算法理論上要求全過程都基于一個(gè)能保障一致性的快照中才能夠進(jìn)行分析

    可從GC Roots再繼續(xù)往下遍歷對(duì)象圖,這一步驟的停頓時(shí)間就必定會(huì)與Java堆容量直接成正比例關(guān)系了:堆越大,存儲(chǔ)的對(duì)象越多,對(duì)象圖結(jié)構(gòu)越復(fù)雜,要標(biāo)記更多對(duì)象而產(chǎn)生的停頓時(shí)間自然就更長

    想解決或者降低用戶線程的停頓,就要先搞清楚為什么必須在一個(gè)能保障一致性的快照上才能進(jìn)行對(duì)象圖的遍歷?為了能解釋清楚這個(gè)問題,我們引入三色標(biāo)記

  • 白色:表示對(duì)象尚未被垃圾收集器訪問過。顯然在可達(dá)性分析剛剛開始的階段,所有的對(duì)象都是白色的,若在分析結(jié)束的階段,仍然是白色的對(duì)象,即代表不可達(dá)。
  • 黑色:表示對(duì)象已經(jīng)被垃圾收集器訪問過,且這個(gè)對(duì)象的所有引用都已經(jīng)掃描過。黑色的對(duì)象代表已經(jīng)掃描過,它是安全存活的,如果有其他對(duì)象引用指向了黑色對(duì)象,無須重新掃描一遍。黑色對(duì)象不可能直接(不經(jīng)過灰色對(duì)象)指向某個(gè)白色對(duì)象。
  • 灰色:表示對(duì)象已經(jīng)被垃圾收集器訪問過,但這個(gè)對(duì)象上至少存在一個(gè)引用還沒有被掃描過。
  • Wilson于1994年在理論上證明了,當(dāng)且僅當(dāng)以下兩個(gè)條件同時(shí)滿足時(shí),會(huì)產(chǎn)生“對(duì)象消失”的問題,即原本應(yīng)該是黑色的對(duì)象被誤標(biāo)為白色:

  • 賦值器插入了一條或多條從黑色對(duì)象到白色對(duì)象的新引用;
  • 賦值器刪除了全部從灰色對(duì)象到該白色對(duì)象的直接或間接引用。
  • 因此,我們要解決并發(fā)掃描時(shí)的對(duì)象消失問題,只需破壞這兩個(gè)條件的任意一個(gè)即可。由此分別產(chǎn)生了兩種解決方案:增量更新(Incremental Update)和原始快照(Snapshot At The Beginning,SATB)。

    增量更新要破壞的是第一個(gè)條件,當(dāng)黑色對(duì)象插入新的指向白色對(duì)象的引用關(guān)系時(shí),就將這個(gè)新插入的引用記錄下來,等并發(fā)掃描結(jié)束之后,再將這些記錄過的引用關(guān)系中的黑色對(duì)象為根,重新掃描一次。這可以簡化理解為,黑色對(duì)象一旦新插入了指向白色對(duì)象的引用之后,它就變回灰色對(duì)象了。

    原始快照要破壞的是第二個(gè)條件,當(dāng)灰色對(duì)象要?jiǎng)h除指向白色對(duì)象的引用關(guān)系時(shí),就將這個(gè)要?jiǎng)h除的引用記錄下來,在并發(fā)掃描結(jié)束之后,再將這些記錄過的引用關(guān)系中的灰色對(duì)象為根,重新掃描一次。這也可以簡化理解為,無論引用關(guān)系刪除與否,都會(huì)按照剛剛開始掃描那一刻的對(duì)象圖快照來進(jìn)行搜索。

    經(jīng)典的垃圾收集器

    這里垃圾收集器的算法筆記比較粗略,ZGC等收集器暫時(shí)不在這里做筆記了(由于水平有限,自己暫時(shí)也沒弄太明白),如果想要弄清楚,最好參考原書

    如果兩個(gè)收集器之間存在連線,就說明它們可以搭配使用,圖中收集器所處的區(qū)域,則表示它是屬于新生代收集器抑或是老年代收集器。

    Serial收集器

    大家只看名字就能夠猜到,這個(gè)收集器是一個(gè)單線程工作的收集器,但它的“單線程”的意義并不僅僅是說明它只會(huì)使用一個(gè)處理器或一條收集線程去完成垃圾收集工作,更重要的是強(qiáng)調(diào)在它進(jìn)行垃圾收集時(shí),必須暫停其他所有工作線程,直到它收集結(jié)束。

    它依然是HotSpot虛擬機(jī)運(yùn)行在客戶端模式下的默認(rèn)新生代收集器,有著優(yōu)于其他收集器的地方,那就是簡單而高效(與其他收集器的單線程相比),對(duì)于內(nèi)存資源受限的環(huán)境,它是所有收集器里額外內(nèi)存消耗(Memory Footprint)[1]最小的;對(duì)于單核處理器或處理器核心數(shù)較少的環(huán)境來說,**Serial收集器由于沒有線程交互的開銷,專心做垃圾收集自然可以獲得最高的單線程收集效率。**在用戶桌面的應(yīng)用場景以及近年來流行的部分微服務(wù)應(yīng)用中,分配給虛擬機(jī)管理的內(nèi)存一般來說并不會(huì)特別大,收集幾十兆甚至一兩百兆的新生代(僅僅是指新生代使用的內(nèi)存,桌面應(yīng)用甚少超過這個(gè)容量),垃圾收集的停頓時(shí)間完全可以控制在十幾、幾十毫秒,最多一百多毫秒以內(nèi),只要不是頻繁發(fā)生收集,這點(diǎn)停頓時(shí)間對(duì)許多用戶來說是完全可以接受的。

    ParNew收集器

    ParNew收集器實(shí)質(zhì)上是Serial收集器的多線程并行版本,除了同時(shí)使用多條線程進(jìn)行垃圾收集之外,其余的行為包括Serial收集器可用的所有控制參數(shù)(例如:-XX:SurvivorRatio、-XX:PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集算法、Stop The World、對(duì)象分配規(guī)則、回收策略等都與Serial收集器完全一致

    在JDK 5中使用CMS來收集老年代的時(shí)候,新生代只能選擇ParNew或者Serial收集器中的一個(gè)。ParNew收集器是激活CMS后(使用-XX:+UseConcMarkSweepGC選項(xiàng))的默認(rèn)新生代收集器,也可以使用-XX:+/-UseParNewGC選項(xiàng)來強(qiáng)制指定或者禁用它。

    Parallel Scavenge收集器

    Parallel Scavenge收集器也是一款新生代收集器,它同樣是基于標(biāo)記-復(fù)制算法實(shí)現(xiàn)的收集器,也是能夠并行收集的多線程收集器……Parallel Scavenge的諸多特性從表面上看和ParNew非常相似

    Parallel Scavenge收集器的特點(diǎn)是它的關(guān)注點(diǎn)與其他收集器不同,CMS等收集器的關(guān)注點(diǎn)是盡可能地縮短垃圾收集時(shí)用戶線程的停頓時(shí)間,而Parallel Scavenge收集器的目標(biāo)則是達(dá)到一個(gè)可控制的吞吐量(Throughput)。

    Parallel Scavenge收集器提供了兩個(gè)參數(shù)用于精確控制吞吐量,分別是控制最大垃圾收集停頓時(shí)間的-XX:MaxGCPauseMillis參數(shù)以及直接設(shè)置吞吐量大小的-XX:GCTimeRatio參數(shù)。

    Serial Old收集器

    Serial Old是Serial收集器的老年代版本,它同樣是一個(gè)單線程收集器,使用標(biāo)記-整理算法。

    Parallel Old收集器

    Parallel Old是Parallel Scavenge收集器的老年代版本,支持多線程并發(fā)收集,基于標(biāo)記-整理算法實(shí)現(xiàn)。

    直到Parallel Old收集器出現(xiàn)后,“吞吐量優(yōu)先”收集器終于有了比較名副其實(shí)的搭配組合,在注重吞吐量或者處理器資源較為稀缺的場合,都可以優(yōu)先考慮Parallel Scavenge加Parallel Old收集器這個(gè)組合。

    CMS收集器

    CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時(shí)間為目標(biāo)的收集器。這類應(yīng)用通常都會(huì)較為關(guān)注服務(wù)的響應(yīng)速度,希望系統(tǒng)停頓時(shí)間盡可能短,以給用戶帶來良好的交互體驗(yàn)。CMS收集器就非常符合這類應(yīng)用的需求。

    從名字(包含“Mark Sweep”)上就可以看出CMS收集器是基于標(biāo)記-清除算法實(shí)現(xiàn)的,它的運(yùn)作過程相對(duì)于前面幾種收集器來說要更復(fù)雜一些,整個(gè)過程分為四個(gè)步驟,包括:

    1)初始標(biāo)記(CMS initial mark)

    2)并發(fā)標(biāo)記(CMS concurrent mark)

    3)重新標(biāo)記(CMS remark)

    4)并發(fā)清除(CMS concurrent sweep)

    其中初始標(biāo)記、重新標(biāo)記這兩個(gè)步驟仍然需要“Stop The World”。

  • 初始標(biāo)記僅僅只是標(biāo)記一下GCRoots能直接關(guān)聯(lián)到的對(duì)象,速度很快
  • 并發(fā)標(biāo)記階段就是從GC Roots的直接關(guān)聯(lián)對(duì)象開始遍歷整個(gè)對(duì)象圖的過程,這個(gè)過程耗時(shí)較長但是不需要停頓用戶線程,可以與垃圾收集線程一起并發(fā)運(yùn)行
  • 重新標(biāo)記階段則是為了修正并發(fā)標(biāo)記期間,因用戶程序繼續(xù)運(yùn)作而導(dǎo)致標(biāo)記產(chǎn)生變動(dòng)的那一部分對(duì)象的標(biāo)記記錄,這個(gè)階段的停頓時(shí)間通常會(huì)比初始標(biāo)記階段稍長一些,但也遠(yuǎn)比并發(fā)標(biāo)記階段的時(shí)間短
  • 并發(fā)清除階段,清理刪除掉標(biāo)記階段判斷的已經(jīng)死亡的對(duì)象,由于不需要移動(dòng)存活對(duì)象,所以這個(gè)階段也是可以與用戶線程同時(shí)并發(fā)的。

  • ==CMS優(yōu)點(diǎn):==并發(fā)收集、低停頓,一些官方公開文檔里面也稱之為“并發(fā)低停頓收集器”(Concurrent Low Pause Collector)。

    CMS缺點(diǎn):

  • CMS收集器對(duì)處理器資源非常敏感。面向并發(fā)設(shè)計(jì)的程序都對(duì)處理器資源比較敏感。在并發(fā)階段,它雖然不會(huì)導(dǎo)致用戶線程停頓,但卻會(huì)因?yàn)檎加昧艘徊糠志€程(或者說處理器的計(jì)算能力)而導(dǎo)致應(yīng)用程序變慢,降低總吞吐量。
  • CMS收集器無法處理“浮動(dòng)垃圾”(Floating Garbage),有可能出現(xiàn)“Con-current ModeFailure”失敗進(jìn)而導(dǎo)致另一次完全“Stop The World”的Full GC的產(chǎn)生。在CMS的并發(fā)標(biāo)記和并發(fā)清理階段,用戶線程是還在繼續(xù)運(yùn)行的,程序在運(yùn)行自然就還會(huì)伴隨有新的垃圾對(duì)象不斷產(chǎn)生,但這一部分垃圾對(duì)象是出現(xiàn)在標(biāo)記過程結(jié)束以后,CMS無法在當(dāng)次收集中處理掉它們,只好留待下一次垃圾收集時(shí)再清理掉。這一部分垃圾就稱為“浮動(dòng)垃圾”。同樣也是由于在垃圾收集階段用戶線程還需要持續(xù)運(yùn)行,那就還需要預(yù)留足夠內(nèi)存空間提供給用戶線程使用。在JDK5的默認(rèn)設(shè)置下,CMS收集器當(dāng)老年代使用了68%的空間后就會(huì)被激活,這是一個(gè)偏保守的設(shè)置,如果在實(shí)際應(yīng)用中老年代增長并不是太快,可以適當(dāng)調(diào)高參數(shù)-XX:CMSInitiatingOccu-pancyFraction的值來提高CMS的觸發(fā)百分比,降低內(nèi)存回收頻率,獲取更好的性能。到了JDK 6時(shí),CMS收集器的啟動(dòng)閾值就已經(jīng)默認(rèn)提升至92%。
  • **CMS是一款基于“標(biāo)記-清除”算法實(shí)現(xiàn)的收集器,如果讀者對(duì)前面這部分介紹還有印象的話,就可能想到這意味著收集結(jié)束時(shí)會(huì)有大量空間碎片產(chǎn)生。**為了解決這個(gè)問題,CMS收集器提供了一個(gè)-XX:+UseCMS-CompactAtFullCollection開關(guān)參數(shù)(默認(rèn)是開啟的,此參數(shù)從JDK 9開始廢棄),用于在CMS收集器不得不進(jìn)行Full GC時(shí)開啟內(nèi)存碎片的合并整理過程,由于這個(gè)內(nèi)存整理必須移動(dòng)存活對(duì)象,(在Shenandoah和ZGC出現(xiàn)前)是無法并發(fā)的。這樣空間碎片問題是解決了,但停頓時(shí)間又會(huì)變長,因此虛擬機(jī)設(shè)計(jì)者們還提供了另外一個(gè)參數(shù)-XX:CMSFullGCsBeforeCompaction(此參數(shù)從JDK 9開始廢棄),這個(gè)參數(shù)的作用是要求CMS收集器在執(zhí)行過若干次(數(shù)量由參數(shù)值決定)不整理空間的Full GC之后,下一次進(jìn)入Full GC前會(huì)先進(jìn)行碎片整理(默認(rèn)值為0,表示每次進(jìn)入Full GC時(shí)都進(jìn)行碎片整理)。
  • Garbage First收集器(G1收集器)

    HotSpot開發(fā)團(tuán)隊(duì)最初賦予它的期望是(在比較長期的)未來可以替換掉JDK 5中發(fā)布的CMS收集器。

    作為CMS收集器的替代者和繼承人,設(shè)計(jì)者們希望做出一款能夠建立起“停頓時(shí)間模型”(PausePrediction Model)的收集器,停頓時(shí)間模型的意思是能夠支持指定在一個(gè)長度為M毫秒的時(shí)間片段內(nèi),消耗在垃圾收集上的時(shí)間大概率不超過N毫秒這樣的目標(biāo)(即停頓時(shí)間可預(yù)測)

    在G1收集器出現(xiàn)之前的所有其他收集器,包括CMS在內(nèi),垃圾收集的目標(biāo)范圍要么是整個(gè)新生代(Minor GC),要么就是整個(gè)老年代(Major GC),再要么就是整個(gè)Java堆(Full GC)。而G1跳出了這個(gè)樊籠,它可以面向堆內(nèi)存任何部分來組成回收集(Collection Set,一般簡稱CSet)進(jìn)行回收。這就是G1收集器的Mixed GC模式。

    G1不再堅(jiān)持固定大小以及固定數(shù)量的分代區(qū)域劃分,而是==把連續(xù)的Java堆劃分為多個(gè)大小相等的獨(dú)立區(qū)域(Region),每一個(gè)Region都可以根據(jù)需要,扮演新生代的Eden空間、Survivor空間,或者老年代空間。==收集器能夠?qū)Π缪莶煌巧腞egion采用不同的策略去處理,這樣無論是新創(chuàng)建的對(duì)象還是已經(jīng)存活了一段時(shí)間、熬過多次收集的舊對(duì)象都能獲取很好的收集效果。

    Region中還有一類特殊的Humongous區(qū)域,專門用來存儲(chǔ)大對(duì)象。**G1認(rèn)為只要大小超過了一個(gè)Region容量一半的對(duì)象即可判定為大對(duì)象。**每個(gè)Region的大小可以通過參數(shù)-XX:G1HeapRegionSize設(shè)定,取值范圍為1MB~32MB,且應(yīng)為2的N次冪。而對(duì)于那些超過了整個(gè)Region容量的超級(jí)大對(duì)象,將會(huì)被存放在N個(gè)連續(xù)的Humongous Region之中,G1的大多數(shù)行為都把Humongous Region作為老年代的一部分來進(jìn)行看待。

    用戶設(shè)定允許的收集停頓時(shí)間(使用參數(shù)-XX:MaxGCPauseMillis指定,默認(rèn)值是200毫秒)

    Region里面存在的跨Region引用對(duì)象如何解決?

    使用記憶集避免全堆作為GC Roots掃描,但在G1收集器上記憶集的應(yīng)用其實(shí)要復(fù)雜很多,它的每個(gè)Region都維護(hù)有自己的記憶集,這些記憶集會(huì)記錄下別的Region指向自己的指針,并標(biāo)記這些指針分別在哪些卡頁的范圍之內(nèi)。**G1的記憶集在存儲(chǔ)結(jié)構(gòu)的本質(zhì)上是一種哈希表,Key是別的Region的起始地址,Value是一個(gè)集合,里面存儲(chǔ)的元素是卡表的索引號(hào)。這種“雙向”的卡表結(jié)構(gòu)(卡表是“我指向誰”,這種結(jié)構(gòu)還記錄了“誰指向我”)比原來的卡表實(shí)現(xiàn)起來更復(fù)雜,同時(shí)由于Region數(shù)量比傳統(tǒng)收集器的分代數(shù)量明顯要多得多,因此G1收集器要比其他的傳統(tǒng)垃圾收集器有著更高的內(nèi)存占用負(fù)擔(dān)。**根據(jù)經(jīng)驗(yàn),G1至少要耗費(fèi)大約相當(dāng)于Java堆容量10%至20%的額外內(nèi)存來維持收集器工作。

    G1收集器的運(yùn)作過程大致可劃分為以下四個(gè)步驟

    初始標(biāo)記(Initial Marking):僅僅只是標(biāo)記一下GC Roots能直接關(guān)聯(lián)到的對(duì)象,并且修改TAMS指針的值,讓下一階段用戶線程并發(fā)運(yùn)行時(shí),能正確地在可用的Region中分配新對(duì)象。這個(gè)階段需要停頓線程,但耗時(shí)很短,而且是借用進(jìn)行Minor GC的時(shí)候同步完成的,所以G1收集器在這個(gè)階段實(shí)際并沒有額外的停頓。

    并發(fā)標(biāo)記(Concurrent Marking):從GC Root開始對(duì)堆中對(duì)象進(jìn)行可達(dá)性分析,遞歸掃描整個(gè)堆里的對(duì)象圖,找出要回收的對(duì)象,這階段耗時(shí)較長,但可與用戶程序并發(fā)執(zhí)行。當(dāng)對(duì)象圖掃描完成以后,還要重新處理SATB記錄下的在并發(fā)時(shí)有引用變動(dòng)的對(duì)象。

    最終標(biāo)記(Final Marking):對(duì)用戶線程做另一個(gè)短暫的暫停,用于處理并發(fā)階段結(jié)束后仍遺留下來的最后那少量的SATB記錄。

    篩選回收(Live Data Counting and Evacuation):負(fù)責(zé)更新Region的統(tǒng)計(jì)數(shù)據(jù),對(duì)各個(gè)Region的回收價(jià)值和成本進(jìn)行排序,根據(jù)用戶所期望的停頓時(shí)間來制定回收計(jì)劃,可以自由選擇任意多個(gè)Region構(gòu)成回收集,然后把決定回收的那一部分Region的存活對(duì)象復(fù)制到空的Region中,再清理掉整個(gè)舊Region的全部空間。這里的操作涉及存活對(duì)象的移動(dòng),是必須暫停用戶線程,由多條收集器線程并行完成的。

    G1收集器除了并發(fā)標(biāo)記外,其余階段也是要完全暫停用戶線程的,換言之,它并非純粹地追求低延遲,官方給它設(shè)定的目標(biāo)是在延遲可控的情況下獲得盡可能高的吞吐量,所以才能擔(dān)當(dāng)起“全功能收集器”的重任與期望。

    [外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來直接上傳(img-ZHfEglHB-1604326359249)(/Users/duhaiyang/Library/Application Support/typora-user-images/截屏2020-11-02 下午10.02.50.png)]

    相比CMS,G1的優(yōu)點(diǎn)有很多,暫且不論可以指定最大停頓時(shí)間、分Region的內(nèi)存布局、按收益動(dòng)態(tài)確定回收集這些創(chuàng)新性設(shè)計(jì)帶來的紅利,單從最傳統(tǒng)的算法理論上看,G1也更有發(fā)展?jié)摿?。與CMS的“標(biāo)記-清除”算法不同,G1從整體來看是基于“標(biāo)記-整理”算法實(shí)現(xiàn)的收集器,但從局部(兩個(gè)Region之間)上看又是基于“標(biāo)記-復(fù)制”算法實(shí)現(xiàn),無論如何,這兩種算法都意味著G1運(yùn)作期間不會(huì)產(chǎn)生內(nèi)存空間碎片,垃圾收集完成之后能提供規(guī)整的可用內(nèi)存。

    總結(jié)

    以上是生活随笔為你收集整理的JVM2:垃圾收集器与内存分配策略的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。