java 虛擬機(jī)內(nèi)存區(qū)域劃分和GC相關(guān)

資料:
https://zhuanlan.zhihu.com/p/45558897
https://tech.meituan.com/2017/12/29/jvm-optimize.html

虛擬機(jī)內(nèi)存區(qū)域劃分

image.png

堆 (heap)

虛擬機(jī)管理內(nèi)存中最大的一塊。在虛擬機(jī)啟動(dòng)的時(shí)候創(chuàng)建。目的就是存對(duì)象實(shí)例

方法區(qū) (Method Area)

JVM常量池主要分為Class文件常量池、運(yùn)行時(shí)常量池,全局字符串常量池,以及基本類型包裝類對(duì)象常量池。

JDK1.6及之前,運(yùn)行時(shí)常量池是方法區(qū)的一個(gè)部分,同時(shí)方法區(qū)里面存儲(chǔ)了類的元數(shù)據(jù)信息、靜態(tài)變量、即時(shí)編譯器編譯后的代碼等。

JDK1.7及以后,JVM已經(jīng)將運(yùn)行時(shí)常量池從方法區(qū)中移了出來,在JVM堆開辟了一塊區(qū)域存放常量池。字符串常量池被移到了堆中了。此時(shí)常量池存儲(chǔ)的就是引用了。JDK1.8以后方法區(qū)在元空間,元空間在本地內(nèi)存。

程序計(jì)數(shù)器(Program counter Register)

內(nèi)存中比較小的一塊內(nèi)存區(qū)域,作用是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器。此內(nèi)存區(qū)域是唯一一個(gè)在Java虛擬機(jī)規(guī)范中沒有規(guī)定任何OutOfMemoryError情況的區(qū)域。

jvm棧(jvm stacks)

虛擬機(jī)描繪java方法執(zhí)行的內(nèi)存模型。每個(gè)方法執(zhí)行的時(shí)候 都會(huì)創(chuàng)建一個(gè)幀棧(Stack Frame)用于存儲(chǔ)局部變量表,操作棧,動(dòng)態(tài)鏈接方法出口信息。

棧幀

首先 一個(gè)線程對(duì)應(yīng)一個(gè)棧幀, jvm調(diào)用一個(gè)java方法的時(shí)候,他對(duì)應(yīng)的類的類型信息中得到這個(gè)方法的局部變量區(qū)和操作數(shù)棧的大小。并依據(jù)這個(gè)進(jìn)行分配棧幀內(nèi)存。壓入jvm棧中。 在活動(dòng)線程中,只有在棧頂?shù)臈攀怯行У?,被稱為當(dāng)前棧幀。與這個(gè)棧幀關(guān)聯(lián)的方法被稱為當(dāng)前方法 棧幀存儲(chǔ)了方法的局部變量表、操作數(shù)棧、動(dòng)態(tài)連接和方法返回地址等信息。

每一個(gè)方法從調(diào)用開始至執(zhí)行完成的過程,都對(duì)應(yīng)著一個(gè)棧幀在虛擬機(jī)里面從入棧到出棧的過程。 在編譯程序代碼的時(shí)候,棧幀中需要多大的局部變量表,多深的操作數(shù)棧都已經(jīng)完全確定了。因此一個(gè)棧幀需要分配多少內(nèi)存,不會(huì)受到程序運(yùn)行期變量數(shù)據(jù)的影響,而僅僅取決于具體的虛擬機(jī)實(shí)現(xiàn)。

本地方法棧 (Naitve Methord Stacks)

是與虛擬機(jī)棧發(fā)揮作用非常相似, 虛擬機(jī)棧為虛擬機(jī)執(zhí)行java方法, 而本地方法棧為虛擬機(jī)使用到的Native 方法服務(wù)。

GC相關(guān)

GC工作區(qū)域

GC的主要工作是在Heap (堆)和 metaSpace(元空間中的方法區(qū))。如果在Direct Memory(直接內(nèi)存 ) 如果使用的是 DirectByteBuffer,那么在分配內(nèi)存不夠時(shí)則是 GC 通過 Cleaner#clean 間接管理。

為什么回收主要在堆和方法區(qū)

