<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          JVM 第二篇:垃圾收集器以及算法

          共 5684字,需瀏覽 12分鐘

           ·

          2020-10-06 19:57

          ?

          本文內(nèi)容過于硬核,建議有 Java 相關(guān)經(jīng)驗(yàn)人士閱讀。

          ?

          0. 引言

          一說到 JVM ,大多數(shù)人第一個想到的可能就是 GC ,今天我們就來聊一聊和 GC 關(guān)系最大的垃圾收集器以及垃圾收集算法,希望能通過本篇文章,讓各位同學(xué)對 GC 有一個初步大體的認(rèn)知。

          1. 運(yùn)行時數(shù)據(jù)區(qū)

          JVM 在執(zhí)行的時候會把它所管理的內(nèi)存劃分為幾個不同的數(shù)據(jù)區(qū)域。這些區(qū)域有各自的用途,以及創(chuàng)建和銷毀的時間,有的區(qū)域隨著虛擬機(jī)進(jìn)程的啟動而一直存在,有些區(qū)域則是依賴用戶線程的啟動和結(jié)束而建立和銷毀。根據(jù)《Java虛擬機(jī)規(guī)范》的規(guī)定,Java虛擬機(jī)所管理的內(nèi)存將會包括以下幾個運(yùn)行時數(shù)據(jù)區(qū)域:

          1.1 程序計數(shù)器

          指向當(dāng)前線程所執(zhí)行的字節(jié)碼的行號,其實(shí)就是一小塊內(nèi)存,記錄著當(dāng)前程序運(yùn)行到哪了字節(jié)碼解釋器的工作就是通過改變這個計數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼指令。分支,循環(huán),跳轉(zhuǎn),異常處理,線程回復(fù)等都需要依賴這個計數(shù)器來完成。

          由于Java的多線程是通過線程輪流切換完成的,一個線程沒有執(zhí)行完時就需要一個東西記錄它執(zhí)行到哪了,下次搶占到了CPU資源時再從這開始,這個東西就是程序計數(shù)器,正是因?yàn)檫@樣,所以它也是“線程私有”的內(nèi)存。

          如果一個線程執(zhí)行一個主要方法,這個計數(shù)器記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址;如果正在執(zhí)行的是一個本地方法,這個計數(shù)器的值則為空,此內(nèi)存區(qū)域是唯一一個在Java的虛擬機(jī)規(guī)范中沒有規(guī)定任何OutOfMemoryError異常情況的區(qū)域。

          1.2 Java 虛擬機(jī)棧

          與程序計數(shù)器一樣, Java 虛擬機(jī)棧(Java Virtual Machine Stack)也是現(xiàn)成私有的,它的生命周期與線程相同。

          虛擬機(jī)棧描述的是 Java 方法執(zhí)行的線程內(nèi)存模型:每個方法被執(zhí)行的時候, Java 虛擬機(jī)都會同步創(chuàng)建一個棧幀(Stack Frame)用于存儲局部變量表、操作數(shù)棧、動態(tài)連接、方法出口等信息。每一個方法被調(diào)用直至執(zhí)行完畢的過程,就對應(yīng)著一個棧幀在虛擬機(jī)棧中從入棧到出棧的過程。

          經(jīng)常有人把 Java 內(nèi)存區(qū)域籠統(tǒng)地劃分為堆內(nèi)存(Heap)和棧內(nèi)存(Stack),這種劃分方式直接繼承自傳統(tǒng)的 C 、 C++ 程序的內(nèi)存布局結(jié)構(gòu),在 Java 語言里就顯得有些粗糙了,實(shí)際的內(nèi)存區(qū)域劃分要比這更復(fù)雜。不過這也說明了程序員最關(guān)注的實(shí)際上是「堆」和「棧」兩塊,這里的「棧」通常指的就是 Java 虛擬機(jī)棧,或者更多情況下只是指虛擬機(jī)棧中的局部變量表部分。

          1.3 本地方法棧

          本地方法棧(Native Method Stacks)與虛擬機(jī)棧所發(fā)揮的作用是非常相似的,其區(qū)別只是虛擬機(jī)棧為虛擬機(jī)執(zhí)行Java方法(也就是字節(jié)碼)服務(wù),而本地方法棧則是為虛擬機(jī)使用到的本地(Native)方法服務(wù)。

          1.4 Java 堆

          Java 堆(Java Heap)是虛擬機(jī)所管理的內(nèi)存中最大的一塊。Java 堆是被所有線程共享的一塊內(nèi)存區(qū)域,在虛擬機(jī)啟動時創(chuàng)建。此內(nèi)存區(qū)域的唯一目的就是存放對象實(shí)例, Java 世界里“幾乎”所有的對象實(shí)例都在這里分配內(nèi)存。

          1.5 方法區(qū)

          方法區(qū)(Method Area)與 Java 堆一樣,是各個線程共享的內(nèi)存區(qū)域,它用于存儲已被虛擬機(jī)加載的類型信息、常量、靜態(tài)變量、即時編譯器編譯后的代碼緩存等數(shù)據(jù)。雖然《Java虛擬機(jī)規(guī)范》中把方法區(qū)描述為堆的一個邏輯部分,但是它卻有一個別名叫作“非堆”(Non-Heap),目的是與 Java 堆區(qū)分開來。

          說到這里,不得不提一下「永久代(Permanent Generation)」這個概念,大多數(shù)的程序員,都是在 Hotspot 虛擬機(jī)上進(jìn)行開發(fā)、部署程序的,因此很多人都愿意把方法區(qū)稱之為永久代,實(shí)際上這兩者并不是一個等價的關(guān)系,而僅僅只是 Hotspot 團(tuán)隊使用「永久代」來實(shí)現(xiàn)方法區(qū),這樣使得 Hotspot 可以像管理 Java 堆內(nèi)存一樣管理這部分內(nèi)存,實(shí)際上現(xiàn)在回過頭來看,當(dāng)年使用「永久代」來實(shí)現(xiàn)方法區(qū)并不是一個好主意,這種設(shè)計導(dǎo)致 Java 更容易遇到內(nèi)存溢出的問題。因?yàn)橛谰么?-XX:MaxPermSize 的上限,即使不設(shè)置也有默認(rèn)大小,甚至在一些大型項目中,啟動參數(shù)不設(shè)置這個直接就啟動失敗,這種項目我接觸過不止一個。。。

          那有沒其他隊方法區(qū)的實(shí)現(xiàn)方案,當(dāng)然有,比如 BEA JRockit、IBM J9 ,是不存在永久代概念的,在 JRockit 和 J9 當(dāng)中,只要沒有觸碰到進(jìn)程可用的內(nèi)存上限,就不會有問題,在 32 位系統(tǒng)中上限是 4GB 。

          2. 垃圾收集器

          2.1 Serial 收集器

          Serial 收集器是最基礎(chǔ)、歷史最悠久的收集器,曾經(jīng)(在JDK 1.3.1之前)是 HotSpot 虛擬機(jī)新生代收集器的唯一選擇。

          Serial 是一個單線程的收集器,這個單線程并不是僅僅局限在它在進(jìn)行垃圾回收的時候是單線程工作的,更重要的是它在進(jìn)行 GC 的時候,是需要所有的線程都停掉的,直到它工作結(jié)束才能繼續(xù)工作。

          2.2 PerNew 收集器

          PerNew 收集器實(shí)質(zhì)上是 Serial 的多線程并行版本。

          PerNew 除了支持多線程并行收集以外,與 Serial 并沒有太多不一樣的地方,但它卻是運(yùn)行在服務(wù)端模式下的 HotSpot 虛擬機(jī),尤其是 JDK 7 之前的遺留系統(tǒng)中首選的新生代收集器。

          這其中有一個很重要的原因,和功能、性能都無關(guān),是因?yàn)槟壳俺?Serial 以外, PerNew 是唯一一個可以和 CMS 配合工作。

          2.3 Parallel Scavenge 收集器

          Parallel Scavenge 收集器也是一款新生代收集器,它同樣是基于標(biāo)記-復(fù)制算法實(shí)現(xiàn)的收集器,也是能夠并行收集的多線程收集器。

          看起來和 PerNew 貌似沒啥區(qū)別,但是 Parallel Scavenge 的關(guān)注點(diǎn)是達(dá)到一個可控制的吞吐量(Throughput)。

          吞吐量?=?運(yùn)行用戶代碼時間?/?(運(yùn)行用戶代碼時間?+?運(yùn)行垃圾收集時間)

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

          • -XX:MaxGCPauseMillis: 參數(shù)允許的值是一個大于 0 毫秒數(shù),收集器將盡力保證內(nèi)存回收花費(fèi)的時間不超過用戶設(shè)定值。
          • -XX:GCTimeRatio: 參數(shù)的值則應(yīng)當(dāng)是一個大于0小于100的整數(shù),也就是垃圾收集時間占總時間的比率,相當(dāng)于吞吐量的倒數(shù)。

          2.4 Serial Old 收集器

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

          2.5 Parallel Old 收集器

          Parallel Old 是 Parallel Scavenge 收集器的老年代版本,支持多線程并發(fā)收集,基于標(biāo)記-整理算法實(shí)現(xiàn)。這個收集器是直到 JDK 6 時才開始提供的。

          2.6 CMS 收集器

          CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標(biāo)的收集器。

          它的運(yùn)作過程相對于前面幾種收集器來說要更復(fù)雜一些,整個過程分為四個步驟,包括:

          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)記」這兩個過程仍然需要停止當(dāng)前所有的進(jìn)程,然后單獨(dú)進(jìn)行執(zhí)行。

          初始標(biāo)記僅僅只是標(biāo)記一下 GC Root 能關(guān)聯(lián)到的對象,速度很快。

          并發(fā)標(biāo)記是從 GC Roots 的直接關(guān)聯(lián)對象開始遍歷整個對象圖的過程,這個過程雖然耗時較多但是不需要停止用戶線程,可以和用戶線程一起并行。

          重新標(biāo)記則是為了修正并發(fā)標(biāo)記期間,因用戶程序繼續(xù)運(yùn)作而導(dǎo)致標(biāo)記產(chǎn)生變動的那一部分對象的標(biāo)記記錄,這個階段停頓的時間會比初始標(biāo)記的時間長,但是也遠(yuǎn)比并發(fā)標(biāo)記的時間短。

          并發(fā)清除,在這個階段是清理刪除掉已經(jīng)進(jìn)行標(biāo)記判斷死亡的對象,由于不需要移動存活的對象,所以這個階段也可以和用戶線程一起并發(fā)的進(jìn)行。

          2.7 Garbage First 收集器

          Garbage First 就是后來大名鼎鼎的 G1 收集器, G1 收集器是是垃圾收集器技術(shù)發(fā)展歷史上的里程碑式的成果,它開創(chuàng)了收集器面向局部收集的設(shè)計思路和基于 Region 的內(nèi)存布局形式。

          • 初始標(biāo)記:僅僅只是標(biāo)記一下 GC Roots 能直接關(guān)聯(lián)到的對象,并且修改 TAMS 指針的值,讓下一階段用戶線程并發(fā)運(yùn)行時,能正確地在可用的 Region 中分配新對象。這個階段需要停頓線程,但耗時很短,而且是借用進(jìn)行 Minor GC 的時候同步完成的,所以 G1 收集器在這個階段實(shí)際并沒有額外的停頓。
          • 并發(fā)標(biāo)記:從 GC Root 開始對堆中對象進(jìn)行可達(dá)性分析,遞歸掃描整個堆里的對象圖,找出要回收的對象,這階段耗時較長,但可與用戶程序并發(fā)執(zhí)行。當(dāng)對象圖掃描完成以后,還要重新處理SATB記錄下的在并發(fā)時有引用變動的對象。
          • 最終標(biāo)記:對用戶線程做另一個短暫的暫停,用于處理并發(fā)階段結(jié)束后仍遺留下來的最后那少量的 SATB 記錄。
          • 篩選回收:負(fù)責(zé)更新 Region 的統(tǒng)計數(shù)據(jù),對各個 Region 的回收價值和成本進(jìn)行排序,根據(jù)用戶所期望的停頓時間來制定回收計劃,可以自由選擇任意多個 Region 構(gòu)成回收集,然后把決定回收的那一部分 Region 的存活對象復(fù)制到空的 Region 中,再清理掉整個舊 Region 的全部空間。這里的操作涉及存活對象的移動,是必須暫停用戶線程,由多條收集器線程并行完成的。

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

          3. 垃圾收集算法

          垃圾收集都是建立在分代收集之上的,一般而言,我們對垃圾收集分類如下:

          1. 部分收集(Partial GC):指目標(biāo)不是完整收集整個 Java 堆的垃圾收集,其中又分為:
            1. 新生代收集(Minor GC/Young GC):指目標(biāo)只是新生代的垃圾收集。
            2. 老年代收集(Major GC/Old GC):指目標(biāo)只是老年代的垃圾收集。
          2. 混合收集(Mixed GC):指目標(biāo)是收集整個新生代以及部分老年代的垃圾收集。目前只有G1收集器會有這種行為。
          3. 整堆收集(Full GC):收集整個Java堆和方法區(qū)的垃圾收集。

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

          標(biāo)記-清除算法,是最早出現(xiàn)同時也是最基礎(chǔ)的垃圾收集算法,后續(xù)的大多數(shù)算法都是以標(biāo)記-清除算法為基礎(chǔ),對其缺點(diǎn)進(jìn)行改進(jìn)而來的。

          標(biāo)記-清除算法的具體執(zhí)行過程如下:

          這個算法有兩個大的缺陷:

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

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

          標(biāo)記-復(fù)制算法是一種半?yún)^(qū)復(fù)制的垃圾回收算法,它將內(nèi)存大小按照容量劃分為大小相等的兩塊,每次只使用其中的一塊,當(dāng)這一塊用完了,再將還活著的對象一次復(fù)制到另一塊上面,然后將已使用過的全部清除掉,具體執(zhí)行過程如下:

          這種方案的缺陷顯而易見,那就是可用內(nèi)存直接縮小了一半。

          IBM 公司針對新生代「朝生夕滅」的特點(diǎn)做出了更量化的詮釋:新生代中的對象有 98% 熬不過第一輪的收集,因此無需按照 1:1 的比例來劃分新生代的內(nèi)存空間。

          Andrew Appel 針對具備「朝生夕滅」特點(diǎn)的對象,提出了一種更優(yōu)化的半?yún)^(qū)復(fù)制分代策略,現(xiàn)在稱為「Appel式回收」。

          HotSpot 虛擬機(jī)的 Serial 、 ParNew 等新生代收集器均采用了這種策略來設(shè)計新生代的內(nèi)存布局。

          具體做法是把新生代分為一塊較大的 Eden 空間和兩塊較小的 Survivor 空間,每次內(nèi)存分配至分配 Eden 空間和一塊 Survivor 空間,發(fā)生垃圾回收時,將 Eden 和 Survivor 中仍然存活的對象一次性復(fù)制到另外一塊 Survivor 空間上,然后直接清理掉 Eden 和已用過的那塊 Survivor 空間。

          由于沒有人可以確定的說每次垃圾回收存活的對象都能放入 Survivor 空間中,所以,這種垃圾回收算法還設(shè)計了一個「逃生門」的安全設(shè)計,就是如果 Survivor 空間無法容納一次垃圾回收后的對象,就需要依賴其他區(qū)域(大多數(shù)是老年代)進(jìn)行分配擔(dān)保(Handle Promotion)。

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

          由于老年代大多數(shù)的對象都不會被回收,所以標(biāo)記-復(fù)制算法并不適用于老年代的垃圾回收算法,這時,就需要一個新的算法來應(yīng)對老年代的垃圾回收。

          標(biāo)記-整理算法應(yīng)運(yùn)而生,標(biāo)記-整理算法和標(biāo)記-清除算法非常的像,標(biāo)記-整理算法后續(xù)的步驟并不是直接對可回收的垃圾進(jìn)行整理,而是讓所有存活的對象都想空間的一端進(jìn)行移動,然后處理掉其余的內(nèi)存,具體執(zhí)行過程如下:

          參考

          《深入理解Java虛擬機(jī):JVM高級特性與最佳實(shí)踐_周志明》

          https://blog.csdn.net/fanxing1964/article/details/79349824




          感謝閱讀



          瀏覽 41
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  1234视频看看日本在线 | 四虎无码人妻三区 | 日本精品一字幕 | 国产精品怡红院 | 操WWW|