程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 關於垃圾收集的一些話

關於垃圾收集的一些話

編輯:關於JAVA

“很難相信Java居然能和C++一樣快,甚至還能更快一些。”

據我自己的實踐,這種說法確實成立。然而,我也發現許多關於速度的懷疑都來自一些早期的實現方式。由於這些方式並非特別有效,所以沒有一個模型可供參考,不能解釋Java速度快的原因。

我之所以想到速度,部分原因是由於C++模型。C++將自己的主要精力放在編譯期間“靜態”發生的所有事情上,所以程序的運行期版本非常短小和快速。C++也直接建立在C模型的基礎上(主要為了向後兼容),但有時僅僅由於它在C中能按特定的方式工作,所以也是C++中最方便的一種方法。最重要的一種情況是C和C++對內存的管理方式,它是某些人覺得Java速度肯定慢的重要依據:在Java中,所有對象都必須在內存“堆”裡創建。

而在C++中,對象是在堆棧中創建的。這樣可達到更快的速度,因為當我們進入一個特定的作用域時,堆棧指針會向下移動一個單位,為那個作用域內創建的、以堆棧為基礎的所有對象分配存儲空間。而當我們離開作用域的時候(調用完畢所有局部構建器後),堆棧指針會向上移動一個單位。然而,在C++裡創建“內存堆”(Heap)對象通常會慢得多,因為它建立在C的內存堆基礎上。這種內存堆實際是一個大的內存池,要求必須進行再循環(再生)。在C++裡調用delete以後,釋放的內存會在堆裡留下一個洞,所以再調用new的時候,存儲分配機制必須進行某種形式的搜索,使對象的存儲與堆內任何現成的洞相配,否則就會很快用光堆的存儲空間。之所以內存堆的分配會在C++裡對性能造成如此重大的性能影響,對可用內存的搜索正是一個重要的原因。所以創建基於堆棧的對象要快得多。

同樣地,由於C++如此多的工作都在編譯期間進行,所以必須考慮這方面的因素。但在Java的某些地方,事情的發生卻要顯得“動態”得多,它會改變模型。創建對象的時候,垃圾收集器的使用對於提高對象創建的速度產生了顯著的影響。從表面上看,這種說法似乎有些奇怪——存儲空間的釋放會對存儲空間的分配造成影響,但它正是JVM采取的重要手段之一,這意味著在Java中為堆對象分配存儲空間幾乎能達到與C++中在堆棧裡創建存儲空間一樣快的速度。

可將C++的堆(以及更慢的Java堆)想象成一個庭院,每個對象都擁有自己的一塊地皮。在以後的某個時間,這種“不動產”會被拋棄,而且必須再生。但在某些JVM裡,Java堆的工作方式卻是頗有不同的。它更象一條傳送帶:每次分配了一個新對象後,都會朝前移動。這意味著對象存儲空間的分配可以達到非常快的速度。“堆指針”簡單地向前移至處女地,所以它與C++的堆棧分配方式幾乎是完全相同的(當然,在數據記錄上會多花一些開銷,但要比搜索存儲空間快多了)。

現在,大家可能注意到了堆事實並非一條傳送帶。如按那種方式對待它,最終就要求進行大量的頁交換(這對性能的發揮會產生巨大干擾),這樣終究會用光內存,出現內存分頁錯誤。所以這兒必須采取一個技巧,那就是著名的“垃圾收集器”。它在收集“垃圾”的同時,也負責壓縮堆裡的所有對象,將“堆指針”移至盡可能靠近傳送帶開頭的地方,遠離發生(內存)分頁錯誤的地點。垃圾收集器會重新安排所有東西,使其成為一個高速、無限自由的堆模型,同時游刃有余地分配存儲空間。

為真正掌握它的工作原理,我們首先需要理解不同垃圾收集器(GC)采取的工作方案。一種簡單、但速度較慢的GC技術是引用計數。這意味著每個對象都包含了一個引用計數器。每當一個句柄同一個對象連接起來時,引用計數器就會增值。每當一個句柄超出自己的作用域,或者設為null時,引用計數就會減值。這樣一來,只要程序處於運行狀態,就需要連續進行引用計數管理——盡管這種管理本身的開銷比較少。垃圾收集器會在整個對象列表中移動巡視,一旦它發現其中一個引用計數成為0,就釋放它占據的存儲空間。但這樣做也有一個缺點:若對象相互之間進行循環引用,那麼即使引用計數不是0,仍有可能屬於應收掉的“垃圾”。為了找出這種自引用的組,要求垃圾收集器進行大量額外的工作。引用計數屬於垃圾收集的一種類型,但它看起來並不適合在所有JVM方案中采用。

在速度更快的方案裡,垃圾收集並不建立在引用計數的基礎上。相反,它們基於這樣一個原理:所有非死鎖的對象最終都肯定能回溯至一個句柄,該句柄要麼存在於堆棧中,要麼存在於靜態存儲空間。這個回溯鏈可能經歷了幾層對象。所以,如果從堆棧和靜態存儲區域開始,並經歷所有句柄,就能找出所有活動的對象。對於自己找到的每個句柄,都必須跟蹤到它指向的那個對象,然後跟隨那個對象中的所有句柄,“跟蹤追擊”到它們指向的對象……等等,直到遍歷了從堆棧或靜態存儲區域中的句柄發起的整個鏈接網路為止。中途移經的每個對象都必須仍處於活動狀態。注意對於那些特殊的自引用組,並不會出現前述的問題。由於它們根本找不到,所以會自動當作垃圾處理。