Java虛擬機(jī)棧、本地方法棧、程序計(jì)數(shù)器這三者是線程私有的,隨線程而生隨線程而滅。棧中的棧幀隨著方法的進(jìn)入和退出有條不紊的出棧入棧,每個(gè)棧幀需要分配多少內(nèi)存,在類結(jié)構(gòu)確定下來時(shí)就是已知的(盡管運(yùn)行期間會(huì)有JIT編譯器進(jìn)行一些優(yōu)化,但在基于概念模型的討論中,大體可以認(rèn)為是編譯器可知的)。因此上述這些區(qū)域的內(nèi)存分配和回收都具備確定性,故不需要過多考慮垃圾回收的問題。而Java堆和方法區(qū)則不一樣:一個(gè)接口中的多個(gè)實(shí)現(xiàn)類需要的內(nèi)存可能不一樣,一個(gè)方法中的多個(gè)分支所需的內(nèi)存也不一樣,我們只有在程序運(yùn)行期間才知道會(huì)創(chuàng)建哪些對(duì)象,所以這部分內(nèi)存的分配和回收都是動(dòng)態(tài)的,所以垃圾回收器所關(guān)注的重點(diǎn)是位于Java堆和方法區(qū)上的內(nèi)存。

算法決生死(java對(duì)象生死判斷算法)

引用計(jì)數(shù)法(Reference Counting)

對(duì)每個(gè)對(duì)象的引用進(jìn)行計(jì)數(shù),每當(dāng)有一個(gè)地方引用它時(shí)計(jì)數(shù)器 +1、引用失效則 -1,引用的計(jì)數(shù)放到對(duì)象頭中,大于 0 的對(duì)象被認(rèn)為是存活對(duì)象。雖然循環(huán)引用的問題可通過 Recycler 算法解決,但是在多線程環(huán)境下,引用計(jì)數(shù)變更也要進(jìn)行昂貴的同步操作,性能較低,早期的編程語(yǔ)言會(huì)采用此算法。引用計(jì)數(shù)法是可以處理循環(huán)引用問題的。

可達(dá)性分析,又稱引用鏈法(Tracing GC)

從 GC Root 開始進(jìn)行對(duì)象搜索,可以被搜索到的對(duì)象即為可達(dá)對(duì)象,此時(shí)還不足以判斷對(duì)象是否存活/死亡,需要經(jīng)過多次標(biāo)記才能更加準(zhǔn)確地確定,整個(gè)連通圖之外的對(duì)象便可以作為垃圾被回收掉。目前 Java 中主流的虛擬機(jī)均采用此算法 此算法的基本思路就是通過一系列的“GC Roots”的對(duì)象作為起始點(diǎn),從起始點(diǎn)開始向下搜索到對(duì)象的路徑。搜索所經(jīng)過的路徑稱為引用鏈(Reference Chain),當(dāng)一個(gè)對(duì)象到任何GC Roots都沒有引用鏈時(shí),則表明對(duì)象“不可達(dá)”,即該對(duì)象是不可用的。

GC Root 可以使用的對(duì)象
  • 棧幀中的局部變量表中的reference引用所引用的對(duì)象

  • 方法區(qū)中類static靜態(tài)引用的對(duì)象

  • 方法區(qū)中final常量引用的對(duì)象

  • 本地方法棧中JNI(Native方法)引用的對(duì)象

對(duì)象自我救贖

對(duì)象經(jīng)過可達(dá)性算法分析后,判斷為不可達(dá),那么對(duì)象就必死無(wú)疑了么?不一定,對(duì)象在面臨垃圾回收器的處理時(shí),還有最后一次求生的機(jī)會(huì)。

要kill掉一個(gè)對(duì)象,至少要經(jīng)過垃圾回收器的2次標(biāo)記過程,不可達(dá)的對(duì)象被第一次標(biāo)記后會(huì)進(jìn)行一次篩選,篩選的條件是「此對(duì)象是否有必要執(zhí)行finalize()方法」,當(dāng)對(duì)象沒有覆蓋finalize方法或者已經(jīng)執(zhí)行過finalize方法時(shí),會(huì)被判斷為:沒必要執(zhí)行。如果被判斷為有必要執(zhí)行,則該對(duì)象會(huì)被放置在一個(gè)F-Queue隊(duì)列,并在稍后虛擬機(jī)建立的Finalizer線程中執(zhí)行finalize()來kill掉對(duì)象。在回收前垃圾回收器會(huì)對(duì)F-Queue隊(duì)列中的對(duì)象進(jìn)行第二次標(biāo)記,如果在標(biāo)記前,對(duì)象成功與引用鏈上的任意對(duì)象建立了關(guān)聯(lián),則會(huì)在第二次標(biāo)記時(shí)被移出F-Queue,從而實(shí)現(xiàn)自救。

GC垃圾收集算法

Mark-Sweep(標(biāo)記-清除)

