JVM性能優(yōu)化,Part 1 ―― JVM簡(jiǎn)介
眾所周知,Java應(yīng)用程序是運(yùn)行在JVM上的,但是你對(duì)JVM有所了解么?作為這個(gè)系列文章的第一篇,本文將對(duì)經(jīng)典Java虛擬機(jī)的運(yùn)行機(jī)制做簡(jiǎn)單介紹,內(nèi)容包括“一次編寫,到處運(yùn)行”的利弊、垃圾回收的基本原理、常用垃圾回收算法的示例和編譯器優(yōu)化等。后續(xù)的系列文章將會(huì)JVM性能優(yōu)化的內(nèi)容進(jìn)行介紹,包括新一代JVM的設(shè)計(jì)思路,以及如何支持當(dāng)今Java應(yīng)用程序?qū)Ω咝阅芎透邤U(kuò)展性的要求。
這個(gè)系列文章主要面向那些想要了解JVM底層運(yùn)行原理的Java程序員。文章立足于較高的層面展開討論,內(nèi)容涉及到垃圾回收和在不影響應(yīng)用程序運(yùn)行的情況下安全快速的釋放/分配內(nèi)存。你將對(duì)JVM的核心模塊有所了解:垃圾回收、GC算法、編譯器行為,以及一些常用優(yōu)化技巧。此外,還會(huì)討論為什么對(duì)Java做基準(zhǔn)測(cè)試(benchmark)是件很困難的事,并提供一些建議來幫助做基準(zhǔn)測(cè)試。最后,將會(huì)介紹一些JVM和GC的前沿技術(shù),內(nèi)容涉及到Azul的ZingJVM,IBMJVM和Oracle的GarbageFirst(G1)垃圾回收器。
希望在閱讀此系列文章后,你能對(duì)影響Java伸縮性的因素有所了解,并且知道這些因素是如何影響Java開發(fā)的,如何使Java難以優(yōu)化的。希望會(huì)你有那種發(fā)自內(nèi)心的驚嘆,并且能夠激勵(lì)你為Java做一點(diǎn)事情:拒絕限制,努力改變。
Java的性能與“一次編寫,到處運(yùn)行”的挑戰(zhàn)
有不少人認(rèn)為,Java平臺(tái)本身就挺慢。其主要觀點(diǎn)簡(jiǎn)單來說就是,Java性能低已經(jīng)有些年頭了―― 最早可以追溯到Java第一次用于企業(yè)級(jí)應(yīng)用程序開發(fā)的時(shí)候。但這早就是老黃歷了。事實(shí)是,如果你對(duì)不同的開發(fā)平臺(tái)上運(yùn)行簡(jiǎn)單的、靜態(tài)的、確定性任務(wù)的運(yùn)行結(jié)果做比較,你就會(huì)發(fā)現(xiàn)使用經(jīng)過機(jī)器級(jí)優(yōu)化(machine-optimized)代碼的平臺(tái)比任何使用虛擬環(huán)境進(jìn)行運(yùn)算的都要強(qiáng),JVM也不例外。但是,在過去的10年中,Java的性能有了大幅提升。市場(chǎng)上不斷增長(zhǎng)的需求催生了垃圾回收算法的出現(xiàn)和編譯技術(shù)的革新,在不斷探索與優(yōu)化的過程中,JVM茁壯成長(zhǎng)。在這個(gè)系列文章中,我將介紹其中的一些內(nèi)容。
JVM技術(shù)中最迷人的地方也正是其最具挑戰(zhàn)性的地方:“一次編寫,到處運(yùn)行”。JVM并不對(duì)具體的用例、應(yīng)用程序或用戶負(fù)載進(jìn)行優(yōu)化,而是在應(yīng)用程序運(yùn)行過程中不斷收集運(yùn)行時(shí)信息,并以此為根據(jù)動(dòng)態(tài)的進(jìn)行優(yōu)化。這種動(dòng)態(tài)的運(yùn)行時(shí)特性帶來了很多動(dòng)態(tài)問題。在設(shè)計(jì)優(yōu)化方案時(shí),以JVM為工作平臺(tái)的程序無法依靠靜態(tài)編譯和可預(yù)測(cè)的內(nèi)存分配速率(predictableallocation rates)對(duì)應(yīng)用程序做性能評(píng)估,至少在對(duì)生產(chǎn)環(huán)境進(jìn)行性能評(píng)估時(shí)是不行的。
機(jī)器級(jí)優(yōu)化過的代碼有時(shí)可以達(dá)到更好的性能,但它是以犧牲可移植性為代價(jià)的,在企業(yè)級(jí)應(yīng)用程序中,動(dòng)態(tài)負(fù)載和快速迭代更新是更加重要的。大多數(shù)企業(yè)會(huì)愿意犧牲一點(diǎn)機(jī)器級(jí)優(yōu)化代碼帶來的性能,以此換取Java平臺(tái)的諸多優(yōu)勢(shì):
1、編碼簡(jiǎn)單,易于實(shí)現(xiàn)(意味著可以更快的推向市場(chǎng))
2、有很多非常有才的程序員
3、使用JavaAPI和標(biāo)準(zhǔn)庫(kù)實(shí)現(xiàn)快速開發(fā)
4、可移植性 ―― 無需為每個(gè)平臺(tái)都編寫一套代碼
從源代碼到字節(jié)碼
作為一名Java程序員,你可以已經(jīng)對(duì)編碼、編譯和運(yùn)行這一套流程比較熟悉了。假如說,現(xiàn)在你寫了一個(gè)程序代碼MyApp.java,準(zhǔn)備編譯運(yùn)行。為了運(yùn)行這個(gè)程序,首先,你需要使用JDK內(nèi)建的Java語言編譯器:javac,對(duì)這個(gè)文件進(jìn)行編譯,它可以將Java源代碼編譯為字節(jié)碼。javac將根據(jù)Java程序的源代碼生成對(duì)應(yīng)的可執(zhí)行字節(jié)碼,并將其保存為同名類文件:MyApp.class。在經(jīng)過編譯階段后,你就可以在命令行中使用java命令或其他啟動(dòng)腳本載入可執(zhí)行的類文件來運(yùn)行程序,并且可以為程序添加啟動(dòng)參數(shù)。之后,類會(huì)被載入到運(yùn)行時(shí)(這里指的是正在運(yùn)行的JVM),程序開始運(yùn)行。
上面所描述的就是在運(yùn)行Java應(yīng)用程序時(shí)的表面過程,但現(xiàn)在,我們要深入挖掘一下,在調(diào)用Java命令時(shí),到底發(fā)生了什么?JVM到底是什么?大多數(shù)程序員是通過不斷的調(diào)優(yōu),即使用相應(yīng)的啟動(dòng)參數(shù),與JVM進(jìn)行交互,使Java程序運(yùn)行的更快,同時(shí)避免程序出現(xiàn)“OutofMemoryError”錯(cuò)誤。但你是否想過,為什么我們必須要通過JVM來運(yùn)行Java應(yīng)用程序呢?
什么是JVM
簡(jiǎn)單來說,JVM是用于執(zhí)行Java應(yīng)用程序和字節(jié)碼的軟件模塊,并且可以將字節(jié)碼轉(zhuǎn)換為特定硬件和特定操作系統(tǒng)的本地代碼。正因如此,JVM使Java程序做到了“一次編寫,到處運(yùn)行”。Java語言的可移植性是得到企業(yè)級(jí)應(yīng)用程序開發(fā)者青睞的關(guān)鍵:開發(fā)者無需因平臺(tái)不同而把程序重新編寫一遍,因?yàn)橛蠮VM負(fù)責(zé)處理字節(jié)碼到本地代碼的轉(zhuǎn)換和平臺(tái)相關(guān)優(yōu)化的工作。
基本上來說,JVM是一個(gè)虛擬運(yùn)行環(huán)境,對(duì)于字節(jié)碼來說就像是一個(gè)機(jī)器一樣,可以執(zhí)行任務(wù),并通過底層實(shí)現(xiàn)執(zhí)行內(nèi)存相關(guān)的操作。
JVM也可以在運(yùn)行java應(yīng)用程序時(shí),很好的管理動(dòng)態(tài)資源。這指的是他可以正確的分配、回收內(nèi)存,在不同的機(jī)器上維護(hù)一個(gè)具有一致性的線程模型,并且可以為當(dāng)前的CPU架構(gòu)組織可執(zhí)行指令。JVM解放了程序員,使程序員不必再關(guān)心對(duì)象的生命周期,使程序員不必再關(guān)心應(yīng)該在何時(shí)釋放內(nèi)存。而這,正是使用著類似C語言的非動(dòng)態(tài)語言的程序員心中永遠(yuǎn)的痛。
你可以將JVM當(dāng)做是一種專為Java而生的特殊的操作系統(tǒng),它的工作是管理運(yùn)行Java應(yīng)用程序的運(yùn)行時(shí)環(huán)境。簡(jiǎn)單來說,JVM就是運(yùn)行字節(jié)碼指令的虛擬執(zhí)行環(huán)境,并且可以分配執(zhí)行任務(wù),或通過底層實(shí)現(xiàn)對(duì)內(nèi)存進(jìn)行操作。
JVM組件簡(jiǎn)介
關(guān)于JVM內(nèi)部原理與性能優(yōu)化有很多內(nèi)容可寫。作為這個(gè)系列的開篇文章,我簡(jiǎn)單介紹JVM的內(nèi)部組件。這個(gè)簡(jiǎn)要介紹對(duì)于那些JVM新手比較有幫助,也是為后面的深入討論做個(gè)鋪墊。
從一種語言到另一種 ―― 關(guān)于Java編譯器
編譯器以一種語言為輸入,生成另一種可執(zhí)行語言作為輸出。Java編譯器主要完成2個(gè)任務(wù):
1、實(shí)現(xiàn)Java語言的可移植性,不必局限于某一特定平臺(tái);
2、確保輸出代碼可以在目標(biāo)平臺(tái)能夠有效率的運(yùn)行。
編譯器可以是靜態(tài)的,也可以是動(dòng)態(tài)的。靜態(tài)編譯器,如javac,它以Java源代碼為輸入,將其編譯為字節(jié)碼(一種可以運(yùn)行JVM中的語言)。*靜態(tài)編譯器*解釋輸入的源代碼,而生成可執(zhí)行輸出代碼則會(huì)在程序真正運(yùn)行時(shí)用到。因?yàn)檩斎胧庆o態(tài)的,所有輸出結(jié)果總是相同的。只有當(dāng)你修改的源代碼并重新編譯時(shí),才有可能看到不同的編譯結(jié)果。
動(dòng)態(tài)編譯器,如使用Just-In-Time(JIT,即時(shí)編譯)技術(shù)的編譯器,會(huì)動(dòng)態(tài)的將一種編程語言編譯為另一種語言,這個(gè)過程是在程序運(yùn)行中同時(shí)進(jìn)行的。JIT編譯器會(huì)收集程序的運(yùn)行時(shí)數(shù)據(jù)(在程序中插入性能計(jì)數(shù)器),再根據(jù)運(yùn)行時(shí)數(shù)據(jù)和當(dāng)前運(yùn)行環(huán)境數(shù)據(jù)動(dòng)態(tài)規(guī)劃編譯方案。動(dòng)態(tài)編譯可以生成更好的序列指令,使用更有效率的指令集合替換原指令集合,或剔除冗余操作。收集到的運(yùn)行時(shí)數(shù)據(jù)越多,動(dòng)態(tài)編譯的效果就越好;這通常稱為代碼優(yōu)化或重編譯。
動(dòng)態(tài)編譯使你的程序可以應(yīng)對(duì)在不同負(fù)載和行為下對(duì)新優(yōu)化的需求。這也是為什么動(dòng)態(tài)編譯器非常適合Java運(yùn)行時(shí)。這里需要注意的地方是,動(dòng)態(tài)編譯器需要?jiǎng)佑妙~外的數(shù)據(jù)結(jié)構(gòu)、線程資源和CPU指令周期,才能收集運(yùn)行時(shí)信息和優(yōu)化的工作。若想完成更高級(jí)點(diǎn)的優(yōu)化工作,就需要更多的資源。但是在大多數(shù)運(yùn)行環(huán)境中,相對(duì)于獲得的性能提升來說,動(dòng)態(tài)編譯的帶來的性能損耗其實(shí)是非常小的―― 動(dòng)態(tài)編譯后的代碼的運(yùn)行效率可以比純解釋執(zhí)行(即按照字節(jié)碼運(yùn)行,不做任何修改)快5到10倍。
內(nèi)存分配與垃圾回收
內(nèi)存分配是以線程為單位,在“Java進(jìn)程專有內(nèi)存地址空間”中,也就是Java堆中分配的。在普通的客戶端Java應(yīng)用程序中,內(nèi)存分配都是單線程進(jìn)行的。但是,在企業(yè)級(jí)應(yīng)用程序和服務(wù)器端應(yīng)用程序中,單線程內(nèi)存分配卻并不是個(gè)好辦法,因?yàn)樗鼰o法充分利用現(xiàn)代多核時(shí)代的并行特性。
并行應(yīng)用程序設(shè)計(jì)要求JVM確保多線程內(nèi)存分配不會(huì)在同一時(shí)間將同一塊地址空間分配給多個(gè)線程。你可以在整個(gè)內(nèi)存空間中加鎖來解決這個(gè)問題,但是這個(gè)方法(即所謂的“堆鎖”)開銷較大,因?yàn)樗仁顾芯€程在分配內(nèi)存時(shí)逐個(gè)執(zhí)行,對(duì)資源利用和應(yīng)用程序性能有較大影響。多核程序的一個(gè)額外特點(diǎn)是需要有新的資源分配方案,避免出現(xiàn)單線程、序列化資源分配的性能瓶頸。
常用的解決方案是將堆劃分為幾個(gè)區(qū)域,每個(gè)區(qū)域都有適當(dāng)?shù)拇笮?,?dāng)然具體的大小需要根據(jù)實(shí)際情況做相應(yīng)的調(diào)整,因?yàn)椴煌瑧?yīng)用程序之間,內(nèi)存分配速率、對(duì)象大小和線程數(shù)量的差別是非常大的。ThreadLocal Allocation Buffer(TLAB),有時(shí)也稱為ThraedLocal Area(TLA),是線程自己使用的專用內(nèi)存分配區(qū)域,在使用的時(shí)候無需獲取堆鎖。當(dāng)這個(gè)區(qū)域用滿的時(shí)候,線程會(huì)申請(qǐng)新的區(qū)域,直到堆中所有預(yù)留的區(qū)域都用光了。當(dāng)堆中沒有足夠的空間來分配內(nèi)存時(shí),堆就“滿”了,即堆上剩余的空間裝不下待分配空間的對(duì)象。當(dāng)堆滿了的時(shí)候,垃圾回收就開始了。
碎片化
使用TLAB的一個(gè)風(fēng)險(xiǎn)是,由于堆上內(nèi)存碎片的增加,使用內(nèi)存的效率會(huì)下降。如果應(yīng)用程序創(chuàng)建的對(duì)象的大小無法填滿TLAB,而這塊TLAB中剩下的空間又太小,無法分配給新的對(duì)象,那么這塊空間就被浪費(fèi)了,這就是所謂的“碎片”。如果“碎片”周圍已分配出去的內(nèi)存長(zhǎng)時(shí)間無法回收,那么這塊碎片就長(zhǎng)時(shí)間無法得到利用。
碎片化是指堆上存在了大量的碎片,由于這些小碎片的存在而使堆無法得到有效利用,浪費(fèi)了堆空間。為應(yīng)用程序設(shè)置TLAB的大小時(shí),若是沒有對(duì)應(yīng)用程序中對(duì)象大小和生命周期和合理評(píng)估,導(dǎo)致TLAB的大小設(shè)置不當(dāng),就會(huì)是使堆逐漸碎片化。隨著應(yīng)用程序的運(yùn)行,被浪費(fèi)的碎片空間會(huì)逐漸增多,導(dǎo)致應(yīng)用程序性能下降。這是因?yàn)橄到y(tǒng)無法為新線程和新對(duì)象分配空間,為防止出現(xiàn)OOM(out-of-memory)錯(cuò)誤,而頻繁GC的緣故。
對(duì)于TLAB產(chǎn)生的空間浪費(fèi)這個(gè)問題,可以采用“曲線救國(guó)”的策略來解決。例如,可以根據(jù)應(yīng)用程序的具體環(huán)境調(diào)整TLAB的大小。這個(gè)方法既可以臨時(shí),也可以徹底的避免堆空間的碎片化,但需要隨著應(yīng)用程序內(nèi)存分配行為的變化而修改TLAB的值。此外,還可以使用一些復(fù)雜的JVM算法和其他的方法來組織堆空間來獲得更有效率的內(nèi)存分配行為。例如,JVM可以實(shí)現(xiàn)空閑列表(free-list),空閑列表中保存了堆中指定大小的空閑塊。具有類似大小空閑塊保存在一個(gè)空閑列表中,因此可以創(chuàng)建多個(gè)空閑列表,每個(gè)空閑列表保存某個(gè)范圍內(nèi)的空閑塊。在某些事例中,使用空閑列表會(huì)比使用按實(shí)際大小分配內(nèi)存的策略更有效率。線程為某個(gè)對(duì)象分配內(nèi)存時(shí),可以在空閑列表中尋找與對(duì)象大小最接近的空間塊使用,相對(duì)于使用固定大小的TLAB,這種方法更有利于避免碎片化的出現(xiàn)。
GC往事:早期的垃圾回收器有多個(gè)老年代,但實(shí)際上,存在多個(gè)老年代是弊大于利的。
另一種對(duì)抗碎片化的方法是創(chuàng)建一個(gè)所謂的年輕代,在這個(gè)專有的堆空間中,保存了所有新創(chuàng)建的對(duì)象。堆空間中剩余的空間就是所謂的老年代。老年代用于保存具有較長(zhǎng)生命周期的對(duì)象,即當(dāng)對(duì)象能夠挺過幾輪GC而不被回收,或者對(duì)象本身很大(一般來說,大對(duì)象都具有較長(zhǎng)的壽命周期)時(shí),它們就會(huì)被保存到老年代。為了讓你能夠更好的理解這個(gè)方法,我們有必要談?wù)劺厥铡?/p>
垃圾回收與應(yīng)用程序性能
垃圾回收就是JVM釋放那些沒有引用指向的堆內(nèi)存的操作。當(dāng)垃圾回收首次觸發(fā)時(shí),有引用指向的對(duì)象會(huì)被保存下來,那些沒有引用指向的對(duì)象占用的空間會(huì)被回收。當(dāng)所有可回收的內(nèi)存都被回收后,這些空間就可以被分配給新的對(duì)象了。
垃圾回收不會(huì)回收仍有引用指向的對(duì)象;否則就會(huì)違反JVM規(guī)范。這個(gè)規(guī)則有一個(gè)例外,就是對(duì)軟引用或弱引用的使用,當(dāng)垃圾回收器發(fā)現(xiàn)內(nèi)存快要用完時(shí),會(huì)回收只有軟引用或弱引用指向的對(duì)象所占用的內(nèi)存。我的建議是,盡量避免使用弱引用,因?yàn)镴ava規(guī)范中存在的模糊的表述可能會(huì)使你對(duì)弱引用的使用產(chǎn)生誤解。此外,Java本身是動(dòng)態(tài)內(nèi)存管理的,你沒必要考慮什么時(shí)候該釋放哪塊內(nèi)存。
對(duì)于垃圾回收來說,挑戰(zhàn)在于,如何將垃圾回收對(duì)應(yīng)用程序造成的影響降到最小。如果垃圾回收?qǐng)?zhí)行的不充分,那么應(yīng)用程序遲早會(huì)發(fā)生OOM錯(cuò)誤;如果垃圾回收?qǐng)?zhí)行的太頻繁,會(huì)對(duì)應(yīng)用程序的吞吐量和響應(yīng)時(shí)間造成影響,當(dāng)然,這都不是好的影響。
GC算法
目前已經(jīng)出現(xiàn)了很多垃圾回收算法。在這個(gè)系列文章中將對(duì)其中的一些進(jìn)行介紹。概括來說,垃圾回收主要有兩種方式,引用計(jì)數(shù)(reference counting)和引用追蹤(referencetracing)。
引用計(jì)數(shù)垃圾回收器會(huì)記錄指向某個(gè)對(duì)象的引用的數(shù)目。當(dāng)指向某個(gè)對(duì)象引用數(shù)位0時(shí),該對(duì)象占用的內(nèi)存就可以被回收了,這是引用計(jì)數(shù)垃圾回收的一個(gè)主要優(yōu)點(diǎn)。使用引用計(jì)數(shù)垃圾回收需要克服的難點(diǎn)在于如何解決循環(huán)引用帶來的問題,以及如何保證引用計(jì)數(shù)的實(shí)效性。
引用追蹤垃圾回收器會(huì)標(biāo)記所有仍有引用指向的對(duì)象,并從已標(biāo)記的對(duì)象出發(fā),繼續(xù)標(biāo)記這些對(duì)象指向的對(duì)象。當(dāng)所有仍有引用指向的對(duì)象都被標(biāo)記為“l(fā)ive”后,所有未標(biāo)記的對(duì)象會(huì)被回收。這種方式可以解決循環(huán)引用結(jié)果帶來的問題,但是大多數(shù)情況下,垃圾回收器必須等待標(biāo)記完全結(jié)束才能開始進(jìn)行垃圾回收。
上面提到的兩種算法有多種不同的實(shí)現(xiàn)方法,其中最著名的是標(biāo)記或拷貝算法(markingor copying algorithm)和并行或并發(fā)算法(parallelor concurrent algorithm)。我將在后續(xù)的文章中對(duì)它們進(jìn)行介紹。
分代垃圾回收的意思是,將堆劃分為幾個(gè)不同的區(qū)域,分別用于存儲(chǔ)新對(duì)象和老對(duì)象。其中“老對(duì)象”指的是挺過了幾輪垃圾回收而不死的對(duì)象。將堆空間分為年輕代和老年代,分別用于存儲(chǔ)新對(duì)象和老對(duì)象可以通過回收生命周期較短的對(duì)象,并將生命周期較長(zhǎng)的對(duì)象從年輕代提升到老年代的方法來減少堆空間中的碎片,降低堆空間碎片化的風(fēng)險(xiǎn)。此外,使用年輕代還有一個(gè)好處是,它可以推出對(duì)老年代進(jìn)行垃圾回收的需求(對(duì)老年代進(jìn)行垃圾回收的代價(jià)比較大,因?yàn)槔夏甏心切┥芷谳^長(zhǎng)的對(duì)象通常包含有更多的引用,遍歷一次需要花費(fèi)更多的時(shí)間),因那些生命周期較短的對(duì)通常會(huì)重用年輕代中的空間。
還有一個(gè)值得一提的算法改進(jìn)是壓縮,它可以用來管理堆空間中的碎片。基本上壓縮就是將對(duì)象移動(dòng)到一起,再釋放掉較大的連續(xù)空間。如果你對(duì)磁盤碎片和處理磁盤碎片的工具比較熟悉的話你就會(huì)理解壓縮的含義了,只不過這里的壓縮是工作在Java堆空間中的。我將在該系列后續(xù)的內(nèi)容中對(duì)壓縮進(jìn)行介紹。
結(jié)論:回顧與展望
JVM實(shí)現(xiàn)了可移植性(“一次編寫,到處運(yùn)行”)和動(dòng)態(tài)內(nèi)存管理,這兩個(gè)特點(diǎn)也是其廣受歡迎,并且具有較高生產(chǎn)力的原因。作為這個(gè)系列文章的第一篇,我介紹了編譯器如何將字節(jié)碼轉(zhuǎn)換為平臺(tái)相關(guān)指令的語言,以及如何動(dòng)態(tài)優(yōu)化Java程序的運(yùn)行性能。不同的編譯器迎合了不同應(yīng)用程序的需要。此外,簡(jiǎn)單介紹了內(nèi)存分配和垃圾回收的一點(diǎn)內(nèi)容,及其與Java應(yīng)用程序性能的關(guān)系?;旧蠈?,Java應(yīng)用程序運(yùn)行的速度越快,填滿Java堆所需的時(shí)間就越短,觸發(fā)垃圾回收的頻率也越高。這里遇到的問題就是,在應(yīng)用程序出現(xiàn)OOM錯(cuò)誤之前,如何在對(duì)應(yīng)用程序造成的影響盡可能小的情況下,回收足夠多的內(nèi)存空間。
JVM性能優(yōu)化,Part 2 ―― 編譯器
作為JVM性能優(yōu)化系列文章的第2篇,本文將著重介紹Java編譯器,此外還將對(duì)JIT編譯器常用的一些優(yōu)化措施進(jìn)行討論。EvaAndreasson將對(duì)不同種類的編譯器做介紹,并比較客戶端、服務(wù)器端和層次編譯產(chǎn)生的編譯結(jié)果在性能上的區(qū)別,此外將對(duì)通用的JVM優(yōu)化做介紹,包括死代碼剔除、內(nèi)聯(lián)以及循環(huán)優(yōu)化。
Java編譯器存在是Java編程語言能獨(dú)立于平臺(tái)的根本原因。軟件開發(fā)者可以盡全力編寫程序,然后由Java編譯器將源代碼編譯為針對(duì)于特定平臺(tái)的高效、可運(yùn)行的代碼。不同類型的編譯器適合于不同應(yīng)用程序的需求,使編譯結(jié)果可以滿足期望的性能要求。對(duì)編譯器基本原理了解得越多,在優(yōu)化Java應(yīng)用程序性能時(shí)就越能得心應(yīng)手。
什么是編譯器?
簡(jiǎn)單來說,編譯器就是將一種編程語言作為輸入,輸出另一種可執(zhí)行語言的工具。大家都熟悉的javac就是一個(gè)編譯器,所有標(biāo)準(zhǔn)版的JDK中都帶有這個(gè)工具。javac以Java源代碼作為輸入,將其翻譯為可由JVM執(zhí)行的字節(jié)碼。翻譯后的字節(jié)碼存儲(chǔ)在.class文件中,在啟動(dòng)Java進(jìn)程的時(shí)候,被載入到Java運(yùn)行時(shí)中。
標(biāo)準(zhǔn)CPU并不能識(shí)別字節(jié)碼,它需要被轉(zhuǎn)換為當(dāng)前平臺(tái)所能理解的本地指令。在JVM中,有專門的組件負(fù)責(zé)將字節(jié)碼編譯為平臺(tái)相關(guān)指令,實(shí)際上,這也是一種編譯器。有些JVM編譯器可以處理多層級(jí)的編譯工作,例如,編譯器在最終將字節(jié)碼轉(zhuǎn)換為平臺(tái)相關(guān)指令前,會(huì)為相關(guān)的字節(jié)碼建立多層級(jí)的中間表示(intermediaterepresentation)。
以平臺(tái)未知的角度看,我們希望盡可能的保持平臺(tái)獨(dú)立性,因此,最后一級(jí)的編譯,也就是從最低級(jí)表示到實(shí)際機(jī)器碼的轉(zhuǎn)換,是與具體平臺(tái)的處理器架構(gòu)息息相關(guān)的。在最高級(jí)的表示上,會(huì)因使用靜態(tài)編譯器還是動(dòng)態(tài)編譯器而有所區(qū)別。
靜態(tài)編譯器與動(dòng)態(tài)編譯器
前文提到的javac就是使用靜態(tài)編譯器的例子。靜態(tài)編譯器解釋輸入的源代碼,并輸出程序運(yùn)行時(shí)所需的可執(zhí)行文件。如果你修改了源代碼,那么就需要使用編譯器來重新編譯代碼,否則輸出的可執(zhí)行性文件不會(huì)發(fā)生變化;這是因?yàn)殪o態(tài)編譯器的輸入是靜態(tài)的普通文件。
使用靜態(tài)編譯器時(shí),下面的Java代碼
static int add7( int x ) { return x+7; } |
|
會(huì)生成類似如下的字節(jié)碼:
iload0 bipush 7 iadd ireturn |
|
動(dòng)態(tài)編譯器會(huì)動(dòng)態(tài)的將一種編程語言編譯為另一種,即在程序運(yùn)行時(shí)執(zhí)行編譯工作。動(dòng)態(tài)編譯與優(yōu)化使運(yùn)行時(shí)可以根據(jù)當(dāng)前應(yīng)用程序的負(fù)載情況而做出相應(yīng)的調(diào)整。動(dòng)態(tài)編譯器非常適合用于Java運(yùn)行時(shí)中,因?yàn)镴ava運(yùn)行時(shí)通常運(yùn)行在無法預(yù)測(cè)而又會(huì)隨著運(yùn)行而有所變動(dòng)的環(huán)境中。大部分JVM都會(huì)使用諸如Just-In-Time編譯器的動(dòng)態(tài)編譯器。這里面需要注意的是,大部分動(dòng)態(tài)編譯器和代碼優(yōu)化有時(shí)需要使用額外的數(shù)據(jù)結(jié)構(gòu)、線程和CPU資源。要做的優(yōu)化或字節(jié)碼上下文分析越高級(jí),編譯過程所消耗的資源就越多。在大多數(shù)運(yùn)行環(huán)境中,相比于經(jīng)過動(dòng)態(tài)編譯和代碼優(yōu)化所獲得的性能提升,這些損耗微不足道。
JVM的多樣性與Java平臺(tái)的獨(dú)立性
所有的JVM實(shí)現(xiàn)都有一個(gè)共同點(diǎn),即它們都試圖將應(yīng)用程序的字節(jié)碼轉(zhuǎn)換為本地機(jī)器指令。一些JVM在載入應(yīng)用程序后會(huì)解釋執(zhí)行應(yīng)用程序,同時(shí)使用性能計(jì)數(shù)器來查找“熱點(diǎn)”代碼。還有一些JVM會(huì)調(diào)用解釋執(zhí)行的階段,直接編譯運(yùn)行。資源密集型編譯任務(wù)對(duì)應(yīng)用程序來說可能會(huì)產(chǎn)生較大影響,尤其是那些客戶端模式下運(yùn)行的應(yīng)用程序,但是資源密集型編譯任務(wù)可以執(zhí)行一些比較高級(jí)的優(yōu)化任務(wù)。
如果你是Java初學(xué)者,JVM本身錯(cuò)綜復(fù)雜結(jié)構(gòu)會(huì)讓你暈頭轉(zhuǎn)向的。不過,好消息是你無需精通JVM。JVM自己會(huì)做好代碼編譯和優(yōu)化的工作,所以你無需關(guān)心如何針對(duì)目標(biāo)平臺(tái)架構(gòu)來編寫應(yīng)用程序才能編譯、優(yōu)化,從而生成更好的本地機(jī)器指令。
從字節(jié)碼到可運(yùn)行的程序
當(dāng)你編寫完Java源代碼并將之編譯為字節(jié)碼后,下一步就是將字節(jié)碼指令編譯為本地機(jī)器指令。這一步會(huì)由解釋器或編譯器完成。
解釋
解釋是最簡(jiǎn)單的字節(jié)碼編譯形式。解釋器查找每條字節(jié)碼指令對(duì)應(yīng)的硬件指令,再由CPU執(zhí)行相應(yīng)的硬件指令。你可以將解釋器想象為一個(gè)字典:每個(gè)單詞(字節(jié)碼指令)都有準(zhǔn)確的解釋(本地機(jī)器指令)。由于解釋器每次讀取一個(gè)字節(jié)碼指令并立即執(zhí)行,因此它就沒有機(jī)會(huì)對(duì)某個(gè)指令集合進(jìn)行優(yōu)化。由于每次執(zhí)行字節(jié)碼時(shí),解釋器都需要做相應(yīng)的解釋工作,因此程序運(yùn)行起來就很慢。解釋執(zhí)行可以準(zhǔn)確執(zhí)行字節(jié)碼,但是未經(jīng)優(yōu)化而輸出的指令集難以發(fā)揮目標(biāo)平臺(tái)處理器的最佳性能。
編譯
另一方面,編譯執(zhí)行應(yīng)用程序時(shí),*編譯器*會(huì)將加載運(yùn)行時(shí)用到的全部代碼。因?yàn)榫幾g器可以將字節(jié)碼編譯為本地代碼,因此它可以獲取到完整或部分運(yùn)行時(shí)上下文信息,并依據(jù)收集到的信息決定到底應(yīng)該如何編譯字節(jié)碼。編譯器是根據(jù)諸如指令的不同執(zhí)行分支和運(yùn)行時(shí)上下文數(shù)據(jù)等代碼信息來指定決策的。
當(dāng)字節(jié)碼序列被編譯為機(jī)器代碼指令集合時(shí),就可以對(duì)這個(gè)指令集合做一些優(yōu)化操作了,優(yōu)化后的指令集合會(huì)被存儲(chǔ)到成為codecache的數(shù)據(jù)結(jié)構(gòu)中。當(dāng)下一次執(zhí)行這部分字節(jié)碼序列時(shí),就會(huì)執(zhí)行這些經(jīng)過優(yōu)化后被存儲(chǔ)到codecache的指令集合。在某些情況下,性能計(jì)數(shù)器會(huì)失效,并覆蓋掉先前所做的優(yōu)化,這時(shí),編譯器會(huì)執(zhí)行一次新的優(yōu)化過程。使用codecache的好處是優(yōu)化后的指令集可以立即執(zhí)行—— 無需像解釋器一樣再經(jīng)過查找的過程或編譯過程!這可以加速程序運(yùn)行,尤其是像Java應(yīng)用程序這種同一個(gè)方法會(huì)被多次調(diào)用應(yīng)用程序。
優(yōu)化
隨著動(dòng)態(tài)編譯器一起出現(xiàn)的是性能計(jì)數(shù)器。例如,編譯器會(huì)插入性能計(jì)數(shù)器,以統(tǒng)計(jì)每個(gè)字節(jié)碼塊(對(duì)應(yīng)與某個(gè)被調(diào)用的方法)的調(diào)用次數(shù)。在進(jìn)行相關(guān)優(yōu)化時(shí),編譯器會(huì)使用收集到的數(shù)據(jù)來判斷某個(gè)字節(jié)碼塊有多“熱”,這樣可以最大程度的降低對(duì)當(dāng)前應(yīng)用程序的影響。運(yùn)行時(shí)數(shù)據(jù)監(jiān)控有助于編譯器完成多種代碼優(yōu)化工作,進(jìn)一步提升代碼執(zhí)行性能。隨著收集到的運(yùn)行時(shí)數(shù)據(jù)越來越多,編譯器就可以完成一些額外的、更加復(fù)雜的代碼優(yōu)化工作,例如編譯出更高質(zhì)量的目標(biāo)代碼,使用運(yùn)行效率更高的代碼替換原代碼,甚至是剔除冗余操作等。
示例
考慮如下代碼:
static int add7( int x ) { return x+7; } |
|
這段代碼經(jīng)過javac編譯后會(huì)產(chǎn)生如下的字節(jié)碼:
iload0 bipush 7 iadd ireturn |
|
當(dāng)調(diào)用這段代碼時(shí),字節(jié)碼塊會(huì)被動(dòng)態(tài)的編譯為本地機(jī)器指令。當(dāng)性能計(jì)數(shù)器(如果這段代碼應(yīng)用了性能計(jì)數(shù)器的話)發(fā)現(xiàn)這段代碼的運(yùn)行次數(shù)超過了某個(gè)閾值后,動(dòng)態(tài)編譯器會(huì)對(duì)這段代碼進(jìn)行優(yōu)化編譯。后帶的代碼可能會(huì)是下面這個(gè)樣子:
lea rax,[rdx+7] ret |
|
各擅勝場(chǎng)
不同的Java應(yīng)用程序需要滿足不同的需求。相對(duì)來說,企業(yè)級(jí)服務(wù)器端應(yīng)用程序需要長(zhǎng)時(shí)間運(yùn)行,因此可以做更多的優(yōu)化,而稍小點(diǎn)的客戶端應(yīng)用程序可能要求快速啟動(dòng)運(yùn)行,占資源少。接下來我們考察三種編譯器設(shè)置及其各自的優(yōu)缺點(diǎn)。
客戶端編譯器
即大家熟知的優(yōu)化編譯器C1。在啟動(dòng)應(yīng)用程序時(shí),添加JVM啟動(dòng)參數(shù)“-client”可以啟用C1編譯器。正如啟動(dòng)參數(shù)所表示的,C1是一個(gè)客戶端編譯器,它專為客戶端應(yīng)用程序而設(shè)計(jì),資源消耗更少,并且在大多數(shù)情況下,對(duì)應(yīng)用程序的啟動(dòng)時(shí)間很敏感。C1編譯器使用性能計(jì)數(shù)器來收集代碼的運(yùn)行時(shí)信息,執(zhí)行一些簡(jiǎn)單、無侵入的代碼優(yōu)化任務(wù)。
服務(wù)器端編譯器
對(duì)于那些需要長(zhǎng)時(shí)間運(yùn)行的應(yīng)用程序,例如服務(wù)器端的企業(yè)級(jí)Java應(yīng)用程序來說,客戶端編譯器所實(shí)現(xiàn)的功能還略有不足,因此服務(wù)器端的編譯會(huì)使用類似C2這類的編譯器。啟動(dòng)應(yīng)用程序時(shí)添加命令行參數(shù)“-server”可以啟用C2編譯器。由于大多數(shù)服務(wù)器端應(yīng)用程序都會(huì)長(zhǎng)時(shí)間運(yùn)行,因此相對(duì)于運(yùn)行時(shí)間稍短的輕量級(jí)客戶端應(yīng)用程序,在服務(wù)器端應(yīng)用程序中啟用C2編譯器可以收集到更多的運(yùn)行時(shí)數(shù)據(jù),也就可以執(zhí)行一些更高級(jí)的編譯技術(shù)與算法。
提示:給服務(wù)器端編譯器熱身
對(duì)于服務(wù)器端編譯器來說,在應(yīng)用程序開始運(yùn)行之后,編譯器可能會(huì)在一段時(shí)間之后才開始優(yōu)化“熱點(diǎn)”代碼,所以服務(wù)器端編譯器通常需要經(jīng)過一個(gè)“熱身”階段。在服務(wù)器端編譯器執(zhí)行性能優(yōu)化任務(wù)之前,要確保應(yīng)用程序的各項(xiàng)準(zhǔn)備工作都已就緒。給予編譯器足夠多的時(shí)間來完成編譯、優(yōu)化的工作才能取得更好的效果。
在執(zhí)行編譯任務(wù)優(yōu)化時(shí),服務(wù)器端編譯器要比客戶端編譯器綜合考慮更多的運(yùn)行時(shí)信息,執(zhí)行更復(fù)雜的分支分析,即對(duì)哪種優(yōu)化路徑能取得更好的效果作出判斷。獲取的運(yùn)行時(shí)數(shù)據(jù)越多,編譯優(yōu)化所產(chǎn)生的效果越好。當(dāng)然,要完成一些復(fù)雜的、高級(jí)的性能分析任務(wù),編譯器就需要消耗更多的資源。使用了C2編譯器的JVM會(huì)消耗更多的資源,例如更多的線程,更多的CPU指令周期,以及更大的codecache等。
層次編譯
層次編譯綜合了服務(wù)器端編譯器和客戶端編譯器的特點(diǎn)。Azul首先在其ZingJVM中實(shí)現(xiàn)了層次編譯。最近(就是JavaSE 7版本),OracleJava HotSpot VM也采用了這種設(shè)計(jì)。在應(yīng)用程序啟動(dòng)階段,客戶端編譯器最為活躍,執(zhí)行一些由較低的性能計(jì)數(shù)器閾值出發(fā)的性能優(yōu)化任務(wù)。此外,客戶端編譯器還會(huì)插入性能計(jì)數(shù)器,為一些更復(fù)雜的性能優(yōu)化任務(wù)準(zhǔn)備指令集,這些任務(wù)將在后續(xù)的階段中由服務(wù)器端編譯器完成。層次編譯可以更有效的利用資源,因?yàn)榫幾g器在執(zhí)行一些對(duì)應(yīng)用程序影響較小的編譯活動(dòng)時(shí)仍可以繼續(xù)收集運(yùn)行時(shí)信息,而這些信息可以在將來用于完成更高級(jí)的優(yōu)化任務(wù)。使用層次編譯可以比解釋性的代碼性能計(jì)數(shù)器手機(jī)到更多的信息。
Figure1中展示了純解釋運(yùn)行、客戶端模式運(yùn)行、服務(wù)器端模式運(yùn)行和層次編譯模式運(yùn)行下性能之間的區(qū)別。X軸表示運(yùn)行時(shí)間(單位時(shí)間)Y軸表示性能(每單位時(shí)間內(nèi)的操作數(shù))。
編譯性能對(duì)比
相比于純解釋運(yùn)行的的代碼,以客戶端模式編譯運(yùn)行的代碼在性能(指單位時(shí)間執(zhí)行的操作)上可以達(dá)到約5到10倍,因此提升了應(yīng)用程序的運(yùn)行性能。其間的區(qū)別主要在于編譯器的效率、編譯器所作的優(yōu)化,以及應(yīng)用程序在設(shè)計(jì)實(shí)現(xiàn)時(shí)針對(duì)目標(biāo)平臺(tái)做了何種程度的優(yōu)化。實(shí)際上,最后一條不在Java程序員的考慮之列。
相比于客戶端編譯器,使用服務(wù)器端編譯器通常會(huì)有30%到50%的性能提升。在大多數(shù)情況下,這種程度的性能提升足以彌補(bǔ)使用服務(wù)器端編譯所帶來的額外資源消耗。
層次編譯綜合了服務(wù)器端編譯器和客戶端編譯器的優(yōu)點(diǎn),使用客戶端編譯模式實(shí)現(xiàn)快速啟動(dòng)和快速優(yōu)化,使用服務(wù)器端編譯模式在后續(xù)的執(zhí)行周期中完成高級(jí)優(yōu)化的編譯任務(wù)。
常用編譯優(yōu)化手段
到目前為止,已經(jīng)介紹了優(yōu)化代碼的價(jià)值,以及常用JVM編譯器是如何以及何時(shí)編譯代碼的。接下來,將用一些實(shí)際的例子做個(gè)總結(jié)。JVM所作的性能優(yōu)化通常在字節(jié)碼這一層級(jí)(或者是更底層的語言表示),但這里我將使用Java編程語言對(duì)優(yōu)化措施進(jìn)行介紹。
死代碼剔除
死代碼剔除指的是,將無法被調(diào)用的代碼(即“死代碼”)從源代碼中剔除。如果編譯器在運(yùn)行時(shí)發(fā)現(xiàn)某些指令是不必要的,它會(huì)簡(jiǎn)單的將其從可執(zhí)行指令集中剔除。例如,在Listing1中,變量被賦予了確定值,卻從未被使用,因此可以在執(zhí)行時(shí)將其完全忽略掉。在字節(jié)碼這一層級(jí),也就不會(huì)有將數(shù)值載入到寄存器的操作。沒有載入操作意味著可以更少的CPU時(shí)間,更好的運(yùn)行性能,尤其是當(dāng)這段代碼是“熱點(diǎn)”代碼的時(shí)候。
Listing 1中展示了示例代碼,其中被賦予了固定值的代碼從未被使用,屬于無用不必要的操作。
Listing 1. Dead code
int timeToScaleMyApp(boolean endlessOfResources) { int reArchitect = 24; int patchByClustering = 15; int useZing = 2; if(endlessOfResources) return reArchitect + useZing; else return useZing; } |
|
在字節(jié)碼這一層級(jí),如果變量被載入但從未使用,編譯器會(huì)檢測(cè)到并剔除這個(gè)死代碼,如Listing2所示。剔除死代碼可以節(jié)省CPU時(shí)間,從而提升應(yīng)用程序的運(yùn)行速度。
Listing 2. The same code followingoptimization
int timeToScaleMyApp(boolean endlessOfResources) { int reArchitect = 24; //unnecessary operation removed here... int useZing = 2; if(endlessOfResources) return reArchitect + useZing; else return useZing; } |
|
冗余剔除是一種類似的優(yōu)化手段,通過剔除掉重復(fù)的指令來提升應(yīng)用程序性能。
內(nèi)聯(lián)
許多優(yōu)化手段都試圖消除機(jī)器級(jí)跳轉(zhuǎn)指令(例如,x86架構(gòu)的JMP指令)。跳轉(zhuǎn)指令會(huì)修改指令指針寄存器,因此而改變了執(zhí)行流程。相比于其他匯編指令,跳轉(zhuǎn)指令是一個(gè)代價(jià)高昂的指令,這也是為什么大多數(shù)優(yōu)化手段會(huì)試圖減少甚至是消除跳轉(zhuǎn)指令。內(nèi)聯(lián)是一種家喻戶曉而且好評(píng)如潮的優(yōu)化手段,這是因?yàn)樘D(zhuǎn)指令代價(jià)高昂,而內(nèi)聯(lián)技術(shù)可以將經(jīng)常調(diào)用的、具有不容入口地址的小方法整合到調(diào)用方法中。Listing3到Listing5中的Java代碼展示了使用內(nèi)聯(lián)的用法。
Listing 3. Caller method
int whenToEvaluateZing(int y) { return daysLeft(y) + daysLeft(0) + daysLeft(y+1); } |
|
Listing 4. Called method
int daysLeft(int x){ if (x == 0) return 0; else return x - 1; } |
|
Listing 5. Inlined method
int whenToEvaluateZing(int y){ int temp = 0; if(y == 0) temp += 0; else temp += y - 1; if(0 == 0) temp += 0; else temp += 0 - 1; if(y+1 == 0) temp += 0; else temp += (y + 1) - 1; return temp; } |
|
在Listing3到Listing5的代碼中,展示了將調(diào)用3次小方法進(jìn)行內(nèi)聯(lián)的示例,這里我們認(rèn)為使用內(nèi)聯(lián)比跳轉(zhuǎn)有更多的優(yōu)勢(shì)。如果被內(nèi)聯(lián)的方法本身就很少被調(diào)用的話,那么使用內(nèi)聯(lián)也沒什么意義,但是對(duì)頻繁調(diào)用的“熱點(diǎn)”方法進(jìn)行內(nèi)聯(lián)在性能上會(huì)有很大的提升。此外,經(jīng)過內(nèi)聯(lián)處理后,就可以對(duì)內(nèi)聯(lián)后的代碼進(jìn)行進(jìn)一步的優(yōu)化,正如Listing6中所展示的那樣。
Listing 6. After inlining, moreoptimizations can be applied
int whenToEvaluateZing(int y){ if(y == 0) return y; else if (y == -1) return y - 1; else return y + y - 1; } |
|
循環(huán)優(yōu)化
當(dāng)涉及到需要減少執(zhí)行循環(huán)時(shí)的性能損耗時(shí),循環(huán)優(yōu)化起著舉足輕重的作用。執(zhí)行循環(huán)時(shí)的性能損耗包括代價(jià)高昂的跳轉(zhuǎn)操作,大量的條件檢查,和未經(jīng)優(yōu)化的指令流水線(即引起CPU空操作或額外周期的指令序列)等。循環(huán)優(yōu)化可以分為很多種,在各種優(yōu)化手段中占有重要比重。其中值得注意的包括以下幾種:
1、合并循環(huán):當(dāng)兩個(gè)相鄰循環(huán)的迭代次數(shù)相同時(shí),編譯器會(huì)嘗試將兩個(gè)循環(huán)體進(jìn)行合并。當(dāng)兩個(gè)循環(huán)體中沒有相互引用的情況,即各自獨(dú)立時(shí),可以同時(shí)執(zhí)行(并行執(zhí)行)。
2、反轉(zhuǎn)循環(huán):基本上將就是用do-while循環(huán)體換掉常規(guī)的while循環(huán),這個(gè)do-while循環(huán)嵌套在if語句塊中。這個(gè)替換操作可以節(jié)省兩次跳轉(zhuǎn)操作,但是,會(huì)增加一個(gè)條件檢查的操作,因此增加的代碼量。這種優(yōu)化方式完美的展示了以少量增加代碼量為代價(jià)換取較大性能的提升—— 編譯器需要在運(yùn)行時(shí)需要權(quán)衡這種得與失,并制定編譯策略。
3、分塊循環(huán):重新組織循環(huán)體,以便迭代數(shù)據(jù)塊時(shí),便于緩存的應(yīng)用。
4、展開循環(huán):減少判斷循環(huán)條件和跳轉(zhuǎn)的次數(shù)。你可以將之理解為將一些迭代的循環(huán)體“內(nèi)聯(lián)”到一起,而無需跨越循環(huán)條件。展開循環(huán)是有風(fēng)險(xiǎn)的,它有可能會(huì)降低應(yīng)用程序的運(yùn)行性能,因?yàn)樗鼤?huì)影響流水線的運(yùn)行,導(dǎo)致產(chǎn)生了冗余指令。再?gòu)?qiáng)調(diào)一遍,展開循環(huán)是編譯器在運(yùn)行時(shí)根據(jù)各種信息來決定是否使用的優(yōu)化手段,如果有足夠的收益的話,那么即使有些性能損耗也是值得的。
至此,已經(jīng)簡(jiǎn)要介紹了編譯器對(duì)字節(jié)碼層級(jí)(以及更底層)進(jìn)行優(yōu)化,以提升應(yīng)用程序在目標(biāo)平臺(tái)的執(zhí)行性能的幾種方式。這里介紹的幾種優(yōu)化手段是比較常用的幾種,只是眾多優(yōu)化技術(shù)中的幾種。在介紹優(yōu)化方法時(shí)配以簡(jiǎn)單示例和相關(guān)解釋,希望可以洗發(fā)你進(jìn)行深度探索的興趣。
總結(jié):回顧
為滿足不同需要而使用不同的編譯器。解釋是將字節(jié)碼轉(zhuǎn)換為本地機(jī)器指令的最簡(jiǎn)單方式,其工作方式是基于對(duì)本地機(jī)器指令表的查找。編譯器可以基于性能計(jì)數(shù)器進(jìn)行性能優(yōu)化,但是需要消耗更多的資源(如codecache,優(yōu)化線程等)。相比于純解釋執(zhí)行代碼,客戶端編譯器可以將應(yīng)用程序的執(zhí)行性能提升一個(gè)數(shù)量級(jí)(約5到10倍)。相比于客戶端編譯器,服務(wù)器端編譯器可以將應(yīng)用程序的執(zhí)行性能提升30%到50%,但會(huì)消耗更多的資源。層次編譯綜合了客戶端編譯器和服務(wù)器端編譯器的優(yōu)點(diǎn),既可以像客戶端編譯器那樣快速啟動(dòng),又可以像服務(wù)器端編譯器那樣,在長(zhǎng)時(shí)間收集運(yùn)行時(shí)信息的基礎(chǔ)上,優(yōu)化應(yīng)用程序的性能。
目前,已經(jīng)出現(xiàn)了很多代碼優(yōu)化的手段。對(duì)編譯器來說,一個(gè)主要的任務(wù)就是分析所有的可能性,權(quán)衡使用某種優(yōu)化手段的利弊,在此基礎(chǔ)上編譯代碼,優(yōu)化應(yīng)用程序的性能。
JVM性能優(yōu)化,Part 3 —— 垃圾回收
Java平臺(tái)的垃圾回收機(jī)制大大提高的開發(fā)人員的生產(chǎn)力,但實(shí)現(xiàn)糟糕的垃圾回收器卻會(huì)大大消耗應(yīng)用程序的資源。本文作為JVM性能優(yōu)化系列的第3篇,EvaAndeasson將為Java初學(xué)者介紹Java平臺(tái)的內(nèi)存模型和GC機(jī)制。她將解釋為什么碎片化(不是GC)是Java應(yīng)用程序出現(xiàn)性能問題的主要原因,以及為什么當(dāng)前主要通過分代垃圾回收和壓縮,而不是其他最具創(chuàng)意的方法,來解決Java應(yīng)用程序中碎片化的問題。
垃圾回收(GC)是旨在釋放不可達(dá)Java對(duì)象所占用的內(nèi)存的過程,是Java virtual machine(JVM)中動(dòng)態(tài)內(nèi)存管理系統(tǒng)的核心組成部分。在一個(gè)典型的垃圾回收周期中,所有仍被引用的對(duì)象,即可達(dá)對(duì)象,會(huì)被保留。沒有被引用的Java對(duì)象所占用的內(nèi)存會(huì)被釋放并回收,以便分配給新創(chuàng)建的對(duì)象。
為了更好的理解垃圾回收與各種不同的GC算法,你首先需要了解一些關(guān)于Java平臺(tái)內(nèi)存模型的內(nèi)容。
垃圾回收與Java平臺(tái)內(nèi)存模型
當(dāng)你在啟動(dòng)Java應(yīng)用程序時(shí)指定了啟動(dòng)參數(shù)_-Xmx_(例如,java-Xmx2g MyApp),則相應(yīng)大小的內(nèi)存會(huì)被分配給Java進(jìn)程。這塊內(nèi)存即所謂的*Java堆*(或簡(jiǎn)稱為*堆*)。這塊專用的內(nèi)存地址空間用于存儲(chǔ)Java應(yīng)用程序(有時(shí)是JVM)所創(chuàng)建的對(duì)象。隨著Java應(yīng)用程序的運(yùn)行,會(huì)不斷的創(chuàng)建新對(duì)象并為之分配內(nèi)存,Java堆(即地址空間)會(huì)逐漸被填滿。最后,Java堆會(huì)被填滿,這就是說想要申請(qǐng)內(nèi)存的線程無法獲得一塊足夠大的連續(xù)空閑空間來存放新創(chuàng)建的對(duì)象。此時(shí),JVM判斷需要啟動(dòng)垃圾回收器來回收內(nèi)存了。當(dāng)Java程序調(diào)用System.gc()方法時(shí),也有可能會(huì)觸發(fā)垃圾回收器以執(zhí)行垃圾回收的工作。使用System.gc()方法并不能保證垃圾回收工作肯定會(huì)被執(zhí)行。在執(zhí)行垃圾回收前,垃圾回收機(jī)制首先會(huì)檢查當(dāng)前是否是一個(gè)“恰當(dāng)?shù)臅r(shí)機(jī)”,而“恰當(dāng)?shù)臅r(shí)機(jī)”指所有的應(yīng)用程序活動(dòng)線程都處于安全點(diǎn)(safepoint),以便啟動(dòng)垃圾回收。簡(jiǎn)單舉例,為對(duì)象分配內(nèi)存時(shí),或正在優(yōu)化CPU指令時(shí),就不是“恰當(dāng)?shù)臅r(shí)機(jī)”,因?yàn)槟憧赡軙?huì)丟失上下文信息,從而得到混亂的結(jié)果。
垃圾回收不應(yīng)該回收當(dāng)前有活動(dòng)引用指向的對(duì)象所占用的內(nèi)存;因?yàn)檫@樣做將違反JVM規(guī)范。在JVM規(guī)范中,并沒有強(qiáng)制要求垃圾回收器立即回收已死對(duì)象(deadobject)。已死對(duì)象最終會(huì)在后續(xù)的垃圾回收周期中被釋放掉。目前,已經(jīng)有多種垃圾回收的實(shí)現(xiàn),它們都包含兩個(gè)溝通的假設(shè)。對(duì)垃圾回收來說,真正的挑戰(zhàn)在于標(biāo)識(shí)出所有活動(dòng)對(duì)象(即仍有引用指向的對(duì)象),回收所有不可達(dá)對(duì)象所占用的內(nèi)存,并盡可能不對(duì)正在運(yùn)行的應(yīng)用程序產(chǎn)生影響。因此,垃圾回收器運(yùn)行的兩個(gè)目標(biāo):
1、快速釋放不可達(dá)對(duì)象所占用的內(nèi)存,防止應(yīng)用程序出現(xiàn)OOM錯(cuò)誤。
2、回收內(nèi)存時(shí),對(duì)應(yīng)用程序的性能(指延遲和吞吐量)的影響要緊性能小。
兩類垃圾回收
Java的2種主要的垃圾回收方式,引用計(jì)數(shù)(referencecounting)和引用追蹤(tracingcollector)。這里,我將深入這兩種垃圾回收方式,并介紹用于生產(chǎn)環(huán)境的實(shí)現(xiàn)了引用追蹤的垃圾回收方式的相關(guān)算法。
引用計(jì)數(shù)垃圾回收器
引用計(jì)數(shù)垃圾回收器會(huì)對(duì)指向每個(gè)Java對(duì)象的引用數(shù)進(jìn)行跟蹤。一旦發(fā)現(xiàn)指向某個(gè)對(duì)象的引用數(shù)為0,則立即回收該對(duì)象所占用的內(nèi)存。引用計(jì)數(shù)垃圾回收的主要優(yōu)點(diǎn)就在于可以立即訪問被回收的內(nèi)存。垃圾回收器維護(hù)未被引用的內(nèi)存并不需要消耗很大的資源,但是保持并不斷更新引用計(jì)數(shù)卻代價(jià)不菲。
使用引用計(jì)數(shù)方式執(zhí)行垃圾回收的主要困難在于保持引用計(jì)數(shù)的準(zhǔn)確性,而另一個(gè)眾所周知的問題在于解決循環(huán)引用結(jié)構(gòu)所帶來的麻煩。如果兩個(gè)對(duì)象互相引用,并且沒有其他存活東西引用它們,那么這兩個(gè)對(duì)象所占用的內(nèi)存將永遠(yuǎn)不會(huì)被釋放,兩個(gè)對(duì)象都會(huì)因引用計(jì)數(shù)不為0而永遠(yuǎn)存活下去(引用計(jì)數(shù)的難點(diǎn)→相互引用)。要解決循環(huán)引用帶來的問題需要,而這會(huì)使算法復(fù)雜度增加,從而影響應(yīng)用程序的運(yùn)行性能。
引用跟蹤垃圾回收
引用跟蹤垃圾回收器基于這樣一種假設(shè),所有存活對(duì)象都可以通過迭代地跟蹤從已知存活對(duì)象集中對(duì)象發(fā)出的引用及引用的引用來找到。可以通過對(duì)寄存器、全局域、以及觸發(fā)垃圾回收時(shí)棧幀的分析來確定初始存活對(duì)象的集合(稱為“根對(duì)象”,或簡(jiǎn)稱為“根”)。在確定了初始存活對(duì)象集后,引用跟蹤垃圾回收器會(huì)跟蹤從這些對(duì)象中發(fā)出的引用,并將找到的對(duì)象標(biāo)記為“活的(live)”。標(biāo)記所有找到的對(duì)象意味著已知存活對(duì)象的集合會(huì)隨時(shí)間而增長(zhǎng)。這個(gè)過程會(huì)一直持續(xù)到所有被引用的對(duì)象(因此是“存活的”對(duì)象)都被標(biāo)記。當(dāng)引用跟蹤垃圾回收器找到所有存活的對(duì)象后,就會(huì)開始回收未被標(biāo)記的對(duì)象。
不同于引用計(jì)數(shù)垃圾回收器,引用跟蹤垃圾回收器可以解決循環(huán)引用的問題。由于標(biāo)記階段的存在,大多數(shù)引用跟蹤垃圾回收器無法立即釋放“已死”對(duì)象所占用的內(nèi)存。
引用跟蹤垃圾回收器廣泛用于動(dòng)態(tài)語言的內(nèi)存管理;到目前為止,在Java編程語言的視線中也是應(yīng)用最廣的,并且在多年的商業(yè)生產(chǎn)環(huán)境中,已經(jīng)證明其實(shí)用性。在本文余下的內(nèi)容中,我將從一些相關(guān)的實(shí)現(xiàn)算法開始,介紹引用跟蹤垃圾回收器,
引用跟蹤垃圾回收器算法
拷貝和*標(biāo)記-清理*垃圾回收算法并非新近發(fā)明,但仍然是當(dāng)今實(shí)現(xiàn)引用跟蹤垃圾回收器最常用的兩種算法。
拷貝垃圾回收器
傳統(tǒng)的拷貝垃圾回收器會(huì)使用一個(gè)“from”區(qū)和一個(gè)“to”區(qū),它們是堆中兩個(gè)不同的地址空間。在執(zhí)行垃圾回收時(shí),from區(qū)中存活對(duì)象會(huì)被拷貝到to區(qū)。當(dāng)from區(qū)中所有的存活對(duì)象都被拷貝到to后,垃圾回收器會(huì)回收整個(gè)from區(qū)。當(dāng)再次分配內(nèi)存時(shí),會(huì)首先從to區(qū)中的空閑地址開始分配。
在該算法的早期實(shí)現(xiàn)中,from區(qū)和to區(qū)會(huì)在垃圾回收周期后進(jìn)行交換,即當(dāng)to區(qū)被填滿后,將再次啟動(dòng)垃圾回收,這是to區(qū)會(huì)“變成”from區(qū)。如圖Figure1所示。
在該算法的近期實(shí)現(xiàn)中,可以將堆中任意地址空間指定為from區(qū)和to區(qū),這樣就不再需要交換from區(qū)和to區(qū),堆中任意地址空間都可以成為from區(qū)或to區(qū)。
拷貝垃圾回收器的一個(gè)優(yōu)點(diǎn)是存活對(duì)象的位置會(huì)被to區(qū)中重新分配,緊湊存放,可以完全消除碎片化。碎片化是其他垃圾回收算法所要面臨的一大問題,這點(diǎn)會(huì)在后續(xù)討論。
拷貝垃圾回收的缺陷:通常來說,拷貝垃圾回收器是“stop-the-world”式的,即在垃圾回收周期內(nèi),應(yīng)用程序是被掛起的,無法工作。在“stop-the-world”式的實(shí)現(xiàn)中,所需要拷貝的區(qū)域越大,對(duì)應(yīng)用程序的性能所造成的影響也越大。對(duì)于那些非常注重響應(yīng)時(shí)間的應(yīng)用程序來說,這是難以接受的。使用拷貝垃圾回收時(shí),你還需要考慮一下最壞情況,即當(dāng)from區(qū)中所有的對(duì)象都是存活對(duì)象的時(shí)候。因此,你不得不給存活對(duì)象預(yù)留出足夠的空間,也就是說to區(qū)必須足夠大,大到可以將from區(qū)中所有的對(duì)象都放進(jìn)去。正是由于這個(gè)缺陷,拷貝垃圾回收算法在內(nèi)存使用效率上略有不足。
標(biāo)記-清理垃圾回收器
大多數(shù)部署在企業(yè)生產(chǎn)環(huán)境的商業(yè)JVM都使用了標(biāo)記-清理(或標(biāo)記)垃圾回收器,這種垃圾回收器并不會(huì)想拷貝垃圾回收器那樣對(duì)應(yīng)用程序的性能有那么大的影響。其中最著名的幾款是CMS、G1、GenPar和DeterministicGC。
標(biāo)記-清理垃圾回收器會(huì)跟蹤引用,并使用標(biāo)記位將每個(gè)找到的對(duì)象標(biāo)記位“l(fā)ive”。通常來說,每個(gè)標(biāo)記位都關(guān)聯(lián)著一個(gè)地址或堆上的一個(gè)地址集合。例如,標(biāo)記位可能是對(duì)象頭(objectheader)中一位,一個(gè)位向量,或是一個(gè)位圖。當(dāng)所有的存活對(duì)象都被標(biāo)記位“l(fā)ive”后,將會(huì)開始*清理*階段。一般來說,垃圾回收器的清理階段包含了通過再次遍歷堆(不僅僅是標(biāo)記位live的對(duì)象集合,而是整個(gè)堆)來定位內(nèi)存地址空間中未被標(biāo)記的區(qū)域,并將其回收。然后,垃圾回收器會(huì)將這些被回收的區(qū)域保存到空閑列表(freelist)中。在垃圾回收器中可以同時(shí)存在多個(gè)空閑列表——通常會(huì)按照保存的內(nèi)存塊的大小進(jìn)行劃分。某些JVM(例如JRockit實(shí)時(shí)系統(tǒng),JRockit Real Time System)在實(shí)現(xiàn)垃圾回收器時(shí)會(huì)給予應(yīng)用程序分析數(shù)據(jù)和對(duì)象大小統(tǒng)計(jì)數(shù)據(jù)來動(dòng)態(tài)調(diào)整空閑列表所保存的區(qū)域塊的大小范圍。
當(dāng)清理階段結(jié)束后,應(yīng)用程序就可以再次啟動(dòng)了。給新創(chuàng)建的對(duì)象分配內(nèi)存時(shí)會(huì)從空閑列表中查找,而空閑列表中內(nèi)存塊的大小需要匹配于新創(chuàng)建的對(duì)象大小、某個(gè)線程中平均對(duì)象大小,或應(yīng)用程序所設(shè)置的TLAB的大小。從空閑列表中為新創(chuàng)建的對(duì)象找到大小合適的內(nèi)存區(qū)域塊有助于優(yōu)化內(nèi)存的使用,減少內(nèi)存中的碎片。
標(biāo)記-清理垃圾回收器的缺陷:標(biāo)記階段的時(shí)長(zhǎng)取決于堆中存活對(duì)象的總量,而清理階段的時(shí)長(zhǎng)則依賴于堆的大小。由于在*標(biāo)記*階段和*清理*階段完成前,你無事可做,因此對(duì)于那些具有較大的堆和較多存活對(duì)象的應(yīng)用程序來說,使用此算法需要想辦法解決暫停時(shí)間(pause-time)較長(zhǎng)這個(gè)問題。
對(duì)于那些內(nèi)存消耗較大的應(yīng)用程序來說,你可以使用一些GC調(diào)優(yōu)選項(xiàng)來滿足其在某些場(chǎng)景下的特殊需求。很多時(shí)候,調(diào)優(yōu)至少可以將標(biāo)記-清理階段給應(yīng)用程序或性能要求(SLA,SLA指定了應(yīng)用程序需要達(dá)到的響應(yīng)時(shí)間的要求,即延遲)所帶來的風(fēng)險(xiǎn)推后。當(dāng)負(fù)載和應(yīng)用程序發(fā)生改變后,需要重新調(diào)優(yōu),因?yàn)槟炒握{(diào)優(yōu)只對(duì)特定的工作負(fù)載和內(nèi)存分配速率有效。
標(biāo)記-清理算法的實(shí)現(xiàn)
目前,標(biāo)記-清理垃圾回收算法至少已有2種商業(yè)實(shí)現(xiàn),并且都已在生產(chǎn)環(huán)境中被證明有效。其一是并行垃圾回收,另一個(gè)是并發(fā)(或多數(shù)時(shí)間并發(fā))垃圾回收。
并行垃圾回收器
并行垃圾回收指的是垃圾回收是多線程并行完成的。大多數(shù)商業(yè)實(shí)現(xiàn)的并行垃圾回收器都是stop-the-world式的垃圾回收器,即在整個(gè)垃圾回收周期結(jié)束前,所有應(yīng)用程序線程都會(huì)被掛起。掛起所有應(yīng)用程序線程使垃圾回收器可以以并行的方式,更有效的完成標(biāo)記和清理工作。并行使得效率大大提高,通??梢栽谙?a target="_blank" >SPECjbb這樣的吞吐量基準(zhǔn)測(cè)試中跑出高分。如果你的應(yīng)用程序好似有限考慮吞吐量的,那么并行垃圾回收是你最好的選擇。對(duì)于大多數(shù)并行垃圾回收器來說,尤其是考慮到應(yīng)用于生產(chǎn)環(huán)境中,最大的問題是,像拷貝垃圾回收算法一樣,在垃圾回收周期內(nèi)應(yīng)用程序無法工作。使用stop-the-world式的并行垃圾回收會(huì)對(duì)優(yōu)先考慮響應(yīng)時(shí)間的應(yīng)用程序產(chǎn)生較大影響,尤其是當(dāng)你有大量的引用需要跟蹤,而此時(shí)恰好又有大量的、具有復(fù)雜結(jié)構(gòu)的對(duì)象存活于堆中的時(shí)候,情況將更加糟糕。(記住,標(biāo)記-清理垃圾回收器回收內(nèi)存的時(shí)間取決于跟蹤存活對(duì)象中所有引用的時(shí)間與遍歷整個(gè)堆的時(shí)間之和。)以并行方式執(zhí)行垃圾回收所導(dǎo)致的應(yīng)用程序暫停會(huì)一直持續(xù)到整個(gè)垃圾回收周期結(jié)束。
并發(fā)垃圾回收器
并發(fā)垃圾回收器更適用于那些對(duì)響應(yīng)時(shí)間比較敏感的應(yīng)用程序。并發(fā)指的是一些(或大多數(shù))垃圾回收工作可以與應(yīng)用程序線程同時(shí)運(yùn)行。由于并非所有的資源都由垃圾回收器使用,因此這里所面臨的問題如何決定何時(shí)開始執(zhí)行垃圾回收,可以保證垃圾回收順利完成。這里需要足夠的時(shí)間來跟蹤存活對(duì)象的引用,并在應(yīng)用程序出現(xiàn)OOM錯(cuò)誤前回收內(nèi)存。如果垃圾回收器無法及時(shí)完成,則應(yīng)用程序就會(huì)拋出OOM錯(cuò)誤。此外,一直做垃圾回收也不好,會(huì)不必要的消耗應(yīng)用程序資源,從而影響應(yīng)用程序吞吐量。要想在動(dòng)態(tài)環(huán)境中保持這種平衡就需要一些技巧,因此設(shè)計(jì)了啟發(fā)式方法來決定何時(shí)開始垃圾回收,何時(shí)執(zhí)行不同的垃圾回收優(yōu)化任務(wù),以及一次執(zhí)行多少垃圾回收優(yōu)化任務(wù)等。
并發(fā)垃圾回收器所面臨的另一個(gè)挑戰(zhàn)是如何決定何時(shí)執(zhí)行一個(gè)需要完整堆快照的操作時(shí)安全的,例如,你需要知道是何時(shí)標(biāo)記所有存活對(duì)象,這樣才能轉(zhuǎn)而進(jìn)入清理階段。在大多數(shù)并行垃圾回收器采用的stop-the-world方式中,*階段轉(zhuǎn)換(phase-switching)*并不需要什么技巧,因?yàn)槭澜缫鸯o止(堆上對(duì)象暫時(shí)不會(huì)發(fā)生變化)。但是,在并發(fā)垃圾回收中,轉(zhuǎn)換階段時(shí)可能并不是安全的。例如,如果應(yīng)用程序修改了一塊垃圾回收器已經(jīng)標(biāo)記過的區(qū)域,可能會(huì)涉及到一些新的或未被標(biāo)記的引用,而這些引用使其指向的對(duì)象成為存活狀態(tài)。在某些并發(fā)垃圾回收的實(shí)現(xiàn)中,這種情況有可能會(huì)使應(yīng)用程序陷入長(zhǎng)時(shí)間運(yùn)行重標(biāo)記(re-mark)的循環(huán),因此當(dāng)應(yīng)用程序需要分配內(nèi)存時(shí)無法得到足夠做的空閑內(nèi)存。
到目前為止的討論中,已經(jīng)介紹了各種垃圾回收器和垃圾回收算法,他們各自適用于不同的場(chǎng)景,滿足不同應(yīng)用程序的需求。各種垃圾回收方式不僅在算法上有所區(qū)別,在具體實(shí)現(xiàn)上也不盡相同。所以,在命令行中指定垃圾回收器之前,最好能了解應(yīng)用程序的需求及其自身特點(diǎn)。在下一節(jié)中,將介紹Java平臺(tái)內(nèi)存模型中的陷阱,在這里,陷阱指的是在動(dòng)態(tài)生產(chǎn)環(huán)境中,Java程序員常常做出的一些中使性能更糟,而非更好的假設(shè)。
為什么調(diào)優(yōu)無法取代垃圾回收
大多數(shù)Java程序員都知道,有不少方法可以最大化Java程序的性能。而當(dāng)今眾多的JVM實(shí)現(xiàn),垃圾回收器實(shí)現(xiàn),以及多到令人頭暈的調(diào)優(yōu)選項(xiàng)都可能會(huì)讓開發(fā)人員將大量的時(shí)間消耗在無窮無盡的性能調(diào)優(yōu)上。這種情況催生了這樣一種結(jié)論,“GC是糟糕的,努力調(diào)優(yōu)以降低GC的頻率或時(shí)長(zhǎng)才是王道”。但是,真這么做是有風(fēng)險(xiǎn)的。
考慮一下針對(duì)指定的應(yīng)用程序需求做調(diào)優(yōu)意味著什么。大多數(shù)調(diào)優(yōu)參數(shù),如內(nèi)存分配速率,對(duì)象大小,響應(yīng)時(shí)間,以及對(duì)象死亡速度等,都是針對(duì)特定的情況而來設(shè)定的,例如測(cè)試環(huán)境下的工作負(fù)載。例如。調(diào)優(yōu)結(jié)果可能有以下兩種:
測(cè)試時(shí)正常,上線就失敗。一旦應(yīng)用程序本身,或工作負(fù)載發(fā)生改變,就需要全部重調(diào)。
調(diào)優(yōu)是需要不斷往復(fù)的。使用并發(fā)垃圾回收器需要做很多調(diào)優(yōu)工作,尤其是在生產(chǎn)環(huán)境中。為滿足應(yīng)用程序的需求,你需要不斷挑戰(zhàn)可能要面對(duì)的最差情況。這樣做的結(jié)果就是,最終形成的配置非??贪?,而且在這個(gè)過程中也浪費(fèi)了大量的資源。這種調(diào)優(yōu)方式(試圖通過調(diào)優(yōu)來消除GC)是一種堂吉訶德式探索——以根本不存在的理由去挑戰(zhàn)一個(gè)假想敵。而事實(shí)是,你針對(duì)某個(gè)特定的負(fù)載而垃圾回收器做的調(diào)優(yōu)越多,你距離Java運(yùn)行時(shí)的動(dòng)態(tài)特性就越遠(yuǎn)。畢竟,有多少應(yīng)用程序的工作負(fù)載能保持不變呢?你所預(yù)估的工作負(fù)載可靠性又有多高呢?
那么,如果不從調(diào)優(yōu)入手又該怎么辦呢?有什么其他的辦法可以防止應(yīng)用程序出現(xiàn)OOM錯(cuò)誤,并降低響應(yīng)時(shí)間呢?這里,首先要做的是明確影響Java應(yīng)用程序性能的真正因素。
碎片化
影響Java應(yīng)用程序性能的罪魁禍?zhǔn)撞⒉皇抢厥掌鞅旧?,而是碎片化,以及垃圾回收器如何處理碎片。碎片是Java堆中空閑空間,但由于連續(xù)空間不夠大而無法容納將要?jiǎng)?chuàng)建的對(duì)象。碎片可能是TLAB中的剩余空間,也可能是(這種情況比較多)被釋放掉的具有較長(zhǎng)生命周期的小對(duì)象所占用的空間。
隨著應(yīng)用程序的運(yùn)行,這種無法使用的碎片會(huì)遍布于整個(gè)堆空間。在某些情況下,這種狀態(tài)會(huì)因靜態(tài)調(diào)優(yōu)選項(xiàng)(如提升速率和空閑列表等)更糟糕,以至于無法滿足應(yīng)用程序的原定需求。這些剩下的空間(也就是碎片)無法被應(yīng)用程序有效利用起來。如果你對(duì)此放任自流,就會(huì)導(dǎo)致不斷垃圾回收,垃圾回收器會(huì)不斷的釋放內(nèi)存以便創(chuàng)建新對(duì)象時(shí)使用。在最差情況下,甚至垃圾回收也無法騰出足夠的內(nèi)存空間(因?yàn)樗槠啵?,JVM會(huì)強(qiáng)制拋出OOM(outof memory)錯(cuò)誤當(dāng)然,你也可以重啟應(yīng)用程序來消除碎片,這樣可以使Java堆煥然一新,于是就又可以為對(duì)象分配內(nèi)存了。但是,重新啟動(dòng)會(huì)導(dǎo)致服務(wù)器停機(jī),另外,一段時(shí)間之后,堆將再次充滿碎片,你也不得不再次重啟。
OOM錯(cuò)誤(OutOfMemoryErrors)會(huì)掛起進(jìn)程,日志中顯示的垃圾回收器很忙,是垃圾回收器努力釋放內(nèi)存的標(biāo)志,也說明了堆中碎片非常多。一些開發(fā)人員通過重新調(diào)優(yōu)垃圾回收器來解決碎片化的問題,但我覺著在解決碎片問題成為垃圾回收的使命之前應(yīng)該用一些更有新意的方法來解決這個(gè)問題。本文后面的內(nèi)容將聚焦于能有效解決碎片化問題的方法:分代式垃圾回收和壓縮。
分代式垃圾回收
這個(gè)理論你可以已經(jīng)聽說過,即在生產(chǎn)環(huán)境中,大部分對(duì)象的生命周期都很短。分代式垃圾回收就源于這個(gè)理論。在分代式垃圾回收中,堆被分為兩個(gè)不同的空間(或成為“代”),每個(gè)空間存放具有不同年齡的對(duì)象,在這里,年齡是指該對(duì)象所經(jīng)歷的垃圾回收的次數(shù)(也就是該對(duì)象挺過了多少次垃圾回收而沒有死掉)。
當(dāng)新創(chuàng)建的對(duì)象所處的空間(*年輕代*)被對(duì)象填滿后,該空間中仍然存活的對(duì)象會(huì)被移動(dòng)到老年代。(譯者注,以HotSpot為例,這里應(yīng)該是挺過若干次GC而不死的,才會(huì)被搬到老年代,而一些比較大的對(duì)象會(huì)直接放到老年代。)大多數(shù)的實(shí)現(xiàn)都將堆會(huì)分為兩代,年輕代和老年代。通常來說,分代式垃圾回收器都是單向拷貝的,即從年輕代向老年代拷貝。近幾年出現(xiàn)的年輕代垃圾回收器已經(jīng)可以實(shí)現(xiàn)并行垃圾回收,當(dāng)然也可以實(shí)現(xiàn)一些其他的垃圾回收算法實(shí)現(xiàn)對(duì)年輕代和老年代的垃圾回收。如果你使用拷貝垃圾回收器(可能具有并行收集功能)對(duì)年輕代進(jìn)行垃圾回收,那垃圾回收是stop-the-world式的。
分代式垃圾回收的缺陷:在分代式垃圾回收中,老年代執(zhí)行垃圾回收的頻率較低,而年輕代較高,垃圾回收的時(shí)間較短,侵入性也較低。但在某些情況下,年輕代的存在會(huì)是老年代的垃圾回收更加頻繁。典型的例子是,相比于Java堆的大小,年輕代被設(shè)置的太大,而應(yīng)用程序中對(duì)象的生命周期又很長(zhǎng)(又或者給年輕代對(duì)象提升速率設(shè)了一個(gè)“不正確”的值)。在這種情況下,老年代因太小而放不下所有的存活對(duì)象,因此垃圾回收器就會(huì)忙于釋放內(nèi)存以便存放從年輕代提升上來的對(duì)象。但一般來說,使用分代式垃圾回收器可以使應(yīng)用程序的性能和系統(tǒng)延遲保持在一個(gè)合適的水平。
使用分代式垃圾回收器的一個(gè)額外效果是部分解決了碎片化的問題,或者說,發(fā)生最差情況的時(shí)間被推遲了??赡茉斐伤槠男?duì)象被分配于年輕代,也在年輕代被釋放掉。老年代中的對(duì)象分布會(huì)相對(duì)緊湊一些,因?yàn)檫@些對(duì)象在從年輕代中提升上來的時(shí)候會(huì)被會(huì)緊湊存放。但隨著應(yīng)用程序的運(yùn)行,如果運(yùn)行時(shí)間夠長(zhǎng)的話,老年代也會(huì)充滿碎片的。這時(shí)就需要對(duì)年輕代和老年代執(zhí)行一次或多次stop-the-world式的全垃圾回收,導(dǎo)致JVM拋出OOM錯(cuò)誤,或者表明提升失敗的錯(cuò)誤。但年輕代的存在使這種情況的出現(xiàn)被推遲了,對(duì)某些應(yīng)用程序來說,這就足夠了。(在某些情況下,這種糟糕情況會(huì)被推遲到應(yīng)用程序完全不關(guān)心GC的時(shí)候。)對(duì)大多數(shù)應(yīng)用程序來說,對(duì)于大多數(shù)使用年輕代作為緩沖的應(yīng)用程序來說,年輕代的存在可以降低出現(xiàn)stop-the-world式垃圾回收頻率,減少拋出OOM錯(cuò)誤的次數(shù)。
調(diào)優(yōu)分代式垃圾回收
正如上面提到的,由于使用了分代式垃圾回收,你需要針對(duì)每個(gè)新版本的應(yīng)用程序和不同的工作負(fù)載來調(diào)整年輕代大小和對(duì)象提升速度。我無法完整評(píng)估出固定運(yùn)行時(shí)的代價(jià):由于針對(duì)某個(gè)指定工作負(fù)載而設(shè)置了一系列優(yōu)化參數(shù),垃圾回收器應(yīng)對(duì)動(dòng)態(tài)變化的能力降低了,而變化是不可避免的。
對(duì)于調(diào)整年輕代大小來說,最重要的規(guī)則是要確保年輕代的大小不應(yīng)該使因執(zhí)行stop-the-world式垃圾回收而導(dǎo)致的暫停過長(zhǎng)。(假設(shè)年輕代中使用的并行垃圾回收器。)還要記住的是,你要在堆中為老年代留出足夠的空間來存放那些生命周期較長(zhǎng)的對(duì)象。下面還有一些在調(diào)優(yōu)分代式垃圾回收器時(shí)需要考慮的因素:
大多數(shù)年輕代垃圾回收都是stop-the-world式的,年輕代越大,相應(yīng)的暫停時(shí)間越長(zhǎng)。所以,對(duì)于那些受GC暫停影響較大的應(yīng)用程序來說,應(yīng)該仔細(xì)斟酌年輕代的大小。
你可以綜合考慮不同代的垃圾回收算法。可以在年輕代使用并行垃圾回收,而在老年代使用并行垃圾回收。
當(dāng)提升失敗頻繁發(fā)生時(shí),這通常說明老年代中的碎片較多。提升失敗指的是老年代中沒有足夠大的空間來存放年輕代中的存活對(duì)象。當(dāng)出現(xiàn)提示失敗時(shí),你可以微調(diào)對(duì)象提升速率(即調(diào)整對(duì)象提升時(shí)年齡),或者確保老年代垃圾回收算法會(huì)將對(duì)象進(jìn)行壓縮(將在下一節(jié)討論),并以一種適合當(dāng)前應(yīng)用程序工作負(fù)載的方式調(diào)整壓縮。你也可以增大堆和各個(gè)代的大小,但這會(huì)使老年代垃圾回收的暫停時(shí)間延長(zhǎng)——記住,碎片化是不可避免的。
分代式垃圾回收最適用于那些具有大量短生命周期對(duì)象的應(yīng)用程序,這些對(duì)象的生命周期短到活不過一次垃圾回收周期。在這種場(chǎng)景中,分代式垃圾回收可有效的減緩碎片化的趨勢(shì),主要是將碎片化隨帶來的影響推出到將來,而那時(shí)可能應(yīng)用程序?qū)Υ撕敛魂P(guān)心。
壓縮
盡管分代式垃圾回收推出了碎片化和OOM錯(cuò)誤出現(xiàn)的時(shí)機(jī),但壓縮仍然是唯一真正解決碎片化的方法。*壓縮*是將對(duì)象移動(dòng)到一起,以便釋放掉大塊連續(xù)內(nèi)存空間的GC策略。因此,壓縮可以生成足夠大的空間來存放新創(chuàng)建的對(duì)象。
移動(dòng)對(duì)象并修改相關(guān)引用是一個(gè)stop-the-world式的操作,這會(huì)對(duì)應(yīng)用程序的性能造成影響。(只有一種情況是個(gè)例外,將在本系列的下一篇文章中討論。)存活對(duì)象越多,垃圾回收造成的暫停也越長(zhǎng)。假如堆中的空間所剩無幾,而且碎片化又比較嚴(yán)重(這通常是由于應(yīng)用程序運(yùn)行的時(shí)間很長(zhǎng)了),那么對(duì)一塊存活對(duì)象多的區(qū)域進(jìn)行壓縮可能會(huì)耗費(fèi)數(shù)秒的時(shí)間。而如果因出現(xiàn)OOM而導(dǎo)致應(yīng)用程序無法運(yùn)行,因此而對(duì)整個(gè)堆進(jìn)行壓縮時(shí),所消耗的時(shí)間可達(dá)數(shù)十秒。
壓縮導(dǎo)致的暫停時(shí)間的長(zhǎng)短取決于需要移動(dòng)的存活對(duì)象所占用的內(nèi)存有多大以及有多少引用需要更新。當(dāng)堆比較大時(shí),從統(tǒng)計(jì)上講,存活對(duì)象和需要更新的引用都會(huì)很多。從已觀察到的數(shù)據(jù)看,每壓縮1到2GB存活數(shù)據(jù)的需要約1秒鐘。所以,對(duì)于4GB的堆來說,很可能會(huì)有至少25%的存活數(shù)據(jù),從而導(dǎo)致約1秒鐘的暫停。
壓縮與應(yīng)用程序內(nèi)存墻
應(yīng)用程序內(nèi)存墻涉及到在GC暫停時(shí)間對(duì)應(yīng)用程序的影響大到無法達(dá)到滿足預(yù)定需求之前所能設(shè)置的的堆的最大值。目前,大部分Java應(yīng)用程序在碰到內(nèi)存墻時(shí),每個(gè)JVM實(shí)例的堆大小介于4GB到20GB之間,具體數(shù)值依賴于具體的環(huán)境和應(yīng)用程序本身。這也是大多數(shù)企業(yè)及應(yīng)用程序會(huì)部署多個(gè)小堆JVM而不是部署少數(shù)大堆(50到60GB)JVM的原因之一。在這里,我們需要思考一下:現(xiàn)代企業(yè)中有多少Java應(yīng)用程序的設(shè)計(jì)與部署架構(gòu)受制于JVM中的壓縮?在這種情況下,我們接受多個(gè)小實(shí)例的部署方案,以增加管理維護(hù)時(shí)間為代價(jià),繞開為處理充滿碎片的堆而執(zhí)行stop-the-world式垃圾回收所帶來的問題??紤]到現(xiàn)今的硬件性能和企業(yè)級(jí)Java應(yīng)用程序中對(duì)內(nèi)存越來越多的訪問要求,這種方案是在非常奇怪。為什么僅僅只能給每個(gè)JVM實(shí)例設(shè)置這么小的堆?并發(fā)壓縮是一種可選方法,它可以降低內(nèi)存墻帶來的影響,這將是本系列中下一篇文章的主題。
從已觀察到的數(shù)據(jù)看,每壓縮1到2GB存活數(shù)據(jù)的需要約1秒鐘。所以,對(duì)于4GB的堆來說,很可能會(huì)有至少25%的存活數(shù)據(jù),從而導(dǎo)致約1秒鐘的暫停。
總結(jié):回顧
本文對(duì)垃圾回收做了總體介紹,目的是為了使你能了解垃圾回收的相關(guān)概念和基本知識(shí)。希望本文能激發(fā)你繼續(xù)深入閱讀相關(guān)文章的興趣。這里所介紹的大部分內(nèi)容,它們。在下一篇文章中,我將介紹一些較新穎的概念,并發(fā)壓縮,目前只有Azul公司的ZingJVM實(shí)現(xiàn)了這一技術(shù)。并發(fā)壓縮是對(duì)GC技術(shù)的綜合運(yùn)用,這些技術(shù)試圖重新構(gòu)建Java內(nèi)存模型,考慮當(dāng)今內(nèi)存容量與處理能力的不斷提升,這一點(diǎn)尤為重要。
現(xiàn)在,回顧一下本文中所介紹的關(guān)于垃圾回收的一些內(nèi)容:
1、不同的垃圾回收算法的方式是為滿足不同的應(yīng)用程序需求而設(shè)計(jì)。目前在商業(yè)環(huán)境中,應(yīng)用最為廣泛的是引用跟蹤垃圾回收器。
2、并行垃圾回收器會(huì)并行使用可用資源執(zhí)行垃圾回收任務(wù)。這種策略的常用實(shí)現(xiàn)是stop-the-world式垃圾回收器,使所有可用系統(tǒng)資源快速完成垃圾回收任務(wù)。因此,并行垃圾回收可以提供較高的吞吐量,但在垃圾回收的過程中,所有應(yīng)用程序線程都會(huì)被掛起,對(duì)延遲有較大影響。
3、并發(fā)垃圾回收器可以與應(yīng)用程序并發(fā)工作。使用并發(fā)垃圾回收器時(shí)要注意的是,確保在應(yīng)用程序發(fā)生OOM錯(cuò)誤之前完成垃圾回收。
4、分代式垃圾回收可以推遲碎片化的出現(xiàn),但并不能消除碎片化。它將堆分為兩塊空間,一塊用于存放“年輕對(duì)象”,另一塊用于存放從年輕代中存活下來的存活對(duì)象。對(duì)于那些使用了很多具有較短生命周期活不過幾次垃圾回收周期的Java應(yīng)用程序來說,使用分代式垃圾回收是非常合適的。
5、壓縮是可以完全解決碎片化的唯一方法。大多數(shù)垃圾回收器在壓縮的時(shí)候是都stop-the-world式的。應(yīng)用程序運(yùn)行的時(shí)間越長(zhǎng),對(duì)象間的引就用越復(fù)雜,對(duì)象大小的異質(zhì)性也越高。相應(yīng)的,完成壓縮所需要的時(shí)間也越長(zhǎng)。如果堆的大小較大的話也會(huì)對(duì)壓縮所產(chǎn)生的暫停有影響,因?yàn)檩^大的堆就會(huì)有更多的活動(dòng)數(shù)據(jù)和更多的引用需要處理。
6、調(diào)優(yōu)可以推遲OOM錯(cuò)誤的出現(xiàn),但過度調(diào)優(yōu)是無意義的。在通過試錯(cuò)方式初始調(diào)優(yōu)前,一定要明確生產(chǎn)環(huán)境負(fù)載的動(dòng)態(tài)性,以及應(yīng)用程序中的對(duì)象類型和對(duì)象間的引用情況。在動(dòng)態(tài)負(fù)載下,過于刻板的配置很容會(huì)失效。在設(shè)置非動(dòng)態(tài)調(diào)優(yōu)選項(xiàng)前一定要清楚這樣做后果。
JVM 性能優(yōu)化,Part 4: C4 垃圾回收
到目前為止,本系列的文章將stop-the-world式的垃圾回收視為影響Java應(yīng)用程序伸縮性的一大障礙,而伸縮性又是現(xiàn)代企業(yè)級(jí)Java應(yīng)用程序開發(fā)的基礎(chǔ)要求,因此這一問題亟待改善。幸運(yùn)的是,針對(duì)此問題,JVM中已經(jīng)出現(xiàn)了一些新特性,所使用的方式或是對(duì)stop-the-world式的垃圾回收做微調(diào),或是消除冗長(zhǎng)的暫停(這樣更好些)。在一些多核系統(tǒng)中,內(nèi)存不再是稀缺資源,因此,JVM的一些新特性就充分利用多核系統(tǒng)的潛在優(yōu)勢(shì)來增強(qiáng)Java應(yīng)用程序的伸縮性。
在本文中,我將著重介紹C4算法,該算法是AzulSystem公司中無暫停垃圾回收算法的新成果,目前只在ZingJVM上得到實(shí)現(xiàn)。此外,本文還將對(duì)Oracle公司的G1垃圾回收算法和IBM公司的BalancedGarbage Collection Policy算法做簡(jiǎn)單介紹。希望通過對(duì)這些垃圾回收算法的學(xué)習(xí)可以擴(kuò)展你對(duì)Java內(nèi)存管理模型和Java應(yīng)用程序伸縮性的理解,并激發(fā)你對(duì)這方面內(nèi)容的興趣以便更深入的學(xué)習(xí)相關(guān)知識(shí)。至少,你可以學(xué)習(xí)到在選擇JVM時(shí)有哪些需要關(guān)注的方面,以及在不同應(yīng)用程序場(chǎng)景下要注意的事項(xiàng)。
C4算法中的并發(fā)性
AzulSystem公司的C4(ConcurrentContinuously Compacting Collector,譯者注,Azul官網(wǎng)給出的名字是ContinuouslyConcurrent Compacting Collector)算法使用獨(dú)一無二而又非常有趣的方法來實(shí)現(xiàn)低延遲的分代式垃圾回收。相比于大多數(shù)分代式垃圾回收器,C4的不同之處在于它認(rèn)為垃圾回收并不是什么壞事(即應(yīng)用程序產(chǎn)生垃圾很正常),而壓縮是不可避免的。在設(shè)計(jì)之初,C4就是要犧牲各種動(dòng)態(tài)內(nèi)存管理的需求,以滿足需要長(zhǎng)時(shí)間運(yùn)行的服務(wù)器端應(yīng)用程序的需求。
C4算法將釋放內(nèi)存的過程從應(yīng)用程序行為和內(nèi)存分配速率中分離出來,并加以區(qū)分。這樣就實(shí)現(xiàn)了并發(fā)運(yùn)行,即應(yīng)用程序可以持續(xù)運(yùn)行,而不必等待垃圾回收的完成。其中的并發(fā)性是關(guān)鍵所在,正是由于并發(fā)性的存在才可以使暫停時(shí)間不受垃圾回收周期內(nèi)堆上活動(dòng)數(shù)據(jù)數(shù)量和需要跟蹤與更新的引用數(shù)量的影響,將暫停時(shí)間保持在較低的水平。大多數(shù)垃圾回收器在工作周期內(nèi)都包含了stop-the-world式的壓縮過程,這就是說應(yīng)用程序的暫停時(shí)間會(huì)隨活動(dòng)數(shù)據(jù)總量和堆中對(duì)象間引用的復(fù)雜度的上升而增加。使用C4算法的垃圾回收器可以并發(fā)的執(zhí)行壓縮操作,即壓縮與應(yīng)用程序線程同時(shí)工作,從而解決了影響JVM伸縮性的最大難題。
實(shí)際上,為了實(shí)現(xiàn)并發(fā)性,C4算法改變了現(xiàn)代Java企業(yè)級(jí)架構(gòu)和部署模型的基本假設(shè)。想象一下?lián)碛袛?shù)百GB內(nèi)存的JVM會(huì)是什么樣的:
1、部署Java應(yīng)用程序時(shí),對(duì)伸縮性的要求無需要多個(gè)JVM配合,在單一JVM實(shí)例中即可完成。這時(shí)的部署是什么樣呢?
2、有哪些以往因GC限制而無法在內(nèi)存存儲(chǔ)的對(duì)象?
3、那些分布式集群(如緩存服務(wù)器、區(qū)域服務(wù)器,或其他類型的服務(wù)器節(jié)點(diǎn))會(huì)有什么變化?當(dāng)可以增加JVM內(nèi)存而不會(huì)對(duì)應(yīng)用程序響應(yīng)時(shí)間造成負(fù)面影響時(shí),傳統(tǒng)的節(jié)點(diǎn)數(shù)量、節(jié)點(diǎn)死亡和緩存丟失的計(jì)算會(huì)有什么變化呢?
C4算法的3的階段
C4算法的一個(gè)基本假設(shè)是“垃圾回收不是壞事”和“壓縮不可避免”。C4算法的設(shè)計(jì)目標(biāo)是實(shí)現(xiàn)垃圾回收的并發(fā)與協(xié)作,剔除stop-the-world式的垃圾回收。C4垃圾回收算法包含一下3個(gè)階段:
1、標(biāo)記(Marking)— 找到活動(dòng)對(duì)象
2、重定位(Relocation)— 將存活對(duì)象移動(dòng)到一起,以便可以釋放較大的連續(xù)空間,這個(gè)階段也可稱為“壓縮(compaction)”
3、重映射(Remapping)— 更新被移動(dòng)的對(duì)象的引用。
C4算法中的標(biāo)記階段
在C4算法中,標(biāo)記階段(markingphase)使用了并發(fā)標(biāo)記(concurrentmarking)和引用跟蹤(reference-tracing)的方法來標(biāo)記活動(dòng)對(duì)象。在標(biāo)記階段中,GC線程會(huì)從線程棧和寄存器中的活動(dòng)對(duì)象開始,遍歷所有的引用,標(biāo)記找到的對(duì)象,這些GC線程會(huì)遍歷堆上所有的可達(dá)(reachable)對(duì)象。在這個(gè)階段,C4算法與其他并發(fā)標(biāo)記器的工作方式非常相似。
C4算法的標(biāo)記器與其他并發(fā)標(biāo)記器的區(qū)別也是始于并發(fā)標(biāo)記階段的。在并發(fā)標(biāo)記階段中,如果應(yīng)用程序線程修改未標(biāo)記的對(duì)象,那么該對(duì)象會(huì)被放到一個(gè)隊(duì)列中,以備遍歷。這就保證了該對(duì)象最終會(huì)被標(biāo)記,也因?yàn)槿绱耍珻4垃圾回收器或另一個(gè)應(yīng)用程序線程不會(huì)重復(fù)遍歷該對(duì)象。這樣就節(jié)省了標(biāo)記時(shí)間,消除了遞歸重標(biāo)記(recursive remark)的風(fēng)險(xiǎn)。(注意,長(zhǎng)時(shí)間的遞歸重標(biāo)記有可能會(huì)使應(yīng)用程序因無法獲得足夠的內(nèi)存而拋出OOM錯(cuò)誤,這也是大部分垃圾回收?qǐng)鼍爸械钠毡閱栴}。)如果C4算法的實(shí)現(xiàn)是基于臟卡表(dirty-cardtables)或其他對(duì)已經(jīng)遍歷過的堆區(qū)域的讀寫操作進(jìn)行記錄的方法,那垃圾回收線程就需要重新訪問這些區(qū)域做重標(biāo)記。在極端條件下,垃圾回收線程會(huì)陷入到永無止境的重標(biāo)記中—— 至少這個(gè)過程可能會(huì)長(zhǎng)到使應(yīng)用程序因無法分配到新的內(nèi)存而拋出OOM錯(cuò)誤。但C4算法是基于LVB(loadvalue barrier)實(shí)現(xiàn)的,LVB具有自愈能力,可以使應(yīng)用程序線程迅速查明某個(gè)引用是否已經(jīng)被標(biāo)記過了。如果這個(gè)引用沒有被標(biāo)記過,那么應(yīng)用程序會(huì)將其添加到GC隊(duì)列中。一旦該引用被放入到隊(duì)列中,它就不會(huì)再被重標(biāo)記了。應(yīng)用程序線程可以繼續(xù)做它自己的事。
臟對(duì)象(dirty object)和卡表(cardtable)
由于某些原因(例如在一個(gè)并發(fā)垃圾回收周期中,對(duì)象被修改了),垃圾回收器需要重新訪問某些對(duì)象,那么這些對(duì)象臟對(duì)象(dirtyobject)。這這些臟對(duì)象,或堆中臟區(qū)域的引用,通過會(huì)記錄在一個(gè)專門的數(shù)據(jù)結(jié)構(gòu)中,這就是卡表。
在C4算法中,并沒有重標(biāo)記(re-marking)這個(gè)階段,在第一次便利整個(gè)堆時(shí)就會(huì)將所有可達(dá)對(duì)象做標(biāo)記。因?yàn)檫\(yùn)行時(shí)不需要做重標(biāo)記,也就不會(huì)陷入無限循環(huán)的重標(biāo)記陷阱中,由此而降低了應(yīng)用程序因無法分配到內(nèi)存而拋出OOM錯(cuò)誤的風(fēng)險(xiǎn)。
C4算法中的重定位—— 應(yīng)用程序線程與GC的協(xié)作
C4算法中,*重定位階段(reloacationphase)*是由GC線程和應(yīng)用程序線程以協(xié)作的方式,并發(fā)完成的。這是因?yàn)镚C線程和應(yīng)用程序線程會(huì)同時(shí)工作,而且無論哪個(gè)線程先訪問將被移動(dòng)的對(duì)象,都會(huì)以協(xié)作的方式幫助完成該對(duì)象的移動(dòng)任務(wù)。因此,應(yīng)用程序線程可以繼續(xù)執(zhí)行自己的任務(wù),而不必等待整個(gè)垃圾回收周期的完成。
正如Figure 2所示,碎片內(nèi)存頁(yè)中的活動(dòng)對(duì)象會(huì)被重定位。在這個(gè)例子中,應(yīng)用程序線程先訪問了要被移動(dòng)的對(duì)象,那么應(yīng)用程序線程也會(huì)幫助完成移動(dòng)該對(duì)象的工作的初始部分,這樣,它就可以很快的繼續(xù)做自己的任務(wù)。虛擬地址(指相關(guān)引用)可以指向新的正確位置,內(nèi)存也可以快速回收。
如果是GC線程先訪問到了將被移動(dòng)的對(duì)象,那事情就簡(jiǎn)單多了,GC線程會(huì)執(zhí)行移動(dòng)操作的。如果在重映射階段(re-mappingphase,后續(xù)會(huì)提到)也訪問這個(gè)對(duì)象,那么它必須檢查該對(duì)象是否是要被移動(dòng)的。如果是,那么應(yīng)用程序線程會(huì)重新定位這個(gè)對(duì)象的位置,以便可以繼續(xù)完成自己任務(wù)。(對(duì)大對(duì)象的移動(dòng)是通過將該對(duì)象打碎再移動(dòng)完成的。)當(dāng)所有的活動(dòng)對(duì)象都從某個(gè)內(nèi)存也中移出后,剩下的就都是垃圾數(shù)據(jù)了,這個(gè)內(nèi)存頁(yè)也就可以被整體回收了,正如Figure2中所示。
關(guān)于清理
在C4算法中并沒有清理階段(sweepphase),因此也就不需要這個(gè)在大多數(shù)垃圾回收算法中比較常用的操作。在指向被移動(dòng)的對(duì)象的引用都更新為指向新的位置之前,from頁(yè)中的虛擬地址空間必須被完整保留。所以C4算法的實(shí)現(xiàn)保證了,在所有指向這個(gè)頁(yè)的引用處于穩(wěn)定狀態(tài)前,所有的虛擬地址空間都會(huì)被鎖定。然后,算法會(huì)立即回收物理內(nèi)存頁(yè)。
很明顯,無需執(zhí)行stop-the-world式的移動(dòng)對(duì)象是有很大好處的。由于在重定位階段,所有活動(dòng)對(duì)象都是并發(fā)移動(dòng)的,因此它們可以被更有效率的放入到相鄰的地址中,并且可以充分的壓縮。通過并發(fā)執(zhí)行重定位操作,堆被壓縮為連續(xù)空間,也無需掛起所有的應(yīng)用程序線程。這種方式消除了Java應(yīng)用程序訪問內(nèi)存的傳統(tǒng)限制。
經(jīng)過上述的過程后,如何更新引用呢?如何實(shí)現(xiàn)一個(gè)非stop-the-world式的操作呢?
C4算法中的重映射
在重定位階段,某些指向被移動(dòng)的對(duì)象的引用會(huì)自動(dòng)更新。但是,在重定位階段,那些指向了被移動(dòng)的對(duì)象的引用并沒有更新,仍然指向原處,所以它們需要在后續(xù)完成更新操作。C4算法中的重映射階段(re-mappingphase)負(fù)責(zé)完成對(duì)那些活動(dòng)對(duì)象已經(jīng)移出,但仍指向那些的引用進(jìn)行更新。當(dāng)然,重映射也是一個(gè)協(xié)作式的并發(fā)操作。
Figure 3中,在重定位階段,活動(dòng)對(duì)象已經(jīng)被移動(dòng)到了一個(gè)新的內(nèi)存頁(yè)中。在重定位之后,GC線程立即開始更新那些仍然指向之前的虛擬地址空間的引用,將它們指向那些被移動(dòng)的對(duì)象的新地址。垃圾回收器會(huì)一直執(zhí)行此項(xiàng)任務(wù),直到所有的引用都被更新,這樣原先虛擬內(nèi)存空間就可以被整體回收了。
但如果在GC完成對(duì)所有引用的更新之前,應(yīng)用程序線程想要訪問這些引用的話,會(huì)出現(xiàn)什么情況呢?在C4算法中,應(yīng)用程序線程可以很方便的幫助完成對(duì)引用進(jìn)行更新的工作。如果在重映射階段,應(yīng)用程序線程訪問了處于非穩(wěn)定狀態(tài)的引用,它會(huì)找到該引用的正確指向。如果應(yīng)用程序線程找到了正確的引用,它會(huì)更新該引用的指向。當(dāng)完成更新后,應(yīng)用程序線程會(huì)繼續(xù)自己的工作。
協(xié)作式的重映射保證了引用只會(huì)被更新一次,該引用下的子引用也都可以指向正確的新地址。此外,在大多數(shù)其他GC實(shí)現(xiàn)中,引用指向的地址不會(huì)被存儲(chǔ)在該對(duì)象被移動(dòng)之前的位置;相反,這些地址被存儲(chǔ)在一個(gè)堆外結(jié)構(gòu)(off-heapstructure)中。這樣,無需在對(duì)所有引用的更新完成之前,再花費(fèi)精力保持整個(gè)內(nèi)存頁(yè)完好無損,這個(gè)內(nèi)存頁(yè)可以被整體回收。
C4算法真的是無暫停的么?
在C4算法的重映射階段,正在跟蹤引用的線程僅會(huì)被中斷一次,而這次中斷僅僅會(huì)持續(xù)到對(duì)該引用的檢索和更新完成,在這次中斷后,線程會(huì)繼續(xù)運(yùn)行。相比于其他并發(fā)算法來說,這種實(shí)現(xiàn)會(huì)帶來巨大的性能提升,因?yàn)槠渌牟l(fā)立即回收算法需要等到每個(gè)線程都運(yùn)行到一個(gè)安全點(diǎn)(safepoint),然后同時(shí)掛起所有線程,再開始對(duì)所有的引用進(jìn)行更新,完成后再恢復(fù)所有線程的運(yùn)行。
對(duì)于并發(fā)壓縮垃圾回收器來說,由于垃圾回收所引起的暫停從來都不是問題。在C4算法的重定位階段中,也不會(huì)有再出現(xiàn)更糟的碎片化場(chǎng)景了。實(shí)現(xiàn)了C4算法的垃圾回收器也不會(huì)出現(xiàn)背靠背(back-to-back)式的垃圾回收周期,或者是因垃圾回收而使應(yīng)用程序暫停數(shù)秒甚至數(shù)分鐘。如果你曾經(jīng)體驗(yàn)過這種stop-the-world式的垃圾回收,那么很有可能是你給應(yīng)用程序設(shè)置的內(nèi)存太小了。你可以試用一下實(shí)現(xiàn)了C4算法的垃圾回收器,并為其分配足夠多的內(nèi)存,而完全不必?fù)?dān)心暫停時(shí)間過長(zhǎng)的問題。
評(píng)估C4算法和其他可選方案
像往常一樣,你需要針對(duì)應(yīng)用程序的需求選擇一款JVM和垃圾回收器。C4算法在設(shè)計(jì)之初就是無論堆中活動(dòng)數(shù)據(jù)有多少,只要應(yīng)用程序還有足夠的內(nèi)存可用,暫停時(shí)間都始終保持在較低的水平。正因如此,對(duì)于那些有大量?jī)?nèi)存可用,而對(duì)響應(yīng)時(shí)間比較敏感的應(yīng)用程來說,選擇實(shí)現(xiàn)了C4算法的垃圾回收器正是不二之選。
而對(duì)于那些要求快速啟動(dòng),內(nèi)存有限的客戶端應(yīng)用程序來說,C4就不是那么適用。而對(duì)于那些對(duì)吞吐量有較高要求的應(yīng)用程序來說,C4也并不適用。真正能夠發(fā)揮C4威力的是那些為了提升應(yīng)用程序工作負(fù)載而在每臺(tái)服務(wù)器上部署了4到16個(gè)JVM實(shí)例的場(chǎng)景。此外,如果你經(jīng)常要對(duì)垃圾回收器做調(diào)優(yōu)的話,那么不妨考慮一下使用C4算法。綜上所述,當(dāng)響應(yīng)時(shí)間比吞吐量占有更高的優(yōu)先級(jí)時(shí),C4是個(gè)不錯(cuò)的選擇。而對(duì)那些不能接受長(zhǎng)時(shí)間暫停的應(yīng)用程序來說,C4是個(gè)理想的選擇。
如果你正考慮在生產(chǎn)環(huán)境中使用C4,那么你可能還需要重新考慮一下如何部署應(yīng)用程序。例如,不必為每個(gè)服務(wù)器配置16個(gè)具有2GB堆的JVM實(shí)例,而是使用一個(gè)64GB的JVM實(shí)例(或者增加一個(gè)作為熱備份)。C4需要盡可能大的內(nèi)存來保證始終有一個(gè)空閑內(nèi)存頁(yè)來為新創(chuàng)建的對(duì)象分配內(nèi)存。(記住,內(nèi)存不再是昂貴的資源了!)如果你沒有64GB,128GB,或1TB(或更多)內(nèi)存可用,那么分布式的多JVM部署可能是一個(gè)更好的選擇。在這種場(chǎng)景中,你可以考慮使用OracleHotSpot JVM的G1垃圾回收器,或者IBM JVM的平衡垃圾回收策略(BalancedGarbage Collection Policy)。下面將對(duì)這兩種垃圾回收器做簡(jiǎn)單介紹。
Gargabe-First(G1)垃圾回收器:G1垃圾回收器是新近才出現(xiàn)的垃圾回收器,是OracleHotSpot JVM的一部分,在最近的JDK1.6版本中首次出現(xiàn)。在啟動(dòng)OracleJDK時(shí)附加命令行選項(xiàng)-XX:+UseG1GC,可以啟動(dòng)G1垃圾回收器。與C4類似,這款標(biāo)記-清理(mark-and-sweep)垃圾回收器也可作為對(duì)低延遲有要求的應(yīng)用程序的備選方案。G1算法將堆分為固定大小區(qū)域,垃圾回收會(huì)作用于其中的某些區(qū)域。在應(yīng)用程序線程運(yùn)行的同時(shí),啟用后臺(tái)線程,并發(fā)的完成標(biāo)記工作。這點(diǎn)與其他并發(fā)標(biāo)記算法相似。
G1增量方法可以使暫停時(shí)間更短,但更頻繁,而這對(duì)一些力求避免長(zhǎng)時(shí)間暫停的應(yīng)用程序來說已經(jīng)足夠了。另一方面,使用G1垃圾回收器需要針對(duì)應(yīng)用程序的實(shí)際需求做長(zhǎng)時(shí)間的調(diào)優(yōu),而其GC中斷又是stop-the-world式的。所以對(duì)那些對(duì)低延遲有很高要求的應(yīng)用程序來說,G1并不是一個(gè)好的選擇。進(jìn)一步說,從暫停時(shí)間總長(zhǎng)來看,G1長(zhǎng)于CMS(OracleJVM中廣為人知的并發(fā)垃圾回收器)。
G1使用拷貝算法完成部分垃圾回收任務(wù)。這樣,每次垃圾回收器后,都會(huì)產(chǎn)生完全可用的空閑空間。G1垃圾回收器定義了一些區(qū)域的集合作為年輕代,剩下的作為老年代。G1已經(jīng)吸引了足夠多的注意,引起了不小的轟動(dòng),但是它真正的挑戰(zhàn)在于如何應(yīng)對(duì)現(xiàn)實(shí)世界的需求。正確的調(diào)優(yōu)就是其中一個(gè)挑戰(zhàn)—— 回憶一下,對(duì)于動(dòng)態(tài)應(yīng)用程序負(fù)載來說,沒有永遠(yuǎn)“正確的調(diào)優(yōu)”。一個(gè)問題是如何處理與分區(qū)大小相近的大對(duì)象,因?yàn)槭S嗟目臻g會(huì)成為碎片而無法使用。還有一個(gè)性能問題始終困擾著低延遲垃圾回收器,那就是垃圾回收器必須管理額外的數(shù)據(jù)結(jié)構(gòu)。就我來說,使用G1的關(guān)鍵問題在于如何解決stop-the-world式垃圾回收器引起的暫停。Stop-the-world式的垃圾回收引起的暫停使任何垃圾回收器的能力都受制于堆大小和活動(dòng)數(shù)據(jù)數(shù)量的增長(zhǎng),對(duì)企業(yè)級(jí)Java應(yīng)用程序的伸縮性來說是一大困擾。
IBMJVM的平衡垃圾回收策略(Balanced Garbage CollectionPolicy):IBM JVM的平衡垃圾回收(BalancedGarbage Collection BGC)策略通過在啟動(dòng)IBM JDK時(shí)指定命令行選項(xiàng)-Xgcpolicy:balanced來啟用。乍一看,BGC很像G1,它也是將Java堆劃分成相同大小的空間,稱為區(qū)間(region),執(zhí)行垃圾回收時(shí)會(huì)對(duì)每個(gè)區(qū)間單獨(dú)回收。為了達(dá)到最佳性能,在選擇要執(zhí)行垃圾回收的區(qū)間時(shí)使用了一些啟發(fā)性算法。BGC中關(guān)于代的劃分也與G1相似。
IBM的平衡垃圾回收策略僅在64位平臺(tái)得到實(shí)現(xiàn),是一種NUMA架構(gòu)(Non-UniformMemory Architecture),設(shè)計(jì)之初是為了用于具有4GB以上堆的應(yīng)用程序。由于拷貝算法或壓縮算法的需要,BGC的部分垃圾回收工作是stop-the-world式的,并非完全并發(fā)完成。所以,歸根結(jié)底,BGC也會(huì)遇到與G1和其他沒有實(shí)現(xiàn)并發(fā)壓縮選法的垃圾回收器相似的問題。
結(jié)論:回顧
C4是基于引用跟蹤的、分代式的、并發(fā)的、協(xié)作式垃圾回收算法,目前只在AzulSystem公司的ZingJVM得到實(shí)現(xiàn)。C4算法的真正價(jià)值在于:
1、消除了重標(biāo)記可能引起的重標(biāo)記無限循環(huán),也就消除了在標(biāo)記階段出現(xiàn)OOM錯(cuò)誤的風(fēng)險(xiǎn)。
2、壓縮,以自動(dòng)、且不斷重定位的方式消除了固有限制:堆中活動(dòng)數(shù)據(jù)越多,壓縮所引起的暫停越長(zhǎng)。
3、垃圾回收不再是stop-the-world式的,大大降低垃圾回收對(duì)應(yīng)用程序響應(yīng)時(shí)間造成的影響。
4、沒有了清理階段,降低了在完成GC之前就因?yàn)榭臻e內(nèi)存不足而出現(xiàn)OOM錯(cuò)誤的風(fēng)險(xiǎn)。
5、內(nèi)存可以以頁(yè)為單位立即回收,使那些需要使用較多內(nèi)存的Java應(yīng)用程序有足夠的內(nèi)存可用。
并發(fā)壓縮是C4獨(dú)一無二的優(yōu)勢(shì)。使應(yīng)用程序線程GC線程協(xié)作運(yùn)行,保證了應(yīng)用程序不會(huì)因GC而被阻塞。C4將內(nèi)存分配和提供足夠連續(xù)空閑內(nèi)存的能力完全區(qū)分開。C4使你可以為JVM實(shí)例分配盡可能大的內(nèi)存,而無需為應(yīng)用程序暫停而煩惱。使用得當(dāng)?shù)脑挘@將是JVM技術(shù)的一項(xiàng)革新,它可以借助于當(dāng)今的多核、TB級(jí)內(nèi)存的硬件優(yōu)勢(shì),大大提升低延遲Java應(yīng)用程序的運(yùn)行速度。如果你不介意一遍又一遍的調(diào)優(yōu),以及頻繁的重啟的話,如果你的應(yīng)用程序適用于水平部署模型的話(即部署幾百個(gè)小堆JVM實(shí)例而不是幾個(gè)大堆JVM實(shí)例),G1也是個(gè)不錯(cuò)的選擇。對(duì)于動(dòng)態(tài)低延遲啟發(fā)性自適應(yīng)(dynamiclow-latency heuristic adaption)算法而言,BGC是一項(xiàng)革新,JVM研究者對(duì)此算法已經(jīng)研究了幾十年。該算法可以應(yīng)用于較大的堆。而動(dòng)態(tài)自調(diào)優(yōu)算法(dynamic self-tuning algorithm)的缺陷是,它無法跟上突然出現(xiàn)的負(fù)載高峰。那時(shí),你將不得不面對(duì)最糟糕的場(chǎng)景,并根據(jù)實(shí)際情況再分配相關(guān)資源。
最后,為你的應(yīng)用程序選擇最適合的JVM和垃圾回收器時(shí),最重要的考慮因素是應(yīng)用程序中吞吐量和暫停時(shí)間的優(yōu)先級(jí)次序。你想把時(shí)間和金錢花在哪?從純粹的技術(shù)角度說,基于我十年來對(duì)垃圾回收的經(jīng)驗(yàn),我一直在尋找更多關(guān)于并發(fā)壓縮的革新性技術(shù),或其他可以以較小代價(jià)完成移動(dòng)對(duì)象或重定位的方法。我想影響企業(yè)級(jí)Java應(yīng)用程序伸縮性的關(guān)鍵就在于并發(fā)性。
聯(lián)系客服