jvm系列程序員
就是須要回收的對象。
做爲編寫程序的人,是能夠作出「這個對象已經再也不須要了」這樣的判斷,但計算機是作不到的。所以,若是程序(經過某個變量等等)可能會直接或間接地引用一個對象,那麼這個對象就被視爲「存活」;與之相反,已經引用不到的對象被視爲「死亡」。將這些「死亡」對象找出來,而後做爲垃圾進行回收,這就是GC的本質。
就是判斷對象是否可被引用的起始點。
至於哪裏纔是根,不一樣的語言和編譯器都有不一樣的規定,但基本上是將變量和運行棧空間做爲根。好了,用上面這兩個術語,咱們來說一講主要的GC算法。
標記清除(Mark and Sweep)是最先開發出的GC算法(1960年)。它的原理很是簡單,首先從根開始將可能被引用的對象用遞歸的方式進行標記,而後將沒有標記到的對象做爲垃圾進行回收。
圖1顯示了標記清除算法的大體原理。圖1中的(1)部分顯示了隨着程序的運行而分配出一些對象的狀態,一個對象能夠對其餘的對象進行引用。圖中(2)部分中,GC開始執行,從根開始對可能被引用的對象打上「標記」。大多數狀況下,這種標記是經過對象內部的標誌(Flag)來實現的。因而,被標記的對象咱們把它們塗黑。圖中(3)部分中,被標記的對象所可以引用的對象也被打上標記。重複這一步驟的話,就能夠將從根開始可能被間接引用到的對象所有打上標記。到此爲止的操做,稱爲標記階段(Mark phase)。
標記階段完成時,被標記的對象就被視爲「存活」對象。圖1中的(4)部分中,將所有對象按順序掃描一遍,將沒有被標記的對象進行回收。這一操做被稱爲清除階段(Sweep phase)。
在掃描的同時,還須要將存活對象的標記清除掉,以便爲下一次GC操做作好準備。標記清除算法的處理時間,是和存活對象數與對象總數的總和相關的。
做爲標記清除的變形,還有一種叫作標記壓縮(Mark and Compact)的算法,它不是將被標記的對象清除,而是將它們不斷壓縮。
標記清除算法有一個缺點,就是在分配了大量對象,而且其中只有一小部分存活的狀況下,所消耗的時間會大大超過必要的值,這是由於在清除階段還須要對大量死亡對象進行掃描。複製收集(Copy and Collection)則試圖克服這一缺點。在這種算法中,會將從根開始被引用的對象複製到另外的空間中,而後,再將複製的對象所可以引用的對象用遞歸的方式不斷複製下去。
圖2的(1)部分是GC開始前的內存狀態,這和圖1的(1)部分是同樣的。圖2的(2)部分中,在舊對象所在的「舊空間」之外,再準備出一塊「新空間」,並將可能從根被引用的對象複製到新空間中。圖中(3)部分中,從已經複製的對象開始,再將能夠被引用的對象像一串糖葫蘆同樣複製到新空間中。複製完成以後,「死亡」對象就被留在了舊空間中。圖中(4)部分中,將舊空間廢棄掉,就能夠將死亡對象所佔用的空間一口氣所有釋放出來,而沒有必要再次掃描每一個對象。下次GC的時候,如今的新空間也就變成了未來的舊空間。
經過圖2咱們能夠發現,複製收集方式中,只存在至關於標記清除方式中的標記階段。因爲清除階段中須要對現存的全部對象進行掃描,在存在大量對象,且其中大部分都即將死亡的狀況下,所有掃描一遍的開銷實在是不小。而在複製收集方式中,就不存在這樣的開銷。
可是,和標記相比,將對象複製一份所須要的開銷則比較大,所以在「存活」對象比例較高的狀況下,反而會比較不利。這種算法的另外一個好處是它具備局部性(Lo-cality)。在複製收集過程當中,會按照對象被引用的順序將對象複製到新空間中。因而,關係較近的對象被放在距離較近的內存空間中的可能性會提升,這被稱爲局部性。局部性高的狀況下,內存緩存會更容易有效運做,程序的運行性能也可以獲得提升。
引用計數(Reference Count)方式是GC算法中最簡單也最容易實現的一種,它和標記清除方式差很少是在同一時間發明出來的。它的基本原理是,在每一個對象中保存該對象的引用計數,當引用發生增減時對計數進行更新。引用計數的增減,通常發生在變量賦值、對象內容更新、函數結束(局部變量再也不被引用)等時間點。當一個對象的引用計數變爲0時,則說明它未來不會再被引用,所以能夠釋放相應的內存空間。
圖3的(1)部分中,全部對象中都保存着本身被多少個其餘對象進行引用的數量(引用計數),圖中每一個對象右上角的數字就是引用計數。圖中(2)部分中,當對象引用發生變化時,引用計數也跟着變化。在這裏,由對象B到對象D的引用失效了,因而對象D的引用計數變爲0。因爲對象D的引用計數爲0,所以由對象D到對象C和E的引用數也分別相應減小。結果,對象E的引用計數也變爲0,因而對象E也被釋放掉了。圖3的(3)部分中,引用計數變爲0的對象被釋放,「存活」對象則保留了下來。你們應該注意到,在整個GC處理過程當中,並不須要對全部對象進行掃描。
實現容易是引用計數算法最大的優勢。標記清除和複製收集這些GC機制在實現上都有必定難度;而引用計數方式的話,凡有些年頭的C++程序員(包括我在內),應該都曾經實現過相似的機制,能夠說這種算法是至關具備廣泛性的。除此以外,當對象再也不被引用的瞬間就會被釋放,這也是一個優勢。其餘的GC機制中,要預測一個對象什麼時候會被釋放是很困難的,而在引用計數方式中則是當即被釋放的。並且,因爲釋放操做是針對每一個對象個別執行的,所以和其餘算法相比,由GC而產生的中斷時間(Pause time)就比較短,這也是一個優勢。
引用計數方式的缺點另外一方面,這種方式也有缺點。引用計數最大的缺點,就是沒法釋放循環引用的對象。
圖4中,A、B、C三個對象沒有被其餘對象引用,而是互相之間循環引用,所以它們的引用計數永遠不會爲0,結果這些對象就永遠不會被釋放。引用計數的第二個缺點,就是必須在引用發生增減時對引用計數作出正確的增減,而若是漏掉了某個增減的話,就會引起很難找到緣由的內存錯誤。引用數忘了增長的話,會對不恰當的對象進行釋放;而引用數忘了減小的話,對象會一直殘留在內存中,從而致使內存泄漏。若是語言編譯器自己對引用計數進行管理的話還好,不然,若是是手動管理引用計數的話,那將成爲孕育bug的溫牀。
最後一個缺點就是,引用計數管理並不適合並行處理。若是多個線程同時對引用計數進行增減的話,引用計數的值就可能會產生不一致的問題(結果則會致使內存錯誤)。爲了不這種狀況的發生,對引用計數的操做必須採用獨佔的方式來進行。若是引用操做頻繁發生,每次都要使用加鎖等併發控制機制的話,其開銷也是不可小覷的。綜上所述,引用計數方式的原理和實現雖然簡單,但缺點也不少,所以最近基本上再也不使用了。如今,依然採用引用計數方式的語言主要有Perl和Python,但它們爲了不循環引用的問題,都配合使用了其餘的GC機制。這些語言中,GC基本上是經過引用計數方式來進行的,但偶爾也會用其餘的算法來執行GC,這樣就能夠將引用計數方式沒法回收的那些對象處理掉。