回收過程主要分為兩個(gè)階段,第一階段為追蹤(Tracing)階段,即從 GC Root 開始遍歷對(duì)象圖,并標(biāo)記(Mark)所遇到的每個(gè)對(duì)象,第二階段為清除(Sweep)階段,即回收器檢查堆中每一個(gè)對(duì)象,并將所有未被標(biāo)記的對(duì)象進(jìn)行回收,整個(gè)過程不會(huì)發(fā)生對(duì)象移動(dòng)。整個(gè)算法在不同的實(shí)現(xiàn)中會(huì)使用三色抽象(Tricolour Abstraction)、位圖標(biāo)記(BitMap)等技術(shù)來提高算法的效率,存活對(duì)象較多時(shí)較高效。

缺點(diǎn):

  • 效率較低,因?yàn)闃?biāo)記和清除這兩個(gè)過程效率都比較低

  • 空間問題,標(biāo)記清除后會(huì)產(chǎn)生大量不聯(lián)系的內(nèi)存空間(碎片),導(dǎo)致如果有大內(nèi)存的對(duì)象,那么就無(wú)法找到足夠大的連續(xù)內(nèi)存空間以供分配。

image-20210830152403584.png

Mark-Compact (標(biāo)記-整理/壓縮)

算法的主要目的就是解決在非移動(dòng)式回收器中都會(huì)存在的碎片化問題,也分為兩個(gè)階段,第一階段與 Mark-Sweep 類似,第二階段則會(huì)對(duì)存活對(duì)象按照整理順序(Compaction Order)進(jìn)行整理。主要實(shí)現(xiàn)有雙指針(Two-Finger)回收算法、滑動(dòng)回收(Lisp2)算法和引線整理(Threaded Compaction)算法等。在青年代采用復(fù)制算法是非常合適的,因?yàn)榍嗄甏奶攸c(diǎn)是對(duì)象數(shù)量多,生存時(shí)間短,所以空間利用率比較重要,而復(fù)制算法對(duì)于老年代Old Generation則不太適合,因?yàn)槔夏甏膶?duì)象數(shù)量雖少,但比較穩(wěn)定存活率高這樣會(huì)有較多的復(fù)制開銷,針對(duì)這種情況,出現(xiàn)了標(biāo)記-壓縮算法。標(biāo)記-壓縮算法和標(biāo)記-清除算法類似,先通過標(biāo)記找出等待回收的對(duì)象,然后在清除之前將存活的對(duì)象都整理整齊放到一邊,然后再清除掉邊界以外的內(nèi)存。


image-20210830152422172.png

復(fù)制算法

將完整內(nèi)存區(qū)域分為大小相等的2塊,每次只使用其中的一塊,當(dāng)這塊內(nèi)存滿了(用完),則將此塊內(nèi)存上的對(duì)象都「復(fù)制」到另一塊空內(nèi)存上去,然后將用完的那塊內(nèi)存進(jìn)行垃圾回收。

  • 優(yōu)點(diǎn) 吞吐量高,不需要遍歷全堆,只需要處理活動(dòng)對(duì)象。不會(huì)有碎片化的問題,因?yàn)槊看螐?fù)制都將存活對(duì)象從from復(fù)制到to的一端

  • 缺點(diǎn) 堆利用率較低,因?yàn)樵趶?fù)制算法下,只有一半的內(nèi)存用來存儲(chǔ)對(duì)象

image-20210830152522608.png

分代收集

Java堆是垃圾收集器管理的主要內(nèi)存,由于主流的虛擬機(jī)實(shí)現(xiàn)中,垃圾收集器大多采用分代式垃圾回收算法(Generational Garbage Collection),所以會(huì)將垃圾收集器所管理的堆內(nèi)存劃分為不同的代。

在Java7以前Hotspot虛擬機(jī)中將Java堆內(nèi)存分為3個(gè)部分:

  • 青年代 Young Generation

  • 老年代 Old Generation

  • 永久代(1.8刪除) Permanent Generation

在Java8以后,由于方法區(qū)的內(nèi)存不在分配在Java堆上,而是存儲(chǔ)于本地內(nèi)存元空間Metaspace中,所以永久代就不存在了。

[圖片上傳失敗...(image-b01c3b-1630309095177)]

青年代(新生代)

其中青年代中單獨(dú)劃分成了三塊——Eden+Survivor+Survivor。大部分對(duì)象在Eden區(qū)中生成。在這些不同區(qū)域上任何一個(gè)內(nèi)存“滿”了以后,都會(huì)觸發(fā)一次垃圾收集過程。Java中絕大部分的新創(chuàng)建的對(duì)象都被分配到了青年代中的Eden區(qū)。當(dāng)內(nèi)存不夠時(shí),虛擬機(jī)將會(huì)發(fā)動(dòng)一次MinorGC。

