分區(qū)(Heap Region,HR)
分區(qū)類(lèi)型
- 自由分區(qū)(Free Heap Region,F(xiàn)HR)
- 新生代分區(qū)(Young Heap Region,YHR)
- Eden
- Survivor
- 大對(duì)象分區(qū)(Humongous Heap Region,HHR)
- 大對(duì)象頭分區(qū)
- 大對(duì)象連續(xù)分區(qū)
- 老年代分區(qū)(Old Heap Region,OHR)
分區(qū)大小設(shè)置(1M ~ 32M,且為2的冪)
HR的大小直接影響分配和垃圾回收的效率。
大,HR可以放更多對(duì)象,分配效率高,回收花費(fèi)時(shí)間過(guò)長(zhǎng)
小,分配效率低,易回收
HR大小分配方式
- 配置參數(shù) G1HeapRegionSize,默認(rèn)值為0
- 當(dāng) G1HeapRegionSize 為0,則開(kāi)啟啟發(fā)式推斷
啟發(fā)式推斷
- 依據(jù) 堆空間的最大值和最小值以及HR個(gè)數(shù)進(jìn)行推斷
- 設(shè)置Initial HeapSize(默認(rèn)為0)等價(jià)于設(shè)置Xms
- 設(shè)置MaxHeapSize(默認(rèn)為96M)等價(jià)于設(shè)置Xmx
- 計(jì)算大小的方式在HeapRegion.cpp中的setup_heap_region_size()
- 由于分區(qū)大小需要落在1M~32M之間,按照默認(rèn)的分區(qū)個(gè)數(shù)(2048個(gè))來(lái)計(jì)算,最大內(nèi)存為 64G,最小內(nèi)存為2G
- 例如設(shè)置xms = 32G,xmx=128G,則xms算出的HR大小為 16M,xmx算出的分區(qū)大小為 64M > 32M,所以設(shè)置為32M,兩者取最大值,所以HR大小為32M。所以分區(qū)個(gè)數(shù)動(dòng)態(tài)范圍變化為1024個(gè)到4096個(gè)之間。
- 由于分區(qū)大小需要落在1M~32M之間,按照默認(rèn)的分區(qū)個(gè)數(shù)(2048個(gè))來(lái)計(jì)算,最大內(nèi)存為 64G,最小內(nèi)存為2G
大對(duì)象
- 算出HR大小后,就可以根據(jù)HR大小來(lái)判斷大對(duì)象,即只要 >= 1/2 heap_region_size 的都為大對(duì)象
新生代大小分配
- 直接設(shè)置 MaxNewSize (新生代最大值),NewSize(新生代最小值)
- 如果設(shè)置了Xmn參數(shù),等價(jià)于設(shè)置了 MaxNewSize = NewSize = Xmn
- 如果既設(shè)置了最大值或最小值,又設(shè)置了NewRatio,則NewRatio不生效
- 如果沒(méi)設(shè)置最大值或最小值,但是設(shè)置了NewRatio,則 MaxNewSize = NewSize = 堆空間/(NewRatio+1)
- 如果沒(méi)設(shè)置最大值或最小值,或只設(shè)置了其中一個(gè),那么G1將根據(jù)參數(shù)G1MaxNewSizePercent(默認(rèn)為60)和G1NewSizePercent(默認(rèn)為5)占整個(gè)堆空間的比例來(lái)計(jì)算最大最小值。
- 如果MaxNewSize == NewSize,則說(shuō)明新生代不會(huì)動(dòng)態(tài)變化,在后續(xù)堆新生代垃圾回收的時(shí)候可能不能滿足期望停頓的時(shí)間。
新生代的變化如何實(shí)現(xiàn)
- G1有個(gè)線程專(zhuān)門(mén)抽樣處理預(yù)測(cè)新生代列表的長(zhǎng)度應(yīng)該多大,并動(dòng)態(tài)調(diào)整
- 使用分區(qū)列表。
- 擴(kuò)展時(shí)
- 如果有空閑的分區(qū)列表,則可以直接把空閑分區(qū)加入到新生代分區(qū)列表中。
- 如果沒(méi)有的話,分配新的分區(qū)然后把它加入新生代分區(qū)列表中。
- 擴(kuò)展時(shí)
分配新的分區(qū)時(shí),如何擴(kuò)展,一次拓展多少內(nèi)存
- 參數(shù) -XX:GCTimeRatio 表示GC與應(yīng)用的耗費(fèi)時(shí)間比,G1默認(rèn)是9
- 當(dāng)GC時(shí)間/應(yīng)用時(shí)間超過(guò)(GCTimeRatio+1)% 時(shí),就可以動(dòng)態(tài)擴(kuò)展;按照默認(rèn)值,這個(gè)比例為10%
- 擴(kuò)展的比例由G1ExpandByPercentOfAvailable(默認(rèn)為20)控制;即每次從未提交的內(nèi)存中申請(qǐng)20%
- 一次拓展的內(nèi)存不能小于1M,最多是目前已分配的一倍。
G1停頓預(yù)測(cè)模型
G1是個(gè)響應(yīng)時(shí)間優(yōu)先的GC算法
- 參數(shù)MaxGCPauseMills控制(默認(rèn)值200ms),該值為期望值。G1會(huì)盡可能靠近這個(gè)期望值,但是也有可能完不成。
- G1根據(jù)這個(gè)模型來(lái)分析,這次需要回收多少個(gè)分區(qū),可以滿足這個(gè)期望值。 例如過(guò)去N次回收時(shí)間和回收分區(qū)數(shù)量之間的關(guān)系。
- G1利用衰減平均算法,給越近的數(shù)據(jù)以更高的權(quán)重,來(lái)計(jì)算數(shù)據(jù)的平均值。
卡表和位圖
- 卡表(CardTable):在CMS中用來(lái)記錄內(nèi)存對(duì)象應(yīng)用關(guān)系。
- 位圖(bitmap)
- 設(shè)有HR1和HR2,HR1中有對(duì)象A,HR2中有對(duì)象B,且A.obj = B,這時(shí)候兩個(gè)HR就有引用關(guān)系了。這時(shí)候,我們?cè)贖R1中,如何能引用到HR2呢?這時(shí)候位圖就登場(chǎng)了。
- 設(shè)置位圖的方法,記錄兩個(gè)內(nèi)存分區(qū)之間的引用關(guān)系。假設(shè)在32位的機(jī)器上(一個(gè)字為32位),需要32KB的空間來(lái)描述一個(gè)分區(qū)。那么我們就在A中添加一個(gè)額外的指針,這個(gè)指針指向B的位圖。從這個(gè)指針指向的位圖就能找到被A引用的HR2對(duì)應(yīng)的內(nèi)存塊。這時(shí)候我們只需要判斷位圖里對(duì)應(yīng)的位是否有1,有的話則認(rèn)為發(fā)生了引用。
- 設(shè)有HR1和HR2,HR1中有對(duì)象A,HR2中有對(duì)象B,且A.obj = B,這時(shí)候兩個(gè)HR就有引用關(guān)系了。這時(shí)候,我們?cè)贖R1中,如何能引用到HR2呢?這時(shí)候位圖就登場(chǎng)了。
對(duì)象頭
- java代碼首先被編譯為字節(jié)碼,在JVM執(zhí)行的時(shí)候,才能確定執(zhí)行函數(shù)的地址,通過(guò)把java對(duì)象映射/封裝成一個(gè)C++對(duì)象。比如加一個(gè)對(duì)象頭,里面指向一個(gè)對(duì)象,而這個(gè)對(duì)象存儲(chǔ)了java代碼的地址。
- JVM設(shè)計(jì)了一個(gè)對(duì)象結(jié)構(gòu)來(lái)描述java對(duì)象,結(jié)構(gòu)分為 對(duì)象頭(Header),實(shí)例數(shù)據(jù)(Instance Data),對(duì)齊填充(Padding)
- 虛指針: 虛指針指向一個(gè)虛表,虛表里存的是虛函數(shù)地址。
對(duì)象頭分為兩部分:標(biāo)記信息,元數(shù)據(jù)信息
- jvm中的java對(duì)象都是繼承于oopDesc
class oopDesc {
friend class VMStructs;
friend class JVMCIVMStructs;
private:
// 對(duì)象頭
volatile markOop _mark;
// 元數(shù)據(jù)
union _metadata {
// 對(duì)應(yīng)的Klass對(duì)象
Klass* _klass;
narrowKlass _compressed_klass;
} _metadata;
- 標(biāo)記信息就位于 markOop,java對(duì)象頭的位格式如下
32 bits:
--------
hash:25 ------------>| age:4 biased_lock:1 lock:2 (normal object)
JavaThread*:23 epoch:2 age:4 biased_lock:1 lock:2 (biased object)
size:32 ------------------------------------------>| (CMS free block)
PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)
64 bits:
--------
unused:25 hash:31 -->| unused:1 age:4 biased_lock:1 lock:2 (normal object)
JavaThread*:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2 (biased object)
PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
size:64 ----------------------------------------------------->| (CMS free block)
unused:25 hash:31 -->| cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && normal object)
JavaThread*:54 epoch:2 cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && biased object)
narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object)
unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block)
- age:分代年齡
- biased_lock:是否偏向鎖(1是0非)
- lock:鎖狀態(tài)標(biāo)志位
- 當(dāng)lock為
11時(shí),指針配合對(duì)象晉升時(shí)候發(fā)生的復(fù)制:- 當(dāng)新生代晉升為老年代時(shí)
- 先分配空間
- 再把原有對(duì)象的所有數(shù)據(jù)都復(fù)制過(guò)去
- 最后修改對(duì)象引用指針
- lock設(shè)置為marked,即
11,表示對(duì)象已經(jīng)被標(biāo)記復(fù)制了,ptr指向新的地址。 - 當(dāng)遍歷其他引用對(duì)象時(shí),如果發(fā)現(xiàn)被引用對(duì)象已經(jīng)完成標(biāo)記,則不用再?gòu)?fù)制對(duì)象,直接完成對(duì)象引用的更新即可。
- 當(dāng)lock為
- promoted:當(dāng)對(duì)象從新生代晉升到老年代的時(shí)候,如果晉升失敗,需要重新恢復(fù)對(duì)象頭。如果晉升成功,則promo_bits沒(méi)有意義。實(shí)際上只需要在以下三種情況時(shí)才需要保存對(duì)象頭:
- 使用了偏向鎖,并且偏向鎖被設(shè)置了。
- 對(duì)象被上鎖了
- 對(duì)象設(shè)置了hashcode
- 原數(shù)據(jù)信息
- 指向Klass對(duì)象,Klass對(duì)象是元數(shù)據(jù)對(duì)象。
- GC在根結(jié)點(diǎn)發(fā)現(xiàn)了一個(gè)值(例如0x12345678),那么JVM如何判斷這個(gè)是個(gè)立即數(shù)還是地址呢。實(shí)際上垃圾回收器無(wú)法判斷。
- JVM會(huì)將這個(gè)值看成一個(gè)地址,轉(zhuǎn)換成OOP對(duì)象,再看看這個(gè)OOP是否含有Klass指針,如果有的話,認(rèn)為這個(gè)值是個(gè)指針,否則則認(rèn)為是個(gè)數(shù)。
- 如果這個(gè)數(shù)正好和一個(gè)OOP地址相同,JVM同時(shí)維護(hù)了一個(gè)全局的OopMap,標(biāo)記棧里的數(shù)是立即數(shù)還是個(gè)值。
- 每個(gè)InstanceKlass都維護(hù)了這個(gè)map(OopMapBlock)用于標(biāo)記是OOP還是立即數(shù)。
內(nèi)存分配和管理
JVM如何管理內(nèi)存的
- JVM通過(guò)操作系統(tǒng)的系統(tǒng)調(diào)用(System Call)申請(qǐng),典型的就是mmap。
- 內(nèi)存只能以頁(yè)(Page Size)的方式來(lái)映射,如果映射非Page Size整數(shù)倍的,就先進(jìn)行內(nèi)存對(duì)齊,再以Page Size的倍數(shù)進(jìn)行映射。
- 告知操作系統(tǒng),需要為其保留(reserve)一段連續(xù)的虛擬內(nèi)存,進(jìn)程其他分配內(nèi)存的操作不得使用這段內(nèi)存。
- 提交(commit)虛擬地址,映射到真實(shí)的物理地址內(nèi)存中,這塊內(nèi)存就可以正常使用。
JVM常見(jiàn)的對(duì)象類(lèi)型
- ResourceObj:線程有個(gè)資源空間(Resource Area),里面存放的就是ResourceObj,用于對(duì)JVM提供其他功能的支持。
- StackObj:棧對(duì)象。
- ValueObj:值對(duì)象。在堆對(duì)象需要嵌套時(shí)使用
- AllStatic:靜態(tài)對(duì)象,全局對(duì)象,只有一個(gè)。JVM中的靜態(tài)對(duì)象的初始化,都是顯式調(diào)用靜態(tài)初始化函數(shù)。
- MetaspaceObj:元對(duì)象。例如InstanceKlass
- CHeapObj:堆空間的對(duì)象,由new/delete/free/malloc管理。
線程
JVM線程結(jié)構(gòu)類(lèi)