在這裡闡述的方法中,JVM采用一種“自適應”的垃圾收集方案。對於它找到的那些活動對象,具體采取的操作取決於當前正在使用的是什麼變體。其中一個變體是“停止和復制”。這意味著由於一些不久之後就會非常明顯的原因,程序首先會停止運行(並非一種後台收集方案)。隨後,已找到的每個活動對象都會從一個內存堆復制到另一個,留下所有的垃圾。除此以外,隨著對象復制到新堆,它們會一個接一個地聚焦在一起。這樣可使新堆顯得更加緊湊(並使新的存儲區域可以簡單地抽離末尾,就象前面講述的那樣)。

當然,將一個對象從一處挪到另一處時,指向那個對象的所有句柄(引用)都必須改變。對於那些通過跟蹤內存堆的對象而獲得的句柄,以及那些靜態存儲區域,都可以立即改變。但在“遍歷”過程中,還有可能遇到指向這個對象的其他句柄。一旦發現這個問題,就當即進行修正(可想象一個散列表將老地址映射成新地址)。

有兩方面的問題使復制收集器顯得效率低下。第一個問題是我們擁有兩個堆,所有內存都在這兩個獨立的堆內來回移動,要求付出的管理量是實際需要的兩倍。為解決這個問題,有些JVM根據需要分配內存堆,並將一個堆簡單地復制到另一個。

第二個問題是復制。隨著程序變得越來越“健壯”,它幾乎不產生或產生很少的垃圾。盡管如此,一個副本收集器仍會將所有內存從一處復制到另一處,這顯得非常浪費。為避免這個問題,有些JVM能偵測是否沒有產生新的垃圾,並隨即改換另一種方案(這便是“自適應”的緣由)。另一種方案叫作“標記和清除”,Sun公司的JVM一直采用的都是這種方案。對於常規性的應用,標記和清除顯得非常慢,但一旦知道自己不產生垃圾,或者只產生很少的垃圾,它的速度就會非常快。

標記和清除采用相同的邏輯:從堆棧和靜態存儲區域開始,並跟蹤所有句柄,尋找活動對象。然而,每次發現一個活動對象的時候,就會設置一個標記,為那個對象作上“記號”。但此時尚不收集那個對象。只有在標記過程結束,清除過程才正式開始。在清除過程中,死鎖的對象會被釋放然而,不會進行任何形式的復制,所以假若收集器決定壓縮一個斷續的內存堆,它通過移動周圍的對象來實現。

“停止和復制”向我們表明這種類型的垃圾收集並不是在後台進行的;相反,一旦發生垃圾收集,程序就會停止運行。在Sun公司的文檔庫中,可發現許多地方都將垃圾收集定義成一種低優先級的後台進程,但它只是一種理論上的實驗,實際根本不能工作。在實際應用中,Sun的垃圾收集器會在內存減少時運行。除此以外,“標記和清除”也要求程序停止運行。

正如早先指出的那樣,在這裡介紹的JVM中,內存是按大塊分配的。若分配一個大塊頭對象,它會獲得自己的內存塊。嚴格的“停止和復制”要求在釋放舊堆之前,將每個活動的對象從源堆復制到一個新堆,此時會涉及大量的內存轉換工作。通過內存塊,垃圾收集器通常可利用死塊復制對象,就象它進行收集時那樣。每個塊都有一個生成計數,用於跟蹤它是否依然“存活”。通常,只有自上次垃圾收集以來創建的塊才會得到壓縮;對於其他所有塊,如果已從其他某些地方進行了引用,那麼生成計數都會溢出。這是許多短期的、臨時的對象經常遇到的情況。會周期性地進行一次完整清除工作——大塊頭的對象仍未復制(只是讓它們的生成計數溢出),而那些包含了小對象的塊會進行復制和壓縮。JVM會監視垃圾收集器的效率,如果由於所有對象都屬於長期對象,造成垃圾收集成為浪費時間的一個過程,就會切換到“標記和清除”方案。類似地,JVM會跟蹤監視成功的“標記與清除”工作,若內存堆變得越來越“散亂”,就會換回“停止和復制”方案。“自定義”的說法就是從這種行為來的,我們將其最後總結為:“根據情況,自動轉換停止和復制/標記和清除這兩種模式”。

JVM還采用了其他許多加速方案。其中一個特別重要的涉及裝載器以及JIT編譯器。若必須裝載一個類(通常是我們首次想創建那個類的一個對象時),會找到.class文件,並將那個類的字節碼送入內存。此時,一個方法是用JIT編譯所有代碼,但這樣做有兩方面的缺點:它會花更多的時間,若與程序的運行時間綜合考慮,編譯時間還有可能更長;而且它增大了執行文件的長度(字節碼比擴展過的JIT代碼精簡得多),這有可能造成內存頁交換,從而顯著放慢一個程序的執行速度。另一種替代辦法是:除非確有必要,否則不經JIT編譯。這樣一來,那些根本不會執行的代碼就可能永遠得不到JIT的編譯。

由於JVM對浏覽器來說是外置的,大家可能希望在使用浏覽器的時候從一些JVM的速度提高中獲得好處。但非常不幸,JVM目前不能與不同的浏覽器進行溝通。為發揮一種特定JVM的潛力,要麼使用內建了那種JVM的浏覽器,要麼只有運行獨立的Java應用程序。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved