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