image.png
- JavaThread:執(zhí)行java代碼的線程
- java代碼的啟動(dòng)會(huì)通過(guò)JNI_CreateJavaVM創(chuàng)建一個(gè)JavaThread運(yùn)行
- java的一般線程通過(guò)調(diào)用 Thread 中的start()方法,start()方法再通過(guò)JNI調(diào)用創(chuàng)建JavaThread對(duì)象。
- CompilerThread:執(zhí)行JIT的線程
- WatcherThread:執(zhí)行周期性任務(wù),例如JVM內(nèi)存抽樣等
- NameThread:JVM內(nèi)部線程
- VMThread:JVM執(zhí)行GC的同步線程,主要用于處理垃圾回收。
- 如果是多線程回收,則啟動(dòng)多個(gè)線程。
- 如果是單線程回收,則使用VMThread
- ConcurrentGCThread:并發(fā)執(zhí)行GC任務(wù)的線程
- WorkerThread:工作線程,在G1中使用了FlexibleWorkGang,這個(gè)線程是并行執(zhí)行的,可以認(rèn)為是一個(gè)線程池
JVM線程狀態(tài)

image.png
棧幀(frame)
棧幀是虛擬機(jī)棧的組成元素,
-
棧幀所包含的元素:
img 在GC第一步就是遍歷根,棧幀就是根元素之一,通過(guò)StactFrameStream遍歷根元素。
句柄(handle)
jvm通過(guò)線程的資源區(qū)(handleArea)來(lái)管理所有的句柄。如果函數(shù)還在調(diào)用,那么句柄有效,句柄關(guān)聯(lián)的對(duì)象也是活躍對(duì)象。
句柄的作用主要是用于管理本地代碼對(duì)堆上資源的調(diào)用。
如何管理句柄的生命周期
- JVM引入HandleMark,通常HandleMark分配在棧上,在創(chuàng)建HandleMark的時(shí)候標(biāo)記HandleArea有效
- 在HandleMark析構(gòu)的時(shí)候,從HandleArea中刪除這個(gè)對(duì)象的引用。
- 所有的句柄都形成了一個(gè)鏈表,那么訪問(wèn)這個(gè)句柄鏈表就可以獲得本地代碼執(zhí)行中對(duì)堆對(duì)象的引用。
G1參數(shù)介紹和注意事項(xiàng)
- G1HeapRegionSize:指定堆分區(qū)大小。分區(qū)大小可以指定,也可以不指定;不指定時(shí),由內(nèi)存管理器啟發(fā)式推斷分區(qū)大小。
- xms/xmx:指定堆空間的最小值/最大值。一定要正確設(shè)置xms/xmx,否則將使用默認(rèn)配置,將影響分區(qū)大小推斷。
- 在以前的內(nèi)存管理器中(非G1),為了防止新生代因?yàn)閮?nèi)存不斷地重新分配導(dǎo)致性能變低,通常設(shè)置Xmn或者NewRatio。但是G1中不要設(shè)置MaxNewSize、NewSize、Xmn和NewRatio。原因有兩個(gè),第一G1對(duì)內(nèi)存的管理不是連續(xù)的,所以即使重新分配一個(gè)堆分區(qū)代價(jià)也不高,第二也是最重要的,G1的目標(biāo)滿足垃圾收集停頓,這需要G1根據(jù)停頓時(shí)間動(dòng)態(tài)調(diào)整收集的分區(qū),如果設(shè)置了固定的分區(qū)數(shù),即G1不能調(diào)整新生代的大小,那么G1可能不能滿足停頓時(shí)間的要求。具體情況本書(shū)后續(xù)還會(huì)繼續(xù)討論。
- GCTimeRatio指的是GC與應(yīng)用程序之間的時(shí)間占比,默認(rèn)值為9,表示GC與應(yīng)用程序時(shí)間占比為10%。增大該值將減少GC占用的時(shí)間,帶來(lái)的后果就是動(dòng)態(tài)擴(kuò)展內(nèi)存更容易發(fā)生;在很多情況下10%已經(jīng)很大,例如可以將該值設(shè)置為19,則表示GC時(shí)間不超過(guò)5%。
- 根據(jù)業(yè)務(wù)請(qǐng)求變化的情況,設(shè)置合適的擴(kuò)展G1ExpandByPercentOfAvailable速率,保持效率。
- JVM在對(duì)新生代內(nèi)存分配管理時(shí),還有一個(gè)參數(shù)就是保留內(nèi)存G1ReservePercent(默認(rèn)值是10),即在初始化,或者內(nèi)存擴(kuò)展/收縮的時(shí)候會(huì)計(jì)算更新有多少個(gè)分區(qū)是保留的,在新生代分區(qū)初始化的時(shí)候,在空閑列表中保留一定比例的分區(qū)不使用,那么在對(duì)象晉升的時(shí)候就可以使用了,所以能有效地減小晉升失敗的概率。這個(gè)值最大不超過(guò)50,即最多保留50%的空間,但是保留過(guò)多會(huì)導(dǎo)致新生代可用空間少,過(guò)少可能會(huì)增加新生代晉升失敗,那將會(huì)導(dǎo)致更為復(fù)雜的串行回收。
- G1NewSizePercent是一個(gè)實(shí)驗(yàn)參數(shù),需要使用 -XX:+UnlockExperimentalVMOptions 才能改變選項(xiàng)。有實(shí)驗(yàn)表明G1在回收Eden分區(qū)的時(shí)候,大概每GB需要100ms,所以可以根據(jù)停頓時(shí)間,相應(yīng)地調(diào)整。這個(gè)值在內(nèi)存比較大的時(shí)候需要減少,例如32G可以設(shè)置-XX:G1NewSizePercent = 3,這樣Eden至少保留大約1GB的空間,從而保證收集效率。
- MaxGCPauseMillis指期望停頓時(shí)間,可根據(jù)系統(tǒng)配置和業(yè)務(wù)動(dòng)態(tài)調(diào)整。因?yàn)镚1在垃圾收集的時(shí)候一定會(huì)收集新生代,所以需要配合新生代大小的設(shè)置來(lái)確定,如果該值太小,連新生代都不能收集完成,則沒(méi)有任何意義,每次除了新生代之外只能多收集一個(gè)額外老生代分區(qū)。
- 參數(shù)GCPauseIntervalMillisGC指GC間隔時(shí)間,默認(rèn)值為0,GC啟發(fā)式推斷為MaxGCPauseMillis + 1,設(shè)置該值必須要大于MaxGCPauseMillis。
- 參數(shù)G1ConfidencePercent指GC預(yù)測(cè)置信度,該值越小說(shuō)明基于過(guò)去歷史數(shù)據(jù)的預(yù)測(cè)越準(zhǔn)確,例如設(shè)置為0則表示收集的分區(qū)基本和過(guò)去的衰減均值相關(guān),無(wú)波動(dòng),所以可以根據(jù)過(guò)去的衰減均值直接預(yù)測(cè)下一次預(yù)測(cè)的時(shí)間。反之該值越大,說(shuō)明波動(dòng)越大,越不準(zhǔn)確,需要加上衰減方差來(lái)補(bǔ)償。
- JVM中提供了一個(gè)對(duì)象對(duì)齊的值ObjectAlignmentInBytes,默認(rèn)值為8,需要明白該值對(duì)內(nèi)存使用的影響,這個(gè)影響不僅僅是在JVM對(duì)對(duì)象的分配上面,正如上面看到的它也會(huì)影響對(duì)象在分配時(shí)的標(biāo)記情況。注意這個(gè)值最少要和操作系統(tǒng)支持的位數(shù)一致才能提高對(duì)象分配的效率。所以32位系統(tǒng)最少是4,64位最少是8。一般不用修改該值。