晉升老年代

當(dāng)Eden區(qū)滿時(shí),還存活的對(duì)象將被復(fù)制到兩個(gè)Survivor區(qū)(中的一個(gè))。當(dāng)這個(gè)Survivor區(qū)滿時(shí),此區(qū)的存活且不滿足“晉升”條件的對(duì)象將被復(fù)制到另外一個(gè)Survivor區(qū)。對(duì)象每經(jīng)歷一次Minor GC,年齡加1,達(dá)到“晉升年齡閾值”后,被放到老年代,這個(gè)過程也稱為“晉升”。顯然,“晉升年齡閾值”的大小直接影響著對(duì)象在新生代中的停留時(shí)間,在Serial和ParNew GC兩種回收器中,“晉升年齡閾值”通過參數(shù)MaxTenuringThreshold設(shè)定,默認(rèn)值為15。 為了更好地適應(yīng)不同程序的內(nèi)存狀況,虛擬機(jī)并不是永遠(yuǎn)要求對(duì)象年齡必須達(dá)到「閾值」才能提升至老年代。在有的垃圾收集器實(shí)現(xiàn)中,如果Survivor空間中相同年齡的對(duì)象占用空>Survivor總空間的一半,則此年齡的所有對(duì)象就可以提前進(jìn)入老年代,而不是必須達(dá)到閾值。

老年代

大對(duì)象直接進(jìn)入老年代 所謂的大對(duì)象是指需要大量連續(xù)內(nèi)存空間的Java對(duì)象,長(zhǎng)字符串及大容量的數(shù)組。安放這些大對(duì)象,虛擬機(jī)會(huì)直接將其放在老年代,因?yàn)榇髮?duì)象一般涉及到的引用多,不容易「死」掉。而且大對(duì)象占內(nèi)存,所以直接在老年代為其開辟一塊連續(xù)的內(nèi)存就比較合適。如果內(nèi)存不夠分配,虛擬機(jī)會(huì)觸發(fā)垃圾收集過程。 長(zhǎng)期存活的對(duì)象進(jìn)入老年代 既然虛擬機(jī)采用分代收集的策略來管理內(nèi)存,那么內(nèi)存回收時(shí)就應(yīng)該相應(yīng)的判別哪些對(duì)象該放在青年代,哪些放在老年代。為此,JVM給每個(gè)對(duì)象定義了一個(gè)年齡計(jì)數(shù)器。如果對(duì)象在Eden出生,并且經(jīng)過一次MinorGC后仍然存在,則「年齡」增加1歲。當(dāng)年齡增加到一定數(shù)目(如:默認(rèn)為15歲),就會(huì)被提升至老年代。 MinorGC 在MinorGC之前,JVM會(huì)首先檢查老年代最大可用的連續(xù)內(nèi)存空間是否 > 青年代所有對(duì)象總空間,并以其作為MajorGC執(zhí)行的「擔(dān)?!埂H绻笥趧tMinorGC可以正常執(zhí)行。否則JVM會(huì)查看HandlePromotionFailure設(shè)置值是否允許擔(dān)保失敗,如果允許,則繼續(xù)執(zhí)行MinorGC,否則則執(zhí)行MajorGC用來回收足夠的內(nèi)存空間。 在年輕代內(nèi)存區(qū)域上的垃圾收集過程,因?yàn)榇蠖鄶?shù)在年輕代上的對(duì)象“朝生夕滅”,所以MinorGC非常頻繁,一般收集的速度也很快。

MajorGC/Full GC 指發(fā)生在老年代的GC,此區(qū)域一般對(duì)象存活率高,GC一次的速度同城比MinorGC慢10倍以上。

總結(jié) 可以晉升老年代的類型
1、分配擔(dān)保機(jī)制
Eden區(qū)滿時(shí),進(jìn)行Minor GC,當(dāng)Eden和一個(gè)Survivor區(qū)中依然存活的對(duì)象無(wú)法放入到Survivor中,則通過分配擔(dān)保機(jī)制提前轉(zhuǎn)移到老年代中。

2、對(duì)象過大
若對(duì)象體積太大,新生代無(wú)法容納這個(gè)對(duì)象,就會(huì)繞過新生代, 直接在老年代分配, 此參數(shù)只對(duì)Serial及ParNew兩款收集器有效。

參數(shù)-XX:PretenureSizeThreshold用來設(shè)置這個(gè)門限值。

