關(guān)注性能: 調(diào)優(yōu)垃圾收集將 100 MB 的垃圾打包成 50 MB 的包 ![]() |
![]() |
![]() |
級(jí)別: 初級(jí) Jack Shirazi, 董事, JavaPerformanceTuning.com 2004 年 7 月 30 日 如果您是當(dāng)前寫網(wǎng)志(blogging)狂熱者中的一員,則可能聽說過 Blog-City,這是由蘇格蘭的一家小公司 Blog-City Ltd. 擁有和運(yùn)營(yíng)的網(wǎng)志站點(diǎn)。當(dāng)一些意料之外的性能問題突然出現(xiàn)時(shí),Java 性能專家 Jack Shirazi 和 Kirk Pepperdine 被邀請(qǐng)幫助進(jìn)行 Blog-City 的技術(shù)調(diào)整。他們的檢測(cè)工作因?yàn)槭苡布s束和整個(gè)項(xiàng)目所使用的通信通道(IRC、ftp 和 偶爾的電子郵件)的限制而變得復(fù)雜。 隨著網(wǎng)志作為公共日記的流行,網(wǎng)志主機(jī)迅速地增長(zhǎng)。所以對(duì)于 Blog-City 的人來(lái)說,非常清楚他們的站點(diǎn)需要發(fā)展和提高。為了滿足其增長(zhǎng)的需要,該公司最近剛剛推出了 Blog-City version 2.0。正像經(jīng)常出現(xiàn)的情況那樣,當(dāng)新的應(yīng)用程序轉(zhuǎn)入運(yùn)行階段時(shí),由于各種原因,其性能無(wú)法完全滿足期望的要求,突然出現(xiàn)隨機(jī)的長(zhǎng)時(shí)間應(yīng)用程序被掛起的現(xiàn)象還不是最壞的情況。 在其核心,Blog-City 依靠 Blue Dragon Servlet 引擎(CFML 引擎)和數(shù)據(jù)庫(kù)。令人驚訝的是,所有這些軟件都宿主在運(yùn)行 Red Hat Linux 的相當(dāng)老的 P3 機(jī)器上。這臺(tái)機(jī)器具有單個(gè)硬盤和 512MB 內(nèi)存,這對(duì)于過去的負(fù)載來(lái)說是足夠強(qiáng)大的,但它正在承受不斷增長(zhǎng)的負(fù)載。Blog-City 的運(yùn)作方式很成功,但其資源限制卻成了其成功路上的絆腳石。盡管如此,這就是未來(lái)還要繼續(xù)使用一段時(shí)間的所有硬件。 整個(gè)過程的第一步是確定突然出現(xiàn)應(yīng)用程序減慢的原因。首先我們懷疑的對(duì)象是垃圾收集。正如我們?cè)?本專欄的上月文章 中所論述的那樣,確定垃圾收集和內(nèi)存利用問題是否對(duì)應(yīng)用程序產(chǎn)生負(fù)面影響的最容易的方式是,設(shè)置 從對(duì)日志文件的最初分析中看,在這一應(yīng)用程序中垃圾收集的瓶頸是顯而易見的。種種跡象包括垃圾收集的頻率、持續(xù)時(shí)間和總體效率都已表明這一點(diǎn)。高于普通垃圾收集頻率的常見原因是,堆的大小剛好足以適應(yīng)所有當(dāng)前正在使用的運(yùn)行對(duì)象,無(wú)法適應(yīng)新的正被創(chuàng)建的對(duì)象。雖然應(yīng)用程序消耗大量堆可能有許多原因,但主要原因可能是沒有足夠內(nèi)存而導(dǎo)致垃圾收集器運(yùn)行,因?yàn)樗O(shè)法滿足當(dāng)前需要。換句話說,應(yīng)用程序試圖分配新對(duì)象,但失敗了,如果失敗的話,將觸發(fā)垃圾收集程序。如果垃圾收集失敗而無(wú)法恢復(fù)足夠內(nèi)存,它將迫使另一個(gè)花費(fèi)更大的垃圾收集程序發(fā)生。即使 GC 恢復(fù)了足夠的空間來(lái)滿足瞬間需求,可以肯定的是,在應(yīng)用程序程序另一次分配失敗,觸發(fā)另一個(gè) GC 之前,時(shí)間不會(huì)很長(zhǎng)。因此,應(yīng)該關(guān)注重復(fù)掃描空閑堆空間的無(wú)效任務(wù),而不是服務(wù)于應(yīng)用程序的 JVM。 應(yīng)用程序逐步消耗所有可用的堆空間可能有許多原因,但如果有更多內(nèi)存的話,臨時(shí)解決方案就是配置更大的堆。假設(shè)應(yīng)用程序沒有內(nèi)存泄漏(或者也就是我們常說的“無(wú)意識(shí)地保留對(duì)象”),它將找到一個(gè)“自然”級(jí)別的堆消耗,在這個(gè)級(jí)別中,GC 將能夠很適應(yīng)地得到維持(除非對(duì)象創(chuàng)建的速度過快,以至 GC 總是處于賽跑狀態(tài))。在這種情況下,以及無(wú)意識(shí)地保留對(duì)象的情況下,我們需要對(duì)應(yīng)用程序做一些變動(dòng),以便獲得某些改進(jìn)。
遺憾的是,我們必須面對(duì)嚴(yán)酷的現(xiàn)實(shí)因素——正在運(yùn)行的機(jī)器只有 512 MB 內(nèi)存。更糟的是,我們必須與數(shù)據(jù)庫(kù)和其他運(yùn)行在機(jī)器中的進(jìn)程共享該空間。要完整理解這一點(diǎn)為什么至關(guān)重要,首先您必須明確理解垃圾收集的基本知識(shí),以及它如何與底層操作系統(tǒng)進(jìn)行交互。 操作系統(tǒng)已經(jīng)使用虛擬內(nèi)存許多年了。正如您所知道的,虛擬內(nèi)存使操作系統(tǒng)的內(nèi)存看起來(lái)比實(shí)際的內(nèi)存要多,這允許計(jì)算機(jī)運(yùn)行那些所需內(nèi)存比可用物理內(nèi)存更大的程序,不使用內(nèi)存的應(yīng)用程序部分將保存在磁盤上。為了進(jìn)一步簡(jiǎn)化,操作系統(tǒng)同時(shí)按頁(yè)管理內(nèi)存。頁(yè)通常包含 512 字節(jié)到 8 KB,所有頁(yè)的組合就組成了一個(gè)虛擬地址空間。操作系統(tǒng)維持一個(gè)頁(yè)表,用于告訴操作系統(tǒng)如何映射虛擬地址到物理地址。當(dāng)應(yīng)用程序要求某個(gè)內(nèi)存位置的內(nèi)容時(shí),操作系統(tǒng)(或硬件)將識(shí)別包含虛擬地址的頁(yè)面。然后確定該頁(yè)面是否在內(nèi)存中,如果不在,將會(huì)報(bào)告 頁(yè)面錯(cuò)誤。但是有許多種方式來(lái)處理頁(yè)面錯(cuò)誤,最終的結(jié)果是,頁(yè)面必須從磁盤載入到內(nèi)存中。這樣應(yīng)用程序就可以訪問到有效虛擬地址的內(nèi)容。 如果相關(guān)對(duì)象總是在內(nèi)存的同一頁(yè)面上聚合,那么 GC 的連續(xù)工作很可能出現(xiàn)困難。但是現(xiàn)實(shí)世界中,相關(guān)對(duì)象很少(如果有的話)出現(xiàn)聚合現(xiàn)象。實(shí)際結(jié)果是,依靠虛擬內(nèi)存的系統(tǒng)將導(dǎo)致操作系統(tǒng)將頁(yè)從內(nèi)存中換入和換出,因?yàn)樗鼧?biāo)記然后廢棄堆空間,而當(dāng)聚合現(xiàn)象發(fā)生時(shí),GC 將很多時(shí)間花在等待頁(yè)面從磁盤換入而不是實(shí)際恢復(fù)內(nèi)存上。因此,應(yīng)用程序正在等待 GC,而 GC 正在等待磁盤,其間未完成任何真正的工作。由于本系統(tǒng)只有一個(gè)磁盤,并且它還需要支持?jǐn)?shù)據(jù)庫(kù),因此我們?cè)诮鉀Q問題時(shí)處于兩難境地。一方面,我們需要增加內(nèi)存數(shù)量,這樣我們可以減少 GC 的頻率,但另一方面,我們還需要確保數(shù)據(jù)庫(kù)的完好運(yùn)行,而數(shù)據(jù)庫(kù)也是內(nèi)存的消耗大戶。因此,我們需要了解應(yīng)用程序所需的最小內(nèi)存數(shù)量。 正如我們?cè)谏显驴吹降模谌唛L(zhǎng)的 GC 日志中這一信息可以很容易得到,無(wú)需為這一信息而掃描整個(gè)日志,我們使用免費(fèi)的 JTune 工具(請(qǐng)參閱 參考資料)來(lái)解釋冗長(zhǎng)的 GC 日志。圖 1 顯示了經(jīng)過垃圾收集之后的內(nèi)存利用情況,其中我們將 圖 1. 垃圾收集之后的內(nèi)存利用情況 ![]()
在圖 1 中,藍(lán)色部分表示部分 GC。橙色區(qū)域表示完整的 GC,而粉色矩形表示兩個(gè)完整 GC 在它們之間少于一毫秒之內(nèi)已經(jīng)發(fā)生的堆利用情況。從結(jié)果中我們看到,平均每 0.257 秒有 12,823 次清除。總共有 345 次完整的垃圾收集和 44 次緊挨著的垃圾收集。完整垃圾收集的平均持續(xù)時(shí)間是 7.303 秒,結(jié)果表明有 9.36% 的運(yùn)行時(shí)間花費(fèi)在垃圾收集程序上。雖然這個(gè)值偏高,它仍然保持在 10% 的正常水平之內(nèi)。因此,在本例中,GC 是系統(tǒng)的繁重負(fù)擔(dān)但還沒有達(dá)到嚴(yán)重的地步。真正的問題是存在內(nèi)存泄漏,這一點(diǎn)可以從總體上堆利用率不斷增長(zhǎng)的趨勢(shì)看出來(lái)。 即使內(nèi)存泄漏消耗了 50 MB 內(nèi)存,它也應(yīng)該是經(jīng)過很長(zhǎng)一段時(shí)間后才發(fā)生,這使得內(nèi)存泄漏在較短的測(cè)試中很少會(huì)引人注意。內(nèi)存泄漏的實(shí)際結(jié)果是,它把 JVM 的內(nèi)存消耗推動(dòng)到某個(gè)點(diǎn),在該點(diǎn)它強(qiáng)迫 JVM (從而強(qiáng)迫操作系統(tǒng))消耗內(nèi)存,它強(qiáng)迫啟動(dòng)分頁(yè)。圖 2 就證明了這一點(diǎn)。注意正好在 55,000 秒標(biāo)記之后,每一 GC 周期的持續(xù)時(shí)間中內(nèi)存消耗突然地持續(xù)增加。 圖 2. GC 持續(xù)時(shí)間 ![]() 如您所想,由于垃圾收集的阻塞將導(dǎo)致系統(tǒng)只有更少的時(shí)間來(lái)分配給用戶線程,因此用戶響應(yīng)開始增加。在日志的過去 10,000 秒中,我們看到每次完全收集(總共 15 次)花費(fèi)時(shí)間超過了 30 秒,平均持續(xù)時(shí)間大約 70 秒 —— 這導(dǎo)致超過 10% 的處理時(shí)間分配給完全 GC。部分收集(這里剛好超過了 1000 次)無(wú)法正常工作,平均每次請(qǐng)求耗時(shí) 1.24 秒,遠(yuǎn)高于以前 11,800 次清除中的平均 0.25 秒。 本文不涉及太深的細(xì)節(jié)(請(qǐng)參閱 參考資料,獲取分代 GC 的詳細(xì)描述),分代堆空間產(chǎn)生了“年輕”和“年老”對(duì)象,它們位于分開的堆空間中。在本配置中,年輕和年老分代空間可以通過不同的 GC 算法和策略來(lái)維持,以提高 GC 的整體性能。 一種這樣的策略是,進(jìn)一步將年輕分代劃分為創(chuàng)建空間,稱為 Eden,以及殘存(survivor)空間,用于幸存一個(gè)或者多個(gè)收集的年輕對(duì)象。如果在 Eden 中有足夠的內(nèi)存來(lái)適應(yīng)新對(duì)象創(chuàng)建的話,這一般能工作正常。如果不是這種情況,那么對(duì)象可以在年老對(duì)象空間中創(chuàng)建。同樣,如果殘存空間足夠的話,那么對(duì)象將移入年老分代空間。我們將使用這些事實(shí)來(lái)幫助調(diào)優(yōu)遇到的問題。
Blog-City 所碰到的難題是在某一隨機(jī)點(diǎn)出現(xiàn)長(zhǎng)的暫停時(shí)間。一旦應(yīng)用程序啟動(dòng)出現(xiàn)問題,不重新啟動(dòng)機(jī)器的話,就無(wú)法返回跟蹤。由于長(zhǎng)時(shí)間暫停的現(xiàn)象直接與長(zhǎng)的 GC 相關(guān),我們考慮如果將對(duì)象保持在年輕分代來(lái)減少完全 GC 的次數(shù)。由于完全 GC 的代價(jià)如此之大,在年輕分代收集更多對(duì)象能夠得到更短的暫停時(shí)間。要完成這一任務(wù),我們調(diào)整了一些垃圾收集參數(shù),包括 殘存比率(survivor ratio)和 期限閾值(tenuring threshold)。 殘存比率用于設(shè)置與年輕分代空間總體大小相關(guān)的殘存空間的大小。如果殘存比率設(shè)置為 8(Intel 的默認(rèn)值),那么每一殘存空間將是 Eden 空間的 1/8 大小。另一種考察它的方式是,年輕分代將該 Eden 空間劃分為 10 個(gè)相同大小的值,該 Eden 將分配其中的 8 個(gè),每一個(gè)殘存空間的大小為 1。 我們的假設(shè)是,通過減少殘存比率,我們可以減少由于殘存空間中空間的缺乏,對(duì)象過早地被提升為年老分代的幾率。另一種方法是增加期限閾值,這樣的話,對(duì)象在提升之前將需要保留更多的 GC 事件。本著這個(gè)想法,Blog-City 將設(shè)置更改為 由于這次技術(shù)調(diào)優(yōu)的目標(biāo)之一是減少暫停時(shí)間,我們決定拋棄默認(rèn)的單線程、標(biāo)記清掃的垃圾收集程序。我們選擇通過標(biāo)志 圖 3 和圖 4 的輸出展示了使用標(biāo)志 圖 3. 新配置下的內(nèi)存使用情況 ![]() 結(jié)果圖表顯示了明顯的不同。雖然仍然有一個(gè)內(nèi)存漏洞。內(nèi)存消耗的總數(shù)相比前一個(gè)圖已經(jīng)是大大降低了。GC 持續(xù)時(shí)間的快速比較揭示了年輕分代和年老分代的總體 GC 持續(xù)時(shí)間的明顯減少。 圖 4. 新配置下的 GC 持續(xù)時(shí)間 ![]() 由于應(yīng)用程序是依靠?jī)?nèi)存的,跟蹤內(nèi)存泄漏并消除它們已經(jīng)變得越來(lái)越重要。在本例中,用于支持緩存策略的組件決定了主要漏洞的來(lái)源。從最后的內(nèi)存分析情況來(lái)看(圖 3),雖然消除了主要內(nèi)存泄漏,我們可以看到仍有另一個(gè)“低級(jí)別的”的漏洞,但這個(gè)漏洞比較小,因此它在下一版本發(fā)布之前可以忽略。
本文提出了許多挑戰(zhàn)。首先,我們正在調(diào)優(yōu)一個(gè)現(xiàn)實(shí)中的應(yīng)用程序,這意味著更改會(huì)受到很多限制。第二個(gè)挑戰(zhàn)是,這項(xiàng)任務(wù)是使用 IRC 聊天室遠(yuǎn)程操控的。聊天室不提供任何級(jí)別或質(zhì)量的相互通信,而通信在這種類型的任務(wù)中往往是必需的。在本例中,團(tuán)隊(duì)已經(jīng)習(xí)慣了聊天室的真實(shí)性,并能通過這種真實(shí)性毫無(wú)任何阻礙地工作著。 最后也是最困難的挑戰(zhàn)是我們受硬件的限制。由于多種原因,我們不可能為系統(tǒng)添加新硬件。其中最大的問題是系統(tǒng)中物理內(nèi)存的數(shù)量,而 JVM 和 MySQL 需要大量的內(nèi)存。但是,通過系統(tǒng)地逐一應(yīng)用許多更改,并度量它們對(duì)系統(tǒng)產(chǎn)生的影響,我們可以逐步地改進(jìn)總體系統(tǒng)性能。
|