淘寶的技術(shù)團(tuán)隊(duì)對(duì)Java虛擬機(jī)的優(yōu)化工作其實(shí)早已不是停留在簡(jiǎn)單的參數(shù)調(diào)整上面颗搂,而是充分結(jié)合了企業(yè)自身的業(yè)務(wù)特點(diǎn)以及實(shí)際的應(yīng)用場(chǎng)景,在OpenJDK的基礎(chǔ)之上通過(guò)修改大量的HotSpot源代碼河狐,深度定制了淘寶專屬的高性能Linux虛擬機(jī)TAOBAOVM。從嚴(yán)格意義上來(lái)說(shuō)铛楣, 在提升Java 虛擬機(jī)性能的同時(shí)刀荒,嚴(yán)重依賴于物理CPU類型弱左。也就是說(shuō)蒜焊,部署有TAOBAOVM 的服務(wù)器中CPU ,全都是清一色的Intel CPU科贬,且編譯手段采用的是Intel C/CPP Compiler進(jìn)行編譯,以此對(duì)GC性能進(jìn)行提升鳖悠。除了優(yōu)化編譯效果外榜掌,TAOBAOVM 還使用crc32 指令降低 JNI 的調(diào)用開(kāi)銷。除了在性能優(yōu)化方面下足了功夫乘综, TAOBAOVM還在HotSpot 的基礎(chǔ)之上大幅度擴(kuò)充了一些特定的增強(qiáng)實(shí)現(xiàn)憎账,比如創(chuàng)新的GCIH (GC invisible heap)技術(shù)實(shí)現(xiàn)offheap,長(zhǎng)生命周期的大對(duì)象直接移到offheap卡辰,降低gc壓力胞皱。
引言主要說(shuō)明,淘寶的技術(shù)團(tuán)隊(duì)已經(jīng)需要在JVM層面著手優(yōu)化自己的性能九妈,可見(jiàn)其對(duì)性能的極致追求反砌,而目前自己在實(shí)踐過(guò)程中,關(guān)注的還是GC的選擇和參數(shù)的調(diào)優(yōu)萌朱。
簡(jiǎn)而言之宴树,JVM就是一臺(tái)執(zhí)行java字節(jié)碼的虛擬計(jì)算機(jī)。其運(yùn)行的java字節(jié)碼(可以是二進(jìn)制文件晶疼,也可以來(lái)自數(shù)據(jù)庫(kù)酒贬,甚至網(wǎng)絡(luò))未必由java語(yǔ)言編譯而成。作為面向?qū)ο蟮恼Z(yǔ)言翠霍,將對(duì)象的創(chuàng)建锭吨、使用與回收過(guò)程進(jìn)行職責(zé)分離,應(yīng)用程序只需關(guān)注前者寒匙,其執(zhí)行過(guò)程中產(chǎn)生的對(duì)象垃圾不再需要編程手動(dòng)回收零如,極大的降低了程序開(kāi)發(fā)難度,減少出錯(cuò),提升開(kāi)發(fā)效率埠况。
一個(gè)封閉系統(tǒng)總是朝著熵增的方向發(fā)展的耸携,必然越來(lái)越混亂,所以對(duì)于java來(lái)說(shuō)辕翰,GC是必然也必須的夺衍。
了解GC必須掌握J(rèn)VM,而掌握J(rèn)VM的關(guān)鍵從JMM內(nèi)存結(jié)構(gòu)喜命,GC算法沟沙,垃圾回收器,JVM調(diào)優(yōu)壁榕,類加載機(jī)制矛紫,ASM(字節(jié)碼)技術(shù)逐步深入。
我們開(kāi)發(fā)程序時(shí)必須對(duì)應(yīng)用進(jìn)程可能產(chǎn)生的stop the world(STW)有更好的感知牌里,這要求我們?cè)诰帉懘a時(shí)颊咬,必須根據(jù)業(yè)務(wù)場(chǎng)景預(yù)估虛擬機(jī)分配對(duì)象和調(diào)用對(duì)象的行為。如棋者過(guò)招牡辽,亦如智者看穿喳篇。
STW時(shí),GC事件發(fā)生過(guò)程中态辛,停止所有應(yīng)用線程的執(zhí)行麸澜。因此,當(dāng)你的程序突然執(zhí)行緩慢時(shí)奏黑,你首先就要考慮和觀察內(nèi)存使用情況炊邦。
應(yīng)用使用GC的架構(gòu)原則
XXX應(yīng)用部分進(jìn)程使用CMS,部分進(jìn)程使用G1熟史,沒(méi)有比較馁害,就沒(méi)有傷害。當(dāng)然這種比較取決于應(yīng)用場(chǎng)景蹂匹。要為真實(shí)場(chǎng)景選擇更合適的GC機(jī)制蜗细,將參數(shù)調(diào)整到最為合適。
使用CMS的應(yīng)用(管理端應(yīng)用怒详,客戶端應(yīng)用)炉媒,內(nèi)存占用一般設(shè)為小于等于8G,調(diào)度引擎內(nèi)存會(huì)設(shè)置為4G昆烁,查詢?yōu)橹鬟M(jìn)程一般設(shè)置為8G吊骤,應(yīng)當(dāng)關(guān)注大對(duì)象的分配和使用,也就是關(guān)注大數(shù)據(jù)量的導(dǎo)入和導(dǎo)出操作静尼。為什么要關(guān)注大對(duì)象下文會(huì)介紹白粉。
使用G1的應(yīng)用传泊,會(huì)開(kāi)啟超過(guò)8G的內(nèi)存空間,一般我們會(huì)使用16G(計(jì)算應(yīng)用:收益率計(jì)算引擎鸭巴、行情計(jì)算引擎眷细,搜索引擎,權(quán)限計(jì)算引擎等)鹃祖,或者32G(應(yīng)用數(shù)據(jù)庫(kù):Cassandra溪椎,智能推薦平臺(tái))。
使用G1時(shí)特別注意恬口,選擇的jdk版本不同校读,G1新增特性不同,如果需要針對(duì)性的特性祖能,請(qǐng)選擇合適的jdk版本歉秫。相對(duì)來(lái)講,jdk11之后养铸,G1已成熟雁芙。但jdk11和jdk8不兼容,不兼容钞螟。當(dāng)前的G1感覺(jué)有一統(tǒng)天下之趨勢(shì)却特。但是紅帽給我們的建議竟然是繼續(xù)使用CMS,比較無(wú)語(yǔ)筛圆。
JMM內(nèi)存結(jié)構(gòu)
JMM由方法區(qū),堆椿浓,Java棧太援,pc寄存器,本地方法棧扳碍,執(zhí)行引擎提岔,類裝載器組成。
JVM實(shí)例和JVM執(zhí)行引擎實(shí)例區(qū)別:JVM實(shí)例對(duì)應(yīng)一個(gè)獨(dú)立運(yùn)行的java程序笋敞,是進(jìn)程級(jí)的碱蒙,JVM執(zhí)行引擎實(shí)例則對(duì)應(yīng)屬于用戶運(yùn)行程序的線程,是線程級(jí)的夯巷。
當(dāng)程序中所有非守護(hù)線程終止時(shí)赛惩,JVM自動(dòng)退出,你可以向jvm中注冊(cè)一個(gè)hook趁餐,當(dāng)jvm自動(dòng)退出之前喷兼,會(huì)執(zhí)行這個(gè)片段。
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
public void run() {
System.out.println("auto clean temporary file");
}
}));
————————————————
版權(quán)聲明:本文為CSDN博主「江湖人稱小白哥」的原創(chuàng)文章后雷,遵循 CC 4.0 BY-SA 版權(quán)協(xié)議季惯,轉(zhuǎn)載請(qǐng)附上原文出處鏈接及本聲明吠各。
原文鏈接:https://blog.csdn.net/dd864140130/article/details/49155179
主線程:main啟動(dòng)時(shí)的線程,作為JVM實(shí)例運(yùn)行的起點(diǎn)勉抓。
非守護(hù)線程:用戶線程贾漏,由主線程或者其它線程創(chuàng)建。
守護(hù)線程:程序運(yùn)行時(shí)在后臺(tái)提供通用服務(wù)的線程藕筋,與主線程同時(shí)銷毀纵散。一般用于應(yīng)用或框架創(chuàng)建的監(jiān)控、調(diào)度或清理有關(guān)功能的線程念逞。
thread.setDaemon(true) 設(shè)置線程為守護(hù)線程困食。
程序計(jì)數(shù)器看作當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器,確保線程切換時(shí)能夠回到正確的執(zhí)行位置 翎承。 它是一個(gè)很小的空間硕盹,是運(yùn)行最快的區(qū)域,java應(yīng)用程序不能直接控制寄存器叨咖。
本地方法棧供C語(yǔ)言函數(shù)調(diào)用瘩例。當(dāng)java程序調(diào)用本地方法native時(shí),就進(jìn)入一個(gè)全新的不再受虛擬機(jī)限制的世界甸各。
戰(zhàn)幀由三部分組成垛贤,即局部變量區(qū)( Local Variables)、操作數(shù)棧( Operand Stack)和幀數(shù)據(jù)區(qū)( Frame Data)趣倾。
棧物理上是事先創(chuàng)建好的內(nèi)存區(qū)域聘惦,在jvm中是通過(guò)jit使用native stack。
當(dāng)虛擬機(jī)調(diào)用本地方法時(shí)儒恋,會(huì)保持本地棧幀不變
堆是所有線程共享的內(nèi)存區(qū)域善绎,是通用內(nèi)存,用于存放所有對(duì)象诫尽。
注意堆是邏輯連續(xù)的而不是物理連續(xù)的
理解JMM堆的關(guān)鍵是理解棧和堆的區(qū)別禀酱,堆和棧的代碼實(shí)現(xiàn)可以參考數(shù)據(jù)結(jié)構(gòu),非常簡(jiǎn)單牧嫉。
在JVM中剂跟,靜態(tài)屬性保存在Stack指令內(nèi)存區(qū),動(dòng)態(tài)屬性保存在Heap數(shù)據(jù)內(nèi)存區(qū)酣藻。
1)棧是運(yùn)行時(shí)的單位曹洽,而堆是存儲(chǔ)的單位。
2)棧解決程序的運(yùn)行問(wèn)題辽剧,即程序如何執(zhí)行衣洁,或者說(shuō)如何處理數(shù)據(jù);堆解決的是數(shù)據(jù)存儲(chǔ)的問(wèn)題抖仅,即數(shù)據(jù)怎么放坊夫、放在哪兒砖第。
理解堆內(nèi)存分析中兩個(gè)重要概念:淺堆和深堆
淺堆(Shallow Heap):對(duì)象所消耗的內(nèi)存。
深堆(Retained Heap) :對(duì)象被垃圾回收后可以釋放的內(nèi)存环凿。包含對(duì)象及對(duì)象引用的全部對(duì)象所消耗的內(nèi)存梧兼。特別注意,當(dāng)一個(gè)對(duì)象C被對(duì)象A,B同時(shí)引用時(shí)智听,A對(duì)象的深堆不包含對(duì)C的引用部分羽杰,因?yàn)镃還被B引用,不能因A直接被回收到推。
堆中有一個(gè)常量池
類文件中常量池(The Constant Pool)
運(yùn)行時(shí)常量池(The Run-Time Constant Pool)
String常量池
字符串常量池則存在于方法區(qū)
其工作在編譯時(shí)考赛,也就是說(shuō)如果都是靜態(tài)變量
字符串池里的內(nèi)容是在類加載完成,經(jīng)過(guò)驗(yàn)證莉测,準(zhǔn)備階段之后在堆中生成字符串對(duì)象實(shí)例颜骤,然后將該字符串對(duì)象實(shí)例的引用值存到string pool中(記住:string pool中存的是引用值而不是具體的實(shí)例對(duì)象捣卤,具體的實(shí)例對(duì)象是在堆中開(kāi)辟的一塊空間存放的)忍抽。 在HotSpot VM里實(shí)現(xiàn)的string pool功能的是一個(gè)StringTable類码泞,它是一個(gè)哈希表躺翻,里面存的是駐留字符串(也就是我們常說(shuō)的用雙引號(hào)括起來(lái)的)的引用(而不是駐留字符串實(shí)例本身),也就是說(shuō)在堆中的某些字符串實(shí)例被這個(gè)StringTable引用之后就等同被賦予了”駐留字符串”的身份辅甥。這個(gè)StringTable在每個(gè)HotSpot VM的實(shí)例只有一份子姜,被所有的類共享祟绊。
GC算法
垃圾回收過(guò)程是應(yīng)用性能和垃圾回收器之間的博弈過(guò)程。但是程序?qū)懙臓€哥捕,GC再努力博弈牧抽,也救不了你。
必知必會(huì)的垃圾回收算法:
-
引用計(jì)數(shù)法
這是最基本想到的算法扭弧,其最為經(jīng)典,最為古老记舆,存在先天缺陷鸽捻。
如果一個(gè)對(duì)象A被其它對(duì)象引用,就+1泽腮,如果取消引用就-1御蒲。只要引用為0,該對(duì)象就不再使用诊赊,可以回收厚满。這種算法會(huì)無(wú)法解決循環(huán)引用這一流氓問(wèn)題。
-
根搜索算法
為了解決循環(huán)引用問(wèn)題碧磅,所以基于根對(duì)象的可達(dá)性分析思想設(shè)計(jì)了根搜索算法碘箍,如下圖
如果對(duì)象已經(jīng)不可達(dá)遵馆,則回收,顯然這種方法更為科學(xué)丰榴。
那么什么對(duì)象才能成為根對(duì)象货邓?這里根對(duì)象很關(guān)鍵,遍歷算法也必須足夠高效四濒。
實(shí)現(xiàn)時(shí)换况,基于可達(dá)性分析,從GCRoots 開(kāi)始向下搜索引用鏈盗蟆,如果不在鏈中戈二,也就是堆中的對(duì)象沒(méi)有被引用到,則該對(duì)象可以被回收喳资。
GC Roots的對(duì)象包括下面幾種:
- 虛擬機(jī)棧(棧幀中的本地變量表)中引用的對(duì)象觉吭。
- 方法區(qū)中類靜態(tài)屬性引用的對(duì)象。
- 方法區(qū)中常量引用的對(duì)象骨饿。
- 本地方法棧中JNI(即一般說(shuō)的Native方法)引用的對(duì)象亏栈。
詳解圖:
這種回收算法的效率,是存活對(duì)象的數(shù)量是線性相關(guān)的宏赘。所以绒北,不要使用大量的小的,存活期長(zhǎng)的活動(dòng)對(duì)象察署,而且一旦不用闷游,立即賦值null,是一個(gè)影響效率的好習(xí)慣贴汪。這中回收算法也為回收性能考量提供了參考脐往,空間越大,存活對(duì)象越多扳埂,回收效率越低业簿。參考Java GC 內(nèi)存回收機(jī)制詳解(二)GC Roots 和 可達(dá)鏈
具體的標(biāo)記方法:著色標(biāo)記,并進(jìn)行三顏色標(biāo)記阳懂,具體標(biāo)記算法如下:
黑色(black):節(jié)點(diǎn)被遍歷完成梅尤,而且子節(jié)點(diǎn)都遍歷完成。
灰色(gray): 當(dāng)前正在遍歷的節(jié)點(diǎn)岩调,而且子節(jié)點(diǎn)還沒(méi)有遍歷巷燥。
白色(white):還沒(méi)有遍歷到的節(jié)點(diǎn),即灰色節(jié)點(diǎn)的子節(jié)點(diǎn)号枕。
- 標(biāo)記清除算法(Mark-Sweep)
大名鼎鼎的CMS就使用了標(biāo)記清除算法缰揪。可以說(shuō)奠定了現(xiàn)代垃圾回收算法的思想基礎(chǔ)葱淳。
簡(jiǎn)單將就是將垃圾回收分為兩個(gè)階段:標(biāo)記階段和清除階段钝腺,這個(gè)標(biāo)記清除其實(shí)根搜索算法動(dòng)作是一樣的抛姑。
這種算法最大問(wèn)題就是回收后空間不再連續(xù),尤其針對(duì)大對(duì)象拍屑,不連續(xù)的空間會(huì)降低工作效率途戒。
從此之后的算法設(shè)計(jì)要解決的兩個(gè)問(wèn)題是:
怎樣實(shí)現(xiàn)更精準(zhǔn)清除、更高效清除效率僵驰?
怎樣確迸缯回收后的空間盡量連續(xù)完整?
- 復(fù)制算法(Copying)
為了解決上面的問(wèn)題蒜茴,自然想到水桶接水的方法星爪。將原有堆空間分為兩份,一份使用粉私,一份空閑顽腾,回收時(shí),將仍然存活的對(duì)象(正常情況下存活對(duì)象非常少)復(fù)制到空閑的一份中诺核,因存活對(duì)象少抄肖,效率非常高,然后清空使用那份的內(nèi)存窖杀,標(biāo)記空閑的空間為使用漓摩,使用的為空閑。
如此解決了空間連續(xù)性的問(wèn)題入客,也提高了清除效率管毙。
但帶來(lái)了新問(wèn)題,GC時(shí)需要將全部存活的對(duì)象從Eden搬到Survivor區(qū)桌硫,空間利用率低夭咬,只能使用一半的空間。
為了解決空間浪費(fèi)的問(wèn)題:
在java新生代串行回收器中铆隘,使用了改進(jìn)的復(fù)制算法卓舵。將新生代分為eden區(qū),survivor from區(qū)和survivor to區(qū)膀钠,默認(rèn)8:1:1掏湾,實(shí)際存活的對(duì)象一般認(rèn)為非常少,所以并不需要對(duì)半分托修。
eden和survivor區(qū)職責(zé)更加明確忘巧,不再必須交換恒界。eden的存活對(duì)象直接復(fù)制到survivor to區(qū)睦刃,survivor from區(qū)的因?yàn)榻邮仗右莸拇婊顚?duì)象也直接復(fù)制到survivor to區(qū)。這樣實(shí)際垃圾回收的是eden區(qū)和survivor from區(qū)十酣∩荆回收時(shí)其實(shí)是survivor from和survivor to區(qū)互相交換际长,這樣浪費(fèi)的內(nèi)存空間就極大的減少了。大對(duì)象直接進(jìn)入老年代兴泥。體現(xiàn)了回收的針對(duì)性工育。
現(xiàn)在似乎大部分問(wèn)題都解決了,但是如果存活對(duì)象非常多搓彻,存活時(shí)間非常長(zhǎng)如绸,那么復(fù)制算法的效率就會(huì)大打折扣,GC時(shí)間就沒(méi)有必要的被拉長(zhǎng)了旭贬。
- 標(biāo)記-壓縮算法(Mark-Compact)
標(biāo)記壓縮算法是一種老年代算法怔接。
和標(biāo)記清除一樣,但做了優(yōu)化稀轨,在標(biāo)記完成之后扼脐,將所有存活對(duì)象壓縮到內(nèi)存的一端,然后清理邊界外的所有空間奋刽,完成回收瓦侮。這樣同樣保證了連續(xù)性,保證了對(duì)象訪問(wèn)的效率佣谐,但不需要浪費(fèi)空間肚吏,減少?gòu)?fù)制次數(shù)。
到這里我們覺(jué)得垃圾回收思想已經(jīng)非常完整了台谍,但是在實(shí)踐中须喂,我們會(huì)發(fā)現(xiàn)垃圾回收暫停的時(shí)間可能會(huì)比較長(zhǎng),尤其是當(dāng)開(kāi)辟的內(nèi)存空間比較大的時(shí)候〕萌铮現(xiàn)在XXX系統(tǒng)坞生,為了提升吞吐量,我們內(nèi)存開(kāi)辟都是8G掷伙,16G是己,32G的初始起步,所以當(dāng)垃圾到來(lái)時(shí)任柜,我們需要更強(qiáng)的算法處理卒废。
- 增量算法:增量算法的基本思想是,如果一次性將所有的垃圾進(jìn)行處理宙地,需要造成系統(tǒng)長(zhǎng)時(shí)間的停頓摔认,那么就可以讓垃圾收集線程和應(yīng)用程序線程交替執(zhí)行。每次宅粥,垃圾收集線程只收集一小片區(qū)域的內(nèi)存空間参袱,接著切換到應(yīng)用程序線程。依次反復(fù),直到垃圾收集完成抹蚀。簡(jiǎn)而言之就是少吃多餐剿牺,做到GC和應(yīng)用服務(wù)兼顧。其優(yōu)點(diǎn)是使用這種方式环壤,由于在垃圾回收過(guò)程中晒来,間斷性地還執(zhí)行了應(yīng)用程序代碼,所以能減少系統(tǒng)的停頓時(shí)間郑现。其缺點(diǎn)也很明顯湃崩,因?yàn)榫€程切換和上下文轉(zhuǎn)換的消耗,會(huì)使得垃圾回收的總體成本上升接箫,造成系統(tǒng)吞吐量的下降竹习。
一種算法實(shí)現(xiàn)TrainGC(火車算法),一種算法實(shí)現(xiàn)時(shí)G1 GC列牺。
Train GC的“精髓”就是試圖通過(guò)把若干有相互引用的region串在一起整陌,保證它們總是一起被收集,來(lái)避免讓每個(gè)region都要關(guān)聯(lián)巨大的remembered set瞎领。G1 GC則不偷這個(gè)懶泌辫,確實(shí)給每個(gè)region都關(guān)聯(lián)上了一個(gè)大remembered set,但通過(guò)一些壓縮技巧來(lái)減小這些大remembered set的開(kāi)銷九默。這樣G1就比train有更高的自由度來(lái)選擇一次增量式GC要收集哪些region及不收集哪些region震放,從而達(dá)到了更好的效果。最終偷懶的死掉了驼修,踏實(shí)的得到了更好的發(fā)展殿遂。
- 分代收集算法(Generational Collecting)
分代算法就是根據(jù)對(duì)象的特性,將內(nèi)存區(qū)域不同的幾個(gè)區(qū)域乙各,不同區(qū)域使用不同的回收算法墨礁,以提高回收效率。分代搞定了GC的性能壓力耳峦,垃圾回收器只需要專注掃描特定區(qū)域恩静,不用再整個(gè)堆掃描。
新生代:存放新生代對(duì)象的堆空間蹲坷,新生代對(duì)象指剛剛創(chuàng)建的或者經(jīng)歷了幾次垃圾回收過(guò)程的對(duì)象驶乾,一般都是朝生夕滅。大多數(shù)內(nèi)存塊的生存周期都比較短循签,垃圾收集器應(yīng)當(dāng)把更多的精力放在檢查和清理新分配的內(nèi)存塊上级乐。新生代對(duì)象比較適合使用改進(jìn)后的復(fù)制算法。
老年代:存放老年代對(duì)象的堆空間县匠,老年代對(duì)象指經(jīng)歷多次垃圾回收仍然存活的對(duì)象 风科,-XX:MaxTenuringThreshold=數(shù)字 參數(shù)可以設(shè)置對(duì)象在經(jīng)過(guò)多少次GC后會(huì)被放入老年代(年齡達(dá)到設(shè)置值罕扎,默認(rèn)為15)。老年代對(duì)象回收時(shí)存活率非常高丐重,所以適合標(biāo)記壓縮或者標(biāo)記清除算法。
永久代 :G1已經(jīng)廢棄永久代杆查,這里不再探討扮惦。簡(jiǎn)單說(shuō)就是不會(huì)被回收的對(duì)象放在永久代。
實(shí)踐中新生代回收頻率高亲桦,老年代非常低崖蜜,但消耗更多時(shí)間,為了進(jìn)一步支持高頻率的新生代回收客峭,避免新生代掃描時(shí)豫领,必須通過(guò)新生代對(duì)象的引用傳遞,不得不全量掃描老年代舔琅,增加了卡表的數(shù)據(jù)結(jié)構(gòu)(Card Table)等恐,用來(lái)標(biāo)記老年代某一區(qū)域是否持有對(duì)新生代的引用,如果持有比特位設(shè)置為1备蚓,否則為0课蔬。
到此為止,我們垃圾回收算法已經(jīng)算非常完整了郊尝,但是現(xiàn)在應(yīng)用為了提高吞吐量和處理的數(shù)據(jù)量二跋,應(yīng)用程序往往都走上了簡(jiǎn)單增加內(nèi)存空間策略的道路。這時(shí)每次Full GC停頓的時(shí)間會(huì)變得非常長(zhǎng)(用戶可感知)流昏,所以如何降低GC停頓的時(shí)間扎即,成為優(yōu)化的重點(diǎn)方向。
- 分區(qū)算法(Region)
分區(qū)算法就是將連續(xù)的空間分成很多不同的小空間况凉,獨(dú)立使用谚鄙,獨(dú)立回收。
相同條件下刁绒,堆空間越大襟锐,回收時(shí)間越長(zhǎng),停頓時(shí)間越長(zhǎng)膛锭。通過(guò)將空間切分粮坞,每次只針對(duì)部分區(qū)域回收,而不是這個(gè)空間初狰,從而減少一次GC停頓的時(shí)間莫杈。
實(shí)踐中分區(qū)算法一定程度改觀了STW對(duì)應(yīng)用提供服務(wù)能力的消弱。
為啥大小必須相等奢入,可以不等嗎筝闹?我覺(jué)得完全可以不相等啊媳叨。每個(gè) Region 都有一 個(gè)關(guān)聯(lián)的 Remembered Set (簡(jiǎn)稱 RS), RS 的數(shù)據(jù)結(jié)構(gòu)是 Hash 表,里面的數(shù)據(jù)是 Card Table (堆 中每 512byte 映射在 card table 1byte) 关顷。簡(jiǎn)單地說(shuō)糊秆, RS 里面存在的是 Region 中存活對(duì)象的指針當(dāng) Region中數(shù)據(jù)發(fā)生變化時(shí),首先反映到 CardTable中的一個(gè)或多個(gè) Card上议双, RS通過(guò)掃描內(nèi) 部的 Card Table 得知 Region 中內(nèi)存使用情況和存活對(duì)象痘番。在使用 Region 過(guò)程中,如果 Region 被填滿了平痰,分配內(nèi)存的線程會(huì)重新選擇一個(gè)新的 Region汞舱, 空閑 Region 被組織到 一個(gè)基于鏈表的 數(shù)據(jù)結(jié)構(gòu) CLinlf:edList)里面,這樣可以快速找到新的 Region宗雇。
經(jīng)歷了漫長(zhǎng)的垃圾回收算法不斷迭代優(yōu)化的心路歷程之后昂芜,我們必須思考一個(gè)深入的問(wèn)題:垃圾回收算法下一部發(fā)展的方向是什么?
- 更智能化:引入AI技術(shù)赔蒲,理解應(yīng)用場(chǎng)景泌神,對(duì)內(nèi)存和CPU使用趨勢(shì)做出預(yù)測(cè),并針對(duì)內(nèi)存空間做針對(duì)性的優(yōu)化舞虱。
- 支持人工定義和標(biāo)記對(duì)象特性腻扇,根據(jù)對(duì)象特性自動(dòng)分代分區(qū)。
- ......目前從java8-java13 G1仍在發(fā)展和完善砾嫉,但垃圾回收算法思想已經(jīng)相對(duì)停滯幼苛,我們?nèi)鄙俚目赡苓€是想象力。
垃圾回收器
串行回收指的是在同一時(shí)間段內(nèi)只允許一件事情發(fā)生焕刮,簡(jiǎn)單來(lái)說(shuō)舶沿,當(dāng)多個(gè) CPU 可用時(shí),也只能有一個(gè) CPU用于執(zhí)行垃圾回收操作配并,井且在執(zhí)行垃圾回收時(shí)括荡,程序中的工作線程將會(huì)被暫停,當(dāng)垃圾收集工作完成后才會(huì)恢復(fù)之前被暫停的工作線程溉旋,這就是串行回收畸冲。串行收集器主要有兩個(gè)特點(diǎn)。首先观腊,它僅僅使用單線程進(jìn)行垃圾回收邑闲。其次,它是獨(dú)占式的垃圾回收方式梧油。并行收集器是工作在年輕代的垃圾收集器苫耸,它只簡(jiǎn)單地將串行回收器多線程化。串行回收器雖然原始儡陨,但可靠褪子,經(jīng)受住了生產(chǎn)環(huán)境的嚴(yán)苛考驗(yàn)量淌。
JVM有如下主流回收器
- Serial垃圾收集器 :?jiǎn)尉€程垃圾收集器, -XX:UseSerialGC 參數(shù)設(shè)置開(kāi)啟嫌褪,新生代和老年代都使用單線程回收呀枢。新生代使用改進(jìn)的復(fù)制算法,老年代使用標(biāo)記壓縮算法笼痛。
- Throughput垃圾收集器 :多線程并行垃圾收集器裙秋,就是將Serial改成多線程版本。Parallel GC根據(jù)MinorGC和FullGC的不同分為三種晃痴,分別是ParNewGC、ParallelGC和ParallelOldGC财忽。
ParNew回收器倘核,工作在新生代〖幢耄回收策略和串行一致紧唱。
ParallelGC也使用復(fù)制算法。但其相比ParNew偏重系統(tǒng)吞吐量隶校,支持自適應(yīng)GC調(diào)節(jié)設(shè)置漏益。有兩個(gè)參數(shù)控制:MaxGCPauseMillis設(shè)置最大垃圾收集停頓時(shí)間,GCTimeRatio設(shè)置吞吐量大小深胳。
ParallelOldGC是關(guān)注吞吐量的老年代垃圾回收器绰疤,使用標(biāo)記壓縮算法。
-XX:UseParNewGC :新生代使用ParNew舞终,老年代使用串行回收器
-XX:UseParallelGC:新生代使用ParallelGC轻庆,老年代使用串行回收器
-XX:UseParallelOldGC:新生代使用ParallelGC,老年代使用ParallelOldGC回收器敛劝。
- 經(jīng)典CMS并發(fā)標(biāo)記清除垃圾收集器
CMS垃圾回收器設(shè)計(jì)初衷是為了消除Throughput收集器和Serial收集器Full GC周期中的長(zhǎng)時(shí)間停頓余爆。也就說(shuō)CMS是一個(gè)非常關(guān)注停頓的垃圾回收器,意圖提供更好的應(yīng)用體驗(yàn)夸盟,所以一般客戶端交互的Server應(yīng)用建議使用CMS蛾方。CMS是老年代垃圾回收器(特別注意),需要配和其它新生代垃圾回收器一起使用上陕。
對(duì)應(yīng)虛擬機(jī)GC日志過(guò)程如下
CMS-initial-mark
CMS-concurrent-mark-start
CMS-concurrent-mark
CMS-concurrent-preclean-start
CMS-concurrent-preclean
CMS-concurrent-abortable-preclean-start
CMS-concurrent-abortable-preclean
CMS-remark
CMS-concurrent-sweep-start
CMS-concurrent-sweep
CMS-concurrent-reset-start
CMS-concurrent-reset
其在初始標(biāo)記和重新標(biāo)記階段是獨(dú)占系統(tǒng)資源的(不需要開(kāi)始結(jié)束過(guò)程)桩砰,其它階段可以和應(yīng)用線程一起使用。CMS垃圾回收器最大的特點(diǎn)是在垃圾回收階段不再整體獨(dú)占释簿,將回收分成6個(gè)階段五芝,只2個(gè)階段獨(dú)占CPU。
-XX:UseConcMarkSweepGC:新生代使用ParNew回收器辕万,老年代使用CMS
由于CMS執(zhí)行時(shí)是并發(fā)的枢步,應(yīng)用程序和垃圾回收線程會(huì)交替執(zhí)行沉删,所以還會(huì)產(chǎn)生新的垃圾,所以垃圾回收時(shí)醉途,不是等滿了才啟動(dòng)收集矾瑰,而是老年代到達(dá)回收閾值時(shí),就觸發(fā)回收隘擎,通過(guò)CMSInitialatingOccupancyFraction指定殴穴,默認(rèn)68%。
CMS是標(biāo)記清除算法货葬,所以存在內(nèi)存碎片采幌,離散的可用空間不能分配較大的對(duì)象。用UseCMSCompacAtFullCollection參數(shù)設(shè)置在進(jìn)行幾次垃圾回收之后震桶,進(jìn)行一次碎片整理休傍。
CMS使用中如果頻繁出現(xiàn)concurrent mode failure日志,表明并發(fā)收集失敗蹲姐,很可能是老年代不夠?qū)е碌哪ト。敲磻?yīng)當(dāng)考慮增加老年代空間。
- G1垃圾收集器
新一代垃圾回收器設(shè)計(jì)初衷是為了盡量縮短超大堆時(shí)產(chǎn)生的停頓柴墩。G1就是將分代和分區(qū)算法思想進(jìn)行了結(jié)合忙厌。在G1中堆被平均的分割成若干大小相等的Region。G1同時(shí)作用于新生代和老年代江咳。
那么為什么叫G1 (Garbage First Garbage Collector,垃圾優(yōu)先的垃圾回收器) ? 顧名思義逢净,優(yōu)先回收垃圾多的region。
G1充分吸收了CMS的并發(fā)標(biāo)記的優(yōu)點(diǎn)歼指,其工作階段分為:
- 新生代GC
- 并發(fā)標(biāo)記周期(多個(gè)階段汹胃,非常細(xì))
- 混合收集
- 如果需要,會(huì)觸發(fā)Full GC
G1新生代GC會(huì)同時(shí)回收eden區(qū)和survivor區(qū)东臀。至少保留一個(gè)survivor區(qū)(請(qǐng)想一下為什么着饥?前面講過(guò),survivor from -> survivor to)惰赋,老年代區(qū)可能會(huì)增多宰掉。
G1的并發(fā)標(biāo)記周期分為如下階段
初始標(biāo)記階段:標(biāo)記從根節(jié)點(diǎn)可直接到達(dá)的對(duì)象。這個(gè)階段會(huì)伴隨一次新生代GC赁濒,會(huì)產(chǎn)生全局停頓轨奄。eden清空,存活對(duì)象全部到survivor區(qū)拒炎。
根區(qū)域掃描:掃描從survivor可直達(dá)老年代的區(qū)域挪拟,并標(biāo)記可直達(dá)的對(duì)象,這個(gè)可以和應(yīng)用程序并行執(zhí)行击你,意味著這個(gè)階段會(huì)產(chǎn)生新的eden區(qū)玉组,但不可和新生代GC并行谎柄,所以只有根區(qū)域掃描結(jié)束,才會(huì)進(jìn)行新生代GC惯雳。
并發(fā)標(biāo)記:和CMS類似朝巫,從整個(gè)堆中找到和標(biāo)記存活對(duì)象∈埃可以并發(fā)執(zhí)行劈猿,可以被新生代GC打斷。
重新標(biāo)記: 修正并發(fā)標(biāo)記結(jié)果潮孽,此過(guò)程產(chǎn)生停頓揪荣,這一過(guò)程使用SATB (Snapshot-At-The-Beginning)算法進(jìn)行。
具體如何做呢往史?
參考G1 SATB和Incremental Update算法的理解
如圖出現(xiàn)如下情況仗颈,我們都知道cmg gc 和g1 gc 都是和程序有并行執(zhí)行的階段。既然有并行怠堪,那就有可能在并行運(yùn)行期間之前的標(biāo)記過(guò)的對(duì)象的引用關(guān)系可能被改變揽乱,比如一個(gè)白色對(duì)象從被灰色的引用變?yōu)楸缓谏膶?duì)象引用名眉。如果不做處理粟矿,那這個(gè)白色的對(duì)象會(huì)被漏掉,會(huì)被錯(cuò)誤的回收损拢。會(huì)導(dǎo)致程序錯(cuò)誤陌粹。
這也是cms gc 和g1 gc 都有remark階段的原因。都需要重新對(duì)被修改的card 進(jìn)行掃描福压。
cms gc 是Incremental Update算法掏秩,g1 gc 是采用的 stab 算法。
- 把一個(gè)白對(duì)象的引用存到黑對(duì)象的字段里,如果這個(gè)情況發(fā)生荆姆,因?yàn)闃?biāo)記為黑色的對(duì)象認(rèn)為是掃描完成的蒙幻,不會(huì)再對(duì)他進(jìn)行掃描。只能通過(guò)灰色的對(duì)象
- 某個(gè)白對(duì)象失去了所有能從灰對(duì)象到達(dá)它的引用路徑(直接或間接)
SATB 即 Snapshot-at-beginning胆筒,satb 算法認(rèn)為開(kāi)始標(biāo)記的都認(rèn)為是活的對(duì)象邮破,如上圖所示,引用B到D 的引用改為B到C時(shí),通過(guò)write barrier寫屏障技術(shù)仆救,會(huì)把B到D 的引用推到gc 遍歷執(zhí)行的堆棧上抒和,保證還可以遍歷到D對(duì)象,相對(duì)于d來(lái)說(shuō)彤蔽,引用從B-->A,SATB 是從源入手解決的摧莽,即上面說(shuō)的第2種情況,
這也能理解為啥叫satb 了,即認(rèn)為開(kāi)始時(shí)所有能遍歷到的對(duì)象都是需要標(biāo)記的钧忽,即都認(rèn)為是活的。如果我吧b = null,那么d 久是垃圾了腮敌, satb算法也還是會(huì)把D最終標(biāo)記為黑色丑蛤,導(dǎo)致D 在本輪gc 不能回收叠聋,成了浮動(dòng)垃圾。
Incremental Update write barrier受裹,Incremental Update 算法判斷如果一個(gè)白色的對(duì)象由一個(gè)黑色的對(duì)象引用碌补,即目的,如上圖棉饶,D的引用由B-->A,A是目的地址厦章,所以cms 的Incremental Update算法是從目標(biāo)入手解決的,這是和SATB的第一個(gè)區(qū)別照藻,發(fā)現(xiàn)這種情況時(shí)袜啃,也是通過(guò)write barrier寫屏障技術(shù),把黑色的對(duì)象重新標(biāo)記為灰色幸缕,讓collector 重新來(lái)掃描群发,活著通過(guò)mod-union table 來(lái)標(biāo)記,cms 就是這樣實(shí)現(xiàn)的发乔,這是第二個(gè)區(qū)別熟妓,做法不一樣,也是上面講的防止第一種情況發(fā)生栏尚。
Incremental Update 和 SATB 的區(qū)別起愈。通過(guò)上面的分析,我們知道SATB write barrier 是認(rèn)為開(kāi)始標(biāo)記那一刻認(rèn)為都是活的译仗,所以有可能有些已經(jīng)是垃圾的對(duì)象就會(huì)也被掃描抬虽,導(dǎo)致 satb 相對(duì) Incremental Update 會(huì)更多的開(kāi)銷,g1 gc 掃描的都是選定的固定個(gè)數(shù)的region纵菌,所以這個(gè)開(kāi)銷應(yīng)該可控阐污,但是而且浮動(dòng)垃圾也更多。
獨(dú)占清理咱圆,計(jì)算各個(gè)區(qū)域存活對(duì)象和GC回收比率并排序(回收比)笛辟,識(shí)別可供混合回收的區(qū)域(G),這些區(qū)域垃圾比較多闷堡,更新記憶集(Remembed Set),這個(gè)階段產(chǎn)生停頓隘膘。
并發(fā)清理階段,識(shí)別并清理完全空閑的區(qū)域杠览,這個(gè)階段不停頓弯菊。根據(jù)獨(dú)占清理階段計(jì)算出來(lái)的每個(gè)區(qū)域的存活對(duì)象,直接回收沒(méi)有存活對(duì)象的區(qū)域
GC pause (young)(initial-mark)
GC concurrent-root-region-scan-start
GC concurrent-root-region-scan-end
GC concurrent-mark-start
GC concurrent-mark-end
GC remark
GC cleanup
GC concurrent-cleanup-start
GC concurrent-cleanup-end
- 混合回收
并發(fā)標(biāo)記后,G1明確知道哪些region垃圾比較多管钳∏仗混合回收階段,就有針對(duì)性才漆。因?yàn)榛厥者@些高垃圾占必的區(qū)域性價(jià)比高牛曹。這個(gè)階段既進(jìn)行年輕代回收,也會(huì)針對(duì)標(biāo)記的老年代進(jìn)行回收醇滥,Eden區(qū)被完全清理黎比,上一步標(biāo)記為G的區(qū)域被清理,被清理區(qū)域存活的對(duì)象會(huì)被移動(dòng)到其它區(qū)域鸳玩,減少空間碎片阅虫。
-XX:UseG1GC 開(kāi)啟使用G1垃圾回收器。
-XX:ConcGCThreads 并行執(zhí)行GC的線程數(shù)不跟,默認(rèn)1/4核數(shù) 颓帝,過(guò)大GC時(shí)CPU過(guò)高,過(guò)小回收時(shí)間變長(zhǎng)
-XX:G1 HeapRegionSize 增大有利于大對(duì)象處理窝革。大對(duì)象沒(méi)有按照普通對(duì)象方式進(jìn)行 管理和分配空間购城,如果增大 Region塊的大小,則一些原本走特殊處理通道的大對(duì)象就可以被納 入普通處理通道了虐译。設(shè)置的過(guò)小瘪板,降低靈活性
配置參考
-XX:+PrintGCDetails
-verbose:gc
-Xloggc:gc.log
-XX:+UseGIGC
-XX:+Prin的CApplicationStoppedTirne
-XX:ConcGCThreads=2
-XX:G1HeapRegionSize=32M
如下是垃圾回收器G1針對(duì)大對(duì)象做的特別優(yōu)化
如何定義大對(duì)象?
假設(shè),當(dāng)前每個(gè)區(qū)間的大小定義為2MB菱蔬, 一個(gè)數(shù)組大小為1MB 篷帅。這個(gè)數(shù)組會(huì)被認(rèn)定為大對(duì)象嗎?是的史侣,這是因?yàn)閿?shù)組大小=數(shù)組內(nèi)部對(duì)象的頭大小+對(duì)象大小拴泌,即這個(gè)數(shù)組超過(guò)了1MB, 即(大對(duì)象)超過(guò)了區(qū)間大小的 50%,符合大對(duì)象的定義要求惊橱,屬于大對(duì)象 蚪腐。
大對(duì)象為何在物理空間上連續(xù)?
如果在回收階段嘗試去頻繁地回收大對(duì)象税朴,存在兩個(gè)缺點(diǎn):
- 移動(dòng)對(duì)象需要拷貝數(shù)據(jù)回季,大對(duì)象較大,拷貝效率終究會(huì)成為瓶頸
- 很難找到連續(xù)的堆內(nèi)存用于大對(duì)象存儲(chǔ)正林,回收越頻繁泡一,越容易出現(xiàn)相互獨(dú)立的區(qū)間
- Region得分配是連續(xù)的,空閑的Region加入一個(gè)LinkedList隊(duì)列中觅廓。
大對(duì)象會(huì)在GC的那些階段回收鼻忠?
jdk8u40之后,年輕代回收杈绸、并行回收循環(huán)帖蔓、 Full GC矮瘟,它們都會(huì)參與到大對(duì)象區(qū)間的回收工作。
- ZGC最新的垃圾回收器
主要就是解決大數(shù)據(jù)下垃圾回收的STW問(wèn)題塑娇。ZGC由Oracle開(kāi)發(fā)澈侠,承諾在數(shù)TB的堆上具有非常低的暫停時(shí)間。參考一語(yǔ)道破Java 11的ZGC為何如此高效
這里暫不詳述埋酬。
JVM調(diào)優(yōu)
暫不表哨啃。
jvm會(huì)為每個(gè)對(duì)象創(chuàng)建一個(gè)對(duì)象頭,保留系統(tǒng)信息写妥,其中就包含對(duì)象鎖的信息棘催。
偏向鎖,jdk1.6之后耳标,對(duì)象鎖使用偏向鎖醇坝,即一個(gè)線程鎖住一個(gè)對(duì)象后,下次再使用這個(gè)對(duì)象時(shí)次坡,不再進(jìn)行相關(guān)的同步操作呼猪。
偏向鎖依據(jù)的原理是如果一個(gè)線程最近用到了鎖,那么線程下一次執(zhí)行由統(tǒng)一把鎖保護(hù)的代碼所需的數(shù)據(jù)可能仍然保存在處理器緩存中砸琅。如果這個(gè)線程獲得這把鎖的權(quán)利宋距,緩存命中率可能就會(huì)增加。
類加載機(jī)制
java類加載使用雙親委派模型症脂,這個(gè)模型我看代碼谚赎,覺(jué)得沒(méi)有那么高深晦澀。實(shí)現(xiàn)都是比較簡(jiǎn)單的诱篷。
加載 → 連接 → 初始化
連接:驗(yàn)證 → 準(zhǔn)備 → 解析
節(jié)省內(nèi)存使用的方法有哪些壶唤?
1.減少對(duì)象使用。僅定義需要的實(shí)例對(duì)象棕所,但不要嘗試用int替代long闸盔,float替代double,意義不大琳省,反倒增加了限制(因?yàn)閖vm針對(duì)對(duì)象數(shù)組迎吵,會(huì)做對(duì)齊處理)
2.延遲初始化。一個(gè)對(duì)象引用的對(duì)象如果不是時(shí)刻必須的针贬,盡量在該存在的生命周期里再初始化击费。對(duì)于非線程安全的對(duì)象,引用對(duì)象的初始化桦他,要加線程保護(hù)機(jī)制蔫巩。
3.使用不可變對(duì)象和標(biāo)準(zhǔn)對(duì)象。例如Boolean對(duì)象,其實(shí)應(yīng)該設(shè)計(jì)成私有的的批幌,只用Boolean.TRUE和Boolean.FALSE
4.字符串保留础锐,理解intern方法。字符串的分配荧缘,和其他的對(duì)象分配一樣皆警,耗費(fèi)高昂的時(shí)間與空間代價(jià),作為最基礎(chǔ)的數(shù)據(jù)類型截粗,大量頻繁的創(chuàng)建字符串信姓,極大程度地影響程序的性能
5.盡量避免對(duì)象重用,避免使用對(duì)象池技術(shù)绸罗,只有在對(duì)象的創(chuàng)建成本非常高的情況下(例如數(shù)據(jù)庫(kù)連接池)意推,對(duì)象池中的對(duì)象非常容易進(jìn)入老年代,反倒增加每次Full GC的成本珊蟀。需要重用的對(duì)象:
線程池
JDBC池
大數(shù)組
原生NIO緩沖區(qū)
安全相關(guān)類MessageDigest Signature
隨機(jī)數(shù)生成器
ZIP解釋碼器
...
當(dāng)然對(duì)于大部分開(kāi)發(fā)人員菊值,我們根本就不會(huì)考慮到這層優(yōu)化的水準(zhǔn)。因?yàn)橄肫饋?lái)容易育灸,做起來(lái)太難了腻窒,會(huì)極大降低開(kāi)發(fā)的效率,增加工作量磅崭,所以更為科學(xué)的操作是儿子,關(guān)注對(duì)象的生命周期
參考資料
深入理解JVM & G1 GC
深入理解java虛擬機(jī)
實(shí)戰(zhàn)java虛擬機(jī)
java性能權(quán)威指南
Java:JVM垃圾回收(GC)機(jī)制
大神教你JVM運(yùn)行原理及Stack和Heap的實(shí)現(xiàn)過(guò)程