3、長(zhǎng)期存活的對(duì)象
對(duì)象頭的Mark Word中包含對(duì)象的年齡。當(dāng)年齡增加到一定的臨界值時(shí),就會(huì)晉升到老年代中。

該臨界值由參數(shù):-XX:MaxTenuringThreshold來設(shè)置,默認(rèn)為15,即對(duì)象在經(jīng)歷15次minor gc后會(huì)晉升到老年代。

4、動(dòng)態(tài)對(duì)象年齡判定
如果在Survivor區(qū)中相同年齡的對(duì)象的所有大小之和超過Survivor空間的一半,年齡大于或等于該年齡的對(duì)象就可以直接進(jìn)入老年代

CMS和G1

CMS(Concurent Mark Sweep

從名字可以看出這款收集器是一款比較優(yōu)秀的基于標(biāo)記-清除算法的并發(fā)收集器。之前也提到過,此收集器的目標(biāo)在于盡量小的Stop The World間隔時(shí)間,用于用戶交互比較多的場(chǎng)景。

收集過程
  • 初始標(biāo)記

  • 并發(fā)標(biāo)記

  • 重新標(biāo)記

  • 并發(fā)清除

其中初始標(biāo)記和重新標(biāo)記兩個(gè)步驟仍需要Stop The World間隔。初始標(biāo)記僅僅是標(biāo)記一下GC Roots能直接關(guān)聯(lián)到的對(duì)象,速度很快。并發(fā)標(biāo)記階段就是進(jìn)行GC Roots追蹤的過程,而重新標(biāo)記則是為了修正并發(fā)標(biāo)記期間由于用戶程序繼續(xù)執(zhí)行可能產(chǎn)生變動(dòng)的那部分對(duì)象的標(biāo)記記錄,此階段會(huì)比初始標(biāo)記長(zhǎng)一些,但遠(yuǎn)小于并發(fā)標(biāo)記的時(shí)間。

整個(gè)階段并發(fā)標(biāo)記和并發(fā)清除是耗時(shí)最長(zhǎng)的兩個(gè)階段。但是由于CMS收集器是并發(fā)執(zhí)行的,故可以和用戶線程一起工作,所以從整體上CMS收集器的工作過程是和用戶線程并發(fā)執(zhí)行的。

優(yōu)點(diǎn):

GC收集間隔時(shí)間短,多線程并發(fā)。

缺點(diǎn):

  • 并發(fā)時(shí)對(duì)CPU資源占用多,不適合CPU核心數(shù)較少的情況。

  • 且由于采用標(biāo)記清除算法,所以會(huì)產(chǎn)生內(nèi)存碎片。

  • 無(wú)法處理浮動(dòng)垃圾。

G1 (Garbage-First)

Java11官網(wǎng)描述中已經(jīng)說明:G1取代了Concurrent Mark-Sweep(CMS)收集器。它也是默認(rèn)的收集器。表明在Java11中G1是默認(rèn)的垃圾收集器,而CMS收集器從JDK 9開始就不推薦使用了

G1特點(diǎn):
  • 并行與并發(fā):

    G1能充分利用多CPU下的優(yōu)勢(shì)來縮短Stop The World的時(shí)間,同時(shí)在其他部分收集器需要停止Java線程來執(zhí)行GC動(dòng)作時(shí),G1收集器仍然可以通過并發(fā)來讓Java線程同步執(zhí)行。

  • 分代收集

    與其他收集器一樣,分代的概念在G1中任然被保留??梢圆恍枰浜掀渌睦占鳎酮?dú)立管理整個(gè)Java堆內(nèi)存的所有分代區(qū)域,且采用不同的方式來獲得更好的垃圾收集效果。

  • 空間整合

    G1從整體來看,使用的是標(biāo)記-壓縮算法實(shí)現(xiàn)的,從局部?jī)蓚€(gè)Region來看,采用的是復(fù)制算法實(shí)現(xiàn)的,對(duì)內(nèi)存空間的利用非常高效,不會(huì)像CMS一樣產(chǎn)生內(nèi)存碎片。

  • 可以預(yù)測(cè)的停頓

除了追求低停頓以外,G1的停頓時(shí)間可以被指定在一個(gè)時(shí)間范圍內(nèi)。

如果不計(jì)算維護(hù)Remenbered Set的操作,G1收集器的工作階段大致區(qū)分如下:

  • 初始標(biāo)記

  • 并發(fā)標(biāo)記

  • 最終標(biāo)記

  • 篩選回收

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

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

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