附錄E 關(guān)于垃圾收集的一些話
“很難相信Java居然能和C++一樣快,甚至還能更快一些。”
據(jù)我自己的實(shí)踐,這種說(shuō)法確實(shí)成立。然而,我也發(fā)現(xiàn)許多關(guān)于速度的懷疑都來(lái)自一些早期的實(shí)現(xiàn)方式。由于這些方式并非特別有效,所以沒(méi)有一個(gè)模型可供參考,不能解釋Java速度快的原因。
我之所以想到速度,部分原因是由于C++模型。C++將自己的主要精力放在編譯期間“靜態(tài)”發(fā)生的所有事情上,所以程序的運(yùn)行期版本非常短小和快速。C++也直接建立在C模型的基礎(chǔ)上(主要為了向后兼容),但有時(shí)僅僅由于它在C中能按特定的方式工作,所以也是C++中最方便的一種方法。最重要的一種情況是C和C++對(duì)內(nèi)存的管理方式,它是某些人覺(jué)得Java速度肯定慢的重要依據(jù):在Java中,所有對(duì)象都必須在內(nèi)存“堆”里創(chuàng)建。
而在C++中,對(duì)象是在堆棧中創(chuàng)建的。這樣可達(dá)到更快的速度,因?yàn)楫?dāng)我們進(jìn)入一個(gè)特定的作用域時(shí),堆棧指針會(huì)向下移動(dòng)一個(gè)單位,為那個(gè)作用域內(nèi)創(chuàng)建的、以堆棧為基礎(chǔ)的所有對(duì)象分配存儲(chǔ)空間。而當(dāng)我們離開作用域的時(shí)候(調(diào)用完畢所有局部構(gòu)建器后),堆棧指針會(huì)向上移動(dòng)一個(gè)單位。然而,在C++里創(chuàng)建“內(nèi)存堆”(Heap)對(duì)象通常會(huì)慢得多,因?yàn)樗⒃贑的內(nèi)存堆基礎(chǔ)上。這種內(nèi)存堆實(shí)際是一個(gè)大的內(nèi)存池,要求必須進(jìn)行再循環(huán)(再生)。在C++里調(diào)用delete以后,釋放的內(nèi)存會(huì)在堆里留下一個(gè)洞,所以再調(diào)用new的時(shí)候,存儲(chǔ)分配機(jī)制必須進(jìn)行某種形式的搜索,使對(duì)象的存儲(chǔ)與堆內(nèi)任何現(xiàn)成的洞相配,否則就會(huì)很快用光堆的存儲(chǔ)空間。之所以內(nèi)存堆的分配會(huì)在C++里對(duì)性能造成如此重大的性能影響,對(duì)可用內(nèi)存的搜索正是一個(gè)重要的原因。所以創(chuàng)建基于堆棧的對(duì)象要快得多。
同樣地,由于C++如此多的工作都在編譯期間進(jìn)行,所以必須考慮這方面的因素。但在Java的某些地方,事情的發(fā)生卻要顯得“動(dòng)態(tài)”得多,它會(huì)改變模型。創(chuàng)建對(duì)象的時(shí)候,垃圾收集器的使用對(duì)于提高對(duì)象創(chuàng)建的速度產(chǎn)生了顯著的影響。從表面上看,這種說(shuō)法似乎有些奇怪——存儲(chǔ)空間的釋放會(huì)對(duì)存儲(chǔ)空間的分配造成影響,但它正是JVM采取的重要手段之一,這意味著在Java中為堆對(duì)象分配存儲(chǔ)空間幾乎能達(dá)到與C++中在堆棧里創(chuàng)建存儲(chǔ)空間一樣快的速度。
可將C++的堆(以及更慢的Java堆)想象成一個(gè)庭院,每個(gè)對(duì)象都擁有自己的一塊地皮。在以后的某個(gè)時(shí)間,這種“不動(dòng)產(chǎn)”會(huì)被拋棄,而且必須再生。但在某些JVM里,Java堆的工作方式卻是頗有不同的。它更象一條傳送帶:每次分配了一個(gè)新對(duì)象后,都會(huì)朝前移動(dòng)。這意味著對(duì)象存儲(chǔ)空間的分配可以達(dá)到非常快的速度。“堆指針”簡(jiǎn)單地向前移至處女地,所以它與C++的堆棧分配方式幾乎是完全相同的(當(dāng)然,在數(shù)據(jù)記錄上會(huì)多花一些開銷,但要比搜索存儲(chǔ)空間快多了)。
現(xiàn)在,大家可能注意到了堆事實(shí)并非一條傳送帶。如按那種方式對(duì)待它,最終就要求進(jìn)行大量的頁(yè)交換(這對(duì)性能的發(fā)揮會(huì)產(chǎn)生巨大干擾),這樣終究會(huì)用光內(nèi)存,出現(xiàn)內(nèi)存分頁(yè)錯(cuò)誤。所以這兒必須采取一個(gè)技巧,那就是著名的“垃圾收集器”。它在收集“垃圾”的同時(shí),也負(fù)責(zé)壓縮堆里的所有對(duì)象,將“堆指針”移至盡可能靠近傳送帶開頭的地方,遠(yuǎn)離發(fā)生(內(nèi)存)分頁(yè)錯(cuò)誤的地點(diǎn)。垃圾收集器會(huì)重新安排所有東西,使其成為一個(gè)高速、無(wú)限自由的堆模型,同時(shí)游刃有余地分配存儲(chǔ)空間。
為真正掌握它的工作原理,我們首先需要理解不同垃圾收集器(GC)采取的工作方案。一種簡(jiǎn)單、但速度較慢的GC技術(shù)是引用計(jì)數(shù)。這意味著每個(gè)對(duì)象都包含了一個(gè)引用計(jì)數(shù)器。每當(dāng)一個(gè)句柄同一個(gè)對(duì)象連接起來(lái)時(shí),引用計(jì)數(shù)器就會(huì)增值。每當(dāng)一個(gè)句柄超出自己的作用域,或者設(shè)為null時(shí),引用計(jì)數(shù)就會(huì)減值。這樣一來(lái),只要程序處于運(yùn)行狀態(tài),就需要連續(xù)進(jìn)行引用計(jì)數(shù)管理——盡管這種管理本身的開銷比較少。垃圾收集器會(huì)在整個(gè)對(duì)象列表中移動(dòng)巡視,一旦它發(fā)現(xiàn)其中一個(gè)引用計(jì)數(shù)成為0,就釋放它占據(jù)的存儲(chǔ)空間。但這樣做也有一個(gè)缺點(diǎn):若對(duì)象相互之間進(jìn)行循環(huán)引用,那么即使引用計(jì)數(shù)不是0,仍有可能屬于應(yīng)收掉的“垃圾”。為了找出這種自引用的組,要求垃圾收集器進(jìn)行大量額外的工作。引用計(jì)數(shù)屬于垃圾收集的一種類型,但它看起來(lái)并不適合在所有JVM方案中采用。
在速度更快的方案里,垃圾收集并不建立在引用計(jì)數(shù)的基礎(chǔ)上。相反,它們基于這樣一個(gè)原理:所有非死鎖的對(duì)象最終都肯定能回溯至一個(gè)句柄,該句柄要么存在于堆棧中,要么存在于靜態(tài)存儲(chǔ)空間。這個(gè)回溯鏈可能經(jīng)歷了幾層對(duì)象。所以,如果從堆棧和靜態(tài)存儲(chǔ)區(qū)域開始,并經(jīng)歷所有句柄,就能找出所有活動(dòng)的對(duì)象。對(duì)于自己找到的每個(gè)句柄,都必須跟蹤到它指向的那個(gè)對(duì)象,然后跟隨那個(gè)對(duì)象中的所有句柄,“跟蹤追擊”到它們指向的對(duì)象……等等,直到遍歷了從堆棧或靜態(tài)存儲(chǔ)區(qū)域中的句柄發(fā)起的整個(gè)鏈接網(wǎng)路為止。中途移經(jīng)的每個(gè)對(duì)象都必須仍處于活動(dòng)狀態(tài)。注意對(duì)于那些特殊的自引用組,并不會(huì)出現(xiàn)前述的問(wèn)題。由于它們根本找不到,所以會(huì)自動(dòng)當(dāng)作垃圾處理。
在這里闡述的方法中,JVM采用一種“自適應(yīng)”的垃圾收集方案。對(duì)于它找到的那些活動(dòng)對(duì)象,具體采取的操作取決于當(dāng)前正在使用的是什么變體。其中一個(gè)變體是“停止和復(fù)制”。這意味著由于一些不久之后就會(huì)非常明顯的原因,程序首先會(huì)停止運(yùn)行(并非一種后臺(tái)收集方案)。隨后,已找到的每個(gè)活動(dòng)對(duì)象都會(huì)從一個(gè)內(nèi)存堆復(fù)制到另一個(gè),留下所有的垃圾。除此以外,隨著對(duì)象復(fù)制到新堆,它們會(huì)一個(gè)接一個(gè)地聚焦在一起。這樣可使新堆顯得更加緊湊(并使新的存儲(chǔ)區(qū)域可以簡(jiǎn)單地抽離末尾,就象前面講述的那樣)。
當(dāng)然,將一個(gè)對(duì)象從一處挪到另一處時(shí),指向那個(gè)對(duì)象的所有句柄(引用)都必須改變。對(duì)于那些通過(guò)跟蹤內(nèi)存堆的對(duì)象而獲得的句柄,以及那些靜態(tài)存儲(chǔ)區(qū)域,都可以立即改變。但在“遍歷”過(guò)程中,還有可能遇到指向這個(gè)對(duì)象的其他句柄。一旦發(fā)現(xiàn)這個(gè)問(wèn)題,就當(dāng)即進(jìn)行修正(可想象一個(gè)散列表將老地址映射成新地址)。
有兩方面的問(wèn)題使復(fù)制收集器顯得效率低下。第一個(gè)問(wèn)題是我們擁有兩個(gè)堆,所有內(nèi)存都在這兩個(gè)獨(dú)立的堆內(nèi)來(lái)回移動(dòng),要求付出的管理量是實(shí)際需要的兩倍。為解決這個(gè)問(wèn)題,有些JVM根據(jù)需要分配內(nèi)存堆,并將一個(gè)堆簡(jiǎn)單地復(fù)制到另一個(gè)。
第二個(gè)問(wèn)題是復(fù)制。隨著程序變得越來(lái)越“健壯”,它幾乎不產(chǎn)生或產(chǎn)生很少的垃圾。盡管如此,一個(gè)副本收集器仍會(huì)將所有內(nèi)存從一處復(fù)制到另一處,這顯得非常浪費(fèi)。為避免這個(gè)問(wèn)題,有些JVM能偵測(cè)是否沒(méi)有產(chǎn)生新的垃圾,并隨即改換另一種方案(這便是“自適應(yīng)”的緣由)。另一種方案叫作“標(biāo)記和清除”,Sun公司的JVM一直采用的都是這種方案。對(duì)于常規(guī)性的應(yīng)用,標(biāo)記和清除顯得非常慢,但一旦知道自己不產(chǎn)生垃圾,或者只產(chǎn)生很少的垃圾,它的速度就會(huì)非常快。
標(biāo)記和清除采用相同的邏輯:從堆棧和靜態(tài)存儲(chǔ)區(qū)域開始,并跟蹤所有句柄,尋找活動(dòng)對(duì)象。然而,每次發(fā)現(xiàn)一個(gè)活動(dòng)對(duì)象的時(shí)候,就會(huì)設(shè)置一個(gè)標(biāo)記,為那個(gè)對(duì)象作上“記號(hào)”。但此時(shí)尚不收集那個(gè)對(duì)象。只有在標(biāo)記過(guò)程結(jié)束,清除過(guò)程才正式開始。在清除過(guò)程中,死鎖的對(duì)象會(huì)被釋放然而,不會(huì)進(jìn)行任何形式的復(fù)制,所以假若收集器決定壓縮一個(gè)斷續(xù)的內(nèi)存堆,它通過(guò)移動(dòng)周圍的對(duì)象來(lái)實(shí)現(xiàn)。
“停止和復(fù)制”向我們表明這種類型的垃圾收集并不是在后臺(tái)進(jìn)行的;相反,一旦發(fā)生垃圾收集,程序就會(huì)停止運(yùn)行。在Sun公司的文檔庫(kù)中,可發(fā)現(xiàn)許多地方都將垃圾收集定義成一種低優(yōu)先級(jí)的后臺(tái)進(jìn)程,但它只是一種理論上的實(shí)驗(yàn),實(shí)際根本不能工作。在實(shí)際應(yīng)用中,Sun的垃圾收集器會(huì)在內(nèi)存減少時(shí)運(yùn)行。除此以外,“標(biāo)記和清除”也要求程序停止運(yùn)行。
正如早先指出的那樣,在這里介紹的JVM中,內(nèi)存是按大塊分配的。若分配一個(gè)大塊頭對(duì)象,它會(huì)獲得自己的內(nèi)存塊。嚴(yán)格的“停止和復(fù)制”要求在釋放舊堆之前,將每個(gè)活動(dòng)的對(duì)象從源堆復(fù)制到一個(gè)新堆,此時(shí)會(huì)涉及大量的內(nèi)存轉(zhuǎn)換工作。通過(guò)內(nèi)存塊,垃圾收集器通常可利用死塊復(fù)制對(duì)象,就象它進(jìn)行收集時(shí)那樣。每個(gè)塊都有一個(gè)生成計(jì)數(shù),用于跟蹤它是否依然“存活”。通常,只有自上次垃圾收集以來(lái)創(chuàng)建的塊才會(huì)得到壓縮;對(duì)于其他所有塊,如果已從其他某些地方進(jìn)行了引用,那么生成計(jì)數(shù)都會(huì)溢出。這是許多短期的、臨時(shí)的對(duì)象經(jīng)常遇到的情況。會(huì)周期性地進(jìn)行一次完整清除工作——大塊頭的對(duì)象仍未復(fù)制(只是讓它們的生成計(jì)數(shù)溢出),而那些包含了小對(duì)象的塊會(huì)進(jìn)行復(fù)制和壓縮。JVM會(huì)監(jiān)視垃圾收集器的效率,如果由于所有對(duì)象都屬于長(zhǎng)期對(duì)象,造成垃圾收集成為浪費(fèi)時(shí)間的一個(gè)過(guò)程,就會(huì)切換到“標(biāo)記和清除”方案。類似地,JVM會(huì)跟蹤監(jiān)視成功的“標(biāo)記與清除”工作,若內(nèi)存堆變得越來(lái)越“散亂”,就會(huì)換回“停止和復(fù)制”方案。“自定義”的說(shuō)法就是從這種行為來(lái)的,我們將其最后總結(jié)為:“根據(jù)情況,自動(dòng)轉(zhuǎn)換停止和復(fù)制/標(biāo)記和清除這兩種模式”。
JVM還采用了其他許多加速方案。其中一個(gè)特別重要的涉及裝載器以及JIT編譯器。若必須裝載一個(gè)類(通常是我們首次想創(chuàng)建那個(gè)類的一個(gè)對(duì)象時(shí)),會(huì)找到.class文件,并將那個(gè)類的字節(jié)碼送入內(nèi)存。此時(shí),一個(gè)方法是用JIT編譯所有代碼,但這樣做有兩方面的缺點(diǎn):它會(huì)花更多的時(shí)間,若與程序的運(yùn)行時(shí)間綜合考慮,編譯時(shí)間還有可能更長(zhǎng);而且它增大了執(zhí)行文件的長(zhǎng)度(字節(jié)碼比擴(kuò)展過(guò)的JIT代碼精簡(jiǎn)得多),這有可能造成內(nèi)存頁(yè)交換,從而顯著放慢一個(gè)程序的執(zhí)行速度。另一種替代辦法是:除非確有必要,否則不經(jīng)JIT編譯。這樣一來(lái),那些根本不會(huì)執(zhí)行的代碼就可能永遠(yuǎn)得不到JIT的編譯。
由于JVM對(duì)瀏覽器來(lái)說(shuō)是外置的,大家可能希望在使用瀏覽器的時(shí)候從一些JVM的速度提高中獲得好處。但非常不幸,JVM目前不能與不同的瀏覽器進(jìn)行溝通。為發(fā)揮一種特定JVM的潛力,要么使用內(nèi)建了那種JVM的瀏覽器,要么只有運(yùn)行獨(dú)立的Java應(yīng)用程序。

英文版主頁(yè) | 中文版主頁(yè) | 詳細(xì)目錄 |