資料:
https://zhuanlan.zhihu.com/p/45558897
https://tech.meituan.com/2017/12/29/jvm-optimize.html
虛擬機內(nèi)存區(qū)域劃分
堆 (heap)
虛擬機管理內(nèi)存中最大的一塊。在虛擬機啟動的時候創(chuàng)建殃恒。目的就是存對象實例
方法區(qū) (Method Area)
JVM常量池主要分為Class文件常量池植旧、運行時常量池,全局字符串常量池离唐,以及基本類型包裝類對象常量池。
JDK1.6及之前亥鬓,運行時常量池是方法區(qū)的一個部分覆积,同時方法區(qū)里面存儲了類的元數(shù)據(jù)信息技健、靜態(tài)變量、即時編譯器編譯后的代碼等偿短。
JDK1.7及以后,JVM已經(jīng)將運行時常量池從方法區(qū)中移了出來降传,在JVM堆開辟了一塊區(qū)域存放常量池婆排。字符串常量池被移到了堆中了段只。此時常量池存儲的就是引用了赞枕。JDK1.8以后方法區(qū)在元空間坪创,元空間在本地內(nèi)存柠掂。
程序計數(shù)器(Program counter Register)
內(nèi)存中比較小的一塊內(nèi)存區(qū)域,作用是當前線程所執(zhí)行的字節(jié)碼的行號指示器涯贞。此內(nèi)存區(qū)域是唯一一個在Java虛擬機規(guī)范中沒有規(guī)定任何OutOfMemoryError情況的區(qū)域悉抵。
jvm棧(jvm stacks)
虛擬機描繪java方法執(zhí)行的內(nèi)存模型傻谁。每個方法執(zhí)行的時候 都會創(chuàng)建一個幀棧(Stack Frame)用于存儲局部變量表审磁,操作棧态蒂,動態(tài)鏈接方法出口信息钾恢。
棧幀
首先 一個線程對應(yīng)一個棧幀瘩蚪, jvm調(diào)用一個java方法的時候,他對應(yīng)的類的類型信息中得到這個方法的局部變量區(qū)和操作數(shù)棧的大小崩哩。并依據(jù)這個進行分配棧幀內(nèi)存。壓入jvm棧中险胰。 在活動線程中鸯乃,只有在棧頂?shù)臈攀怯行У挠环Q為當前棧幀细诸。與這個棧幀關(guān)聯(lián)的方法被稱為當前方法 棧幀存儲了方法的局部變量表陋守、操作數(shù)棧猩系、動態(tài)連接和方法返回地址等信息中燥。
每一個方法從調(diào)用開始至執(zhí)行完成的過程拿霉,都對應(yīng)著一個棧幀在虛擬機里面從入棧到出棧的過程咱扣。 在編譯程序代碼的時候,棧幀中需要多大的局部變量表沪铭,多深的操作數(shù)棧都已經(jīng)完全確定了伦意。因此一個棧幀需要分配多少內(nèi)存驮肉,不會受到程序運行期變量數(shù)據(jù)的影響离钝,而僅僅取決于具體的虛擬機實現(xiàn)。
本地方法棧 (Naitve Methord Stacks)
是與虛擬機棧發(fā)揮作用非常相似浪读, 虛擬機棧為虛擬機執(zhí)行java方法辛藻, 而本地方法棧為虛擬機使用到的Native 方法服務(wù)痘拆。
GC相關(guān)
GC工作區(qū)域
GC的主要工作是在Heap (堆)和 metaSpace(元空間中的方法區(qū))纺蛆。如果在Direct Memory(直接內(nèi)存 ) 如果使用的是 DirectByteBuffer桥氏,那么在分配內(nèi)存不夠時則是 GC 通過 Cleaner#clean 間接管理。
為什么回收主要在堆和方法區(qū)
Java虛擬機棧祥款、本地方法棧刃跛、程序計數(shù)器這三者是線程私有的桨昙,隨線程而生隨線程而滅。棧中的棧幀隨著方法的進入和退出有條不紊的出棧入棧齐苛,每個棧幀需要分配多少內(nèi)存桂塞,在類結(jié)構(gòu)確定下來時就是已知的(盡管運行期間會有JIT編譯器進行一些優(yōu)化玛痊,但在基于概念模型的討論中擂煞,大體可以認為是編譯器可知的)。因此上述這些區(qū)域的內(nèi)存分配和回收都具備確定性蒿涎,故不需要過多考慮垃圾回收的問題同仆。而Java堆和方法區(qū)則不一樣:一個接口中的多個實現(xiàn)類需要的內(nèi)存可能不一樣俗批,一個方法中的多個分支所需的內(nèi)存也不一樣岁忘,我們只有在程序運行期間才知道會創(chuàng)建哪些對象干像,所以這部分內(nèi)存的分配和回收都是動態(tài)的驰弄,所以垃圾回收器所關(guān)注的重點是位于Java堆和方法區(qū)上的內(nèi)存麻汰。
算法決生死(java對象生死判斷算法)
引用計數(shù)法(Reference Counting)
對每個對象的引用進行計數(shù),每當有一個地方引用它時計數(shù)器 +1戚篙、引用失效則 -1五鲫,引用的計數(shù)放到對象頭中,大于 0 的對象被認為是存活對象岔擂。雖然循環(huán)引用的問題可通過 Recycler 算法解決位喂,但是在多線程環(huán)境下浪耘,引用計數(shù)變更也要進行昂貴的同步操作,性能較低塑崖,早期的編程語言會采用此算法规婆。引用計數(shù)法是可以處理循環(huán)引用問題的削锰。
可達性分析蛹稍,又稱引用鏈法(Tracing GC)
從 GC Root 開始進行對象搜索奉芦,可以被搜索到的對象即為可達對象先巴,此時還不足以判斷對象是否存活/死亡,需要經(jīng)過多次標記才能更加準確地確定囚企,整個連通圖之外的對象便可以作為垃圾被回收掉。目前 Java 中主流的虛擬機均采用此算法 此算法的基本思路就是通過一系列的“GC Roots”的對象作為起始點,從起始點開始向下搜索到對象的路徑旁涤。搜索所經(jīng)過的路徑稱為引用鏈(Reference Chain)菌羽,當一個對象到任何GC Roots都沒有引用鏈時氓轰,則表明對象“不可達”时捌,即該對象是不可用的扒袖。
GC Root 可以使用的對象
棧幀中的局部變量表中的reference引用所引用的對象
方法區(qū)中類static靜態(tài)引用的對象
方法區(qū)中final常量引用的對象
本地方法棧中JNI(Native方法)引用的對象
對象自我救贖
對象經(jīng)過可達性算法分析后泞遗,判斷為不可達,那么對象就必死無疑了么障斋?不一定劲赠,對象在面臨垃圾回收器的處理時塑煎,還有最后一次求生的機會系枪。
要kill掉一個對象当犯,至少要經(jīng)過垃圾回收器的2次標記過程,不可達的對象被第一次標記后會進行一次篩選抚芦,篩選的條件是「此對象是否有必要執(zhí)行finalize()方法」褥民,當對象沒有覆蓋finalize方法或者已經(jīng)執(zhí)行過finalize方法時,會被判斷為:沒必要執(zhí)行。如果被判斷為有必要執(zhí)行法严,則該對象會被放置在一個F-Queue隊列呈昔,并在稍后虛擬機建立的Finalizer線程中執(zhí)行finalize()來kill掉對象。在回收前垃圾回收器會對F-Queue隊列中的對象進行第二次標記友绝,如果在標記前堤尾,對象成功與引用鏈上的任意對象建立了關(guān)聯(lián),則會在第二次標記時被移出F-Queue迁客,從而實現(xiàn)自救哀峻。
GC垃圾收集算法
Mark-Sweep(標記-清除)
回收過程主要分為兩個階段,第一階段為追蹤(Tracing)階段哲泊,即從 GC Root 開始遍歷對象圖,并標記(Mark)所遇到的每個對象催蝗,第二階段為清除(Sweep)階段切威,即回收器檢查堆中每一個對象,并將所有未被標記的對象進行回收丙号,整個過程不會發(fā)生對象移動先朦。整個算法在不同的實現(xiàn)中會使用三色抽象(Tricolour Abstraction)、位圖標記(BitMap)等技術(shù)來提高算法的效率犬缨,存活對象較多時較高效喳魏。
缺點:
效率較低,因為標記和清除這兩個過程效率都比較低
空間問題,標記清除后會產(chǎn)生大量不聯(lián)系的內(nèi)存空間(碎片)怀薛,導(dǎo)致如果有大內(nèi)存的對象刺彩,那么就無法找到足夠大的連續(xù)內(nèi)存空間以供分配。
Mark-Compact (標記-整理/壓縮)
算法的主要目的就是解決在非移動式回收器中都會存在的碎片化問題,也分為兩個階段创倔,第一階段與 Mark-Sweep 類似嗡害,第二階段則會對存活對象按照整理順序(Compaction Order)進行整理。主要實現(xiàn)有雙指針(Two-Finger)回收算法畦攘、滑動回收(Lisp2)算法和引線整理(Threaded Compaction)算法等霸妹。在青年代采用復(fù)制算法是非常合適的,因為青年代的特點是對象數(shù)量多知押,生存時間短叹螟,所以空間利用率比較重要,而復(fù)制算法對于老年代Old Generation則不太適合台盯,因為老年代的對象數(shù)量雖少罢绽,但比較穩(wěn)定存活率高這樣會有較多的復(fù)制開銷,針對這種情況爷恳,出現(xiàn)了標記-壓縮算法有缆。標記-壓縮算法和標記-清除算法類似,先通過標記找出等待回收的對象温亲,然后在清除之前將存活的對象都整理整齊放到一邊棚壁,然后再清除掉邊界以外的內(nèi)存。
復(fù)制算法
將完整內(nèi)存區(qū)域分為大小相等的2塊栈虚,每次只使用其中的一塊袖外,當這塊內(nèi)存滿了(用完),則將此塊內(nèi)存上的對象都「復(fù)制」到另一塊空內(nèi)存上去魂务,然后將用完的那塊內(nèi)存進行垃圾回收曼验。
優(yōu)點 吞吐量高,不需要遍歷全堆粘姜,只需要處理活動對象鬓照。不會有碎片化的問題,因為每次復(fù)制都將存活對象從from復(fù)制到to的一端
缺點 堆利用率較低孤紧,因為在復(fù)制算法下豺裆,只有一半的內(nèi)存用來存儲對象
分代收集
Java堆是垃圾收集器管理的主要內(nèi)存,由于主流的虛擬機實現(xiàn)中号显,垃圾收集器大多采用分代式垃圾回收算法(Generational Garbage Collection)臭猜,所以會將垃圾收集器所管理的堆內(nèi)存劃分為不同的代。
在Java7以前Hotspot虛擬機中將Java堆內(nèi)存分為3個部分:
青年代 Young Generation
老年代 Old Generation
永久代(1.8刪除) Permanent Generation
在Java8以后押蚤,由于方法區(qū)的內(nèi)存不在分配在Java堆上蔑歌,而是存儲于本地內(nèi)存元空間Metaspace中,所以永久代就不存在了揽碘。
[圖片上傳失敗...(image-b01c3b-1630309095177)]
青年代(新生代)
其中青年代中單獨劃分成了三塊——Eden+Survivor+Survivor次屠。大部分對象在Eden區(qū)中生成园匹。在這些不同區(qū)域上任何一個內(nèi)存“滿”了以后,都會觸發(fā)一次垃圾收集過程帅矗。Java中絕大部分的新創(chuàng)建的對象都被分配到了青年代中的Eden區(qū)偎肃。當內(nèi)存不夠時,虛擬機將會發(fā)動一次MinorGC浑此。
晉升老年代
當Eden區(qū)滿時累颂,還存活的對象將被復(fù)制到兩個Survivor區(qū)(中的一個)。當這個Survivor區(qū)滿時凛俱,此區(qū)的存活且不滿足“晉升”條件的對象將被復(fù)制到另外一個Survivor區(qū)紊馏。對象每經(jīng)歷一次Minor GC,年齡加1蒲犬,達到“晉升年齡閾值”后朱监,被放到老年代,這個過程也稱為“晉升”原叮。顯然赫编,“晉升年齡閾值”的大小直接影響著對象在新生代中的停留時間,在Serial和ParNew GC兩種回收器中奋隶,“晉升年齡閾值”通過參數(shù)MaxTenuringThreshold設(shè)定擂送,默認值為15。 為了更好地適應(yīng)不同程序的內(nèi)存狀況唯欣,虛擬機并不是永遠要求對象年齡必須達到「閾值」才能提升至老年代嘹吨。在有的垃圾收集器實現(xiàn)中,如果Survivor空間中相同年齡的對象占用空>Survivor總空間的一半境氢,則此年齡的所有對象就可以提前進入老年代蟀拷,而不是必須達到閾值。
老年代
大對象直接進入老年代 所謂的大對象是指需要大量連續(xù)內(nèi)存空間的Java對象萍聊,長字符串及大容量的數(shù)組问芬。安放這些大對象,虛擬機會直接將其放在老年代寿桨,因為大對象一般涉及到的引用多愈诚,不容易「死」掉。而且大對象占內(nèi)存牛隅,所以直接在老年代為其開辟一塊連續(xù)的內(nèi)存就比較合適。如果內(nèi)存不夠分配酌泰,虛擬機會觸發(fā)垃圾收集過程媒佣。 長期存活的對象進入老年代 既然虛擬機采用分代收集的策略來管理內(nèi)存,那么內(nèi)存回收時就應(yīng)該相應(yīng)的判別哪些對象該放在青年代陵刹,哪些放在老年代默伍。為此,JVM給每個對象定義了一個年齡計數(shù)器。如果對象在Eden出生也糊,并且經(jīng)過一次MinorGC后仍然存在炼蹦,則「年齡」增加1歲。當年齡增加到一定數(shù)目(如:默認為15歲)狸剃,就會被提升至老年代掐隐。 MinorGC 在MinorGC之前,JVM會首先檢查老年代最大可用的連續(xù)內(nèi)存空間是否 > 青年代所有對象總空間钞馁,并以其作為MajorGC執(zhí)行的「擔甭鞘。」。如果大于則MinorGC可以正常執(zhí)行僧凰。否則JVM會查看HandlePromotionFailure設(shè)置值是否允許擔保失敗探颈,如果允許,則繼續(xù)執(zhí)行MinorGC训措,否則則執(zhí)行MajorGC用來回收足夠的內(nèi)存空間伪节。 在年輕代內(nèi)存區(qū)域上的垃圾收集過程,因為大多數(shù)在年輕代上的對象“朝生夕滅”绩鸣,所以MinorGC非常頻繁怀大,一般收集的速度也很快。
MajorGC/Full GC 指發(fā)生在老年代的GC全闷,此區(qū)域一般對象存活率高叉寂,GC一次的速度同城比MinorGC慢10倍以上。
總結(jié) 可以晉升老年代的類型
1总珠、分配擔保機制
Eden區(qū)滿時屏鳍,進行Minor GC,當Eden和一個Survivor區(qū)中依然存活的對象無法放入到Survivor中局服,則通過分配擔保機制提前轉(zhuǎn)移到老年代中钓瞭。
2、對象過大
若對象體積太大淫奔,新生代無法容納這個對象山涡,就會繞過新生代, 直接在老年代分配, 此參數(shù)只對Serial及ParNew兩款收集器有效。
參數(shù)-XX:PretenureSizeThreshold用來設(shè)置這個門限值唆迁。
3鸭丛、長期存活的對象
對象頭的Mark Word中包含對象的年齡。當年齡增加到一定的臨界值時唐责,就會晉升到老年代中鳞溉。
該臨界值由參數(shù):-XX:MaxTenuringThreshold來設(shè)置,默認為15鼠哥,即對象在經(jīng)歷15次minor gc后會晉升到老年代熟菲。
4看政、動態(tài)對象年齡判定
如果在Survivor區(qū)中相同年齡的對象的所有大小之和超過Survivor空間的一半,年齡大于或等于該年齡的對象就可以直接進入老年代
CMS和G1
CMS(Concurent Mark Sweep)
從名字可以看出這款收集器是一款比較優(yōu)秀的基于標記-清除算法的并發(fā)收集器抄罕。之前也提到過允蚣,此收集器的目標在于盡量小的Stop The World間隔時間,用于用戶交互比較多的場景呆贿。
收集過程
初始標記
并發(fā)標記
重新標記
并發(fā)清除
其中初始標記和重新標記兩個步驟仍需要Stop The World間隔嚷兔。初始標記僅僅是標記一下GC Roots能直接關(guān)聯(lián)到的對象,速度很快榨崩。并發(fā)標記階段就是進行GC Roots追蹤的過程谴垫,而重新標記則是為了修正并發(fā)標記期間由于用戶程序繼續(xù)執(zhí)行可能產(chǎn)生變動的那部分對象的標記記錄,此階段會比初始標記長一些母蛛,但遠小于并發(fā)標記的時間翩剪。
整個階段并發(fā)標記和并發(fā)清除是耗時最長的兩個階段。但是由于CMS收集器是并發(fā)執(zhí)行的彩郊,故可以和用戶線程一起工作前弯,所以從整體上CMS收集器的工作過程是和用戶線程并發(fā)執(zhí)行的。
優(yōu)點:
GC收集間隔時間短秫逝,多線程并發(fā)恕出。
缺點:
并發(fā)時對CPU資源占用多,不適合CPU核心數(shù)較少的情況违帆。
且由于采用標記清除算法浙巫,所以會產(chǎn)生內(nèi)存碎片。
無法處理浮動垃圾刷后。
G1 (Garbage-First)
Java11官網(wǎng)描述中已經(jīng)說明:G1取代了Concurrent Mark-Sweep(CMS)收集器的畴。它也是默認的收集器。表明在Java11中G1是默認的垃圾收集器尝胆,而CMS收集器從JDK 9開始就不推薦使用了
G1特點:
-
并行與并發(fā):
G1能充分利用多CPU下的優(yōu)勢來縮短Stop The World的時間丧裁,同時在其他部分收集器需要停止Java線程來執(zhí)行GC動作時,G1收集器仍然可以通過并發(fā)來讓Java線程同步執(zhí)行含衔。
-
分代收集:
與其他收集器一樣煎娇,分代的概念在G1中任然被保留√叭荆可以不需要配合其他的垃圾收集器缓呛,就獨立管理整個Java堆內(nèi)存的所有分代區(qū)域,且采用不同的方式來獲得更好的垃圾收集效果杭隙。
-
空間整合:
G1從整體來看哟绊,使用的是標記-壓縮算法實現(xiàn)的,從局部兩個Region來看寺渗,采用的是復(fù)制算法實現(xiàn)的匿情,對內(nèi)存空間的利用非常高效,不會像CMS一樣產(chǎn)生內(nèi)存碎片信殊。
可以預(yù)測的停頓:
除了追求低停頓以外炬称,G1的停頓時間可以被指定在一個時間范圍內(nèi)。
如果不計算維護Remenbered Set的操作涡拘,G1收集器的工作階段大致區(qū)分如下:
初始標記
并發(fā)標記
最終標記
篩選回收