Java 語言的一大特點就是可以進行自動垃圾回收處理,而無需開發人員過於關注系統資源,例如內存資源的釋放情況。自動垃圾收集雖然大大減輕了開發人員的工作量,但是也增加了軟件系統的負擔。
擁有垃圾收集器可以說是 Java 語言與 C++語言的一項顯著區別。在 C++語言中,程序員必須小心謹慎地處理每一項內存分配,且內存使用完後必須手工釋放曾經占用的內存空間。當內存釋放不夠完全時,即存在分配但永不釋放的內存塊,就會引起內存洩漏,嚴重時甚至導致程序癱瘓。
以下列舉了垃圾回收器常用的算法及實驗原理:
引用計數法 (Reference Counting)
引用計數器在微軟的 COM 組件技術中、Adobe 的 ActionScript3 種都有使用。
引用計數器的實現很簡單,對於一個對象 A,只要有任何一個對象引用了 A,則 A 的引用計數器就加 1,當引用失效時,引用計數器就減 1。只要對象 A 的引用計數器的值為 0,則對象 A 就不可能再被使用。
引用計數器的實現也非常簡單,只需要為每個對象配置一個整形的計數器即可。但是引用計數器有一個嚴重的問題,即無法處理循環引用的情況。因此,在 Java 的垃圾回收器中沒有使用這種算法。
一個簡單的循環引用問題描述如下:有對象 A 和對象 B,對象 A 中含有對象 B 的引用,對象 B 中含有對象 A 的引用。此時,對象 A 和對象 B 的引用計數器都不為 0。但是在系統中卻不存在任何第 3 個對象引用了 A 或 B。也就是說,A 和 B 是應該被回收的垃圾對象,但由於垃圾對象間相互引用,從而使垃圾回收器無法識別,引起內存洩漏。
標記-清除算法 (Mark-Sweep)
標記-清除算法將垃圾回收分為兩個階段:標記階段和清除階段。一種可行的實現是,在標記階段首先通過根節點,標記所有從根節點開始的較大對象。因此,未被標記的對象就是未被引用的垃圾對象。然後,在清除階段,清除所有未被標記的對象。該算法最大的問題是存在大量的空間碎片,因為回收後的空間是不連續的。在對象的堆空間分配過程中,尤其是大對象的內存分配,不連續的內存空間的工作效率要低於連續的空間。
復制算法 (Copying)
將現有的內存空間分為兩快,每次只使用其中一塊,在垃圾回收時將正在使用的內存中的存活對象復制到未被使用的內存塊中,之後,清除正在使用的內存塊中的所有對象,交換兩個內存的角色,完成垃圾回收。
如果系統中的垃圾對象很多,復制算法需要復制的存活對象數量並不會太大。因此在真正需要垃圾回收的時刻,復制算法的效率是很高的。又由於對象在垃圾回收過程中統一被復制到新的內存空間中,因此,可確保回收後的內存空間是沒有碎片的。該算法的缺點是將系統內存折半。
Java 的新生代串行垃圾回收器中使用了復制算法的思想。新生代分為 eden 空間、from 空間、to 空間 3 個部分。其中 from 空間和 to 空間可以視為用於復制的兩塊大小相同、地位相等,且可進行角色互換的空間塊。from 和 to 空間也稱為 survivor 空間,即幸存者空間,用於存放未被回收的對象。
在垃圾回收時,eden 空間中的存活對象會被復制到未使用的 survivor 空間中 (假設是 to),正在使用的 survivor 空間 (假設是 from) 中的年輕對象也會被復制到 to 空間中 (大對象,或者老年對象會直接進入老年帶,如果 to 空間已滿,則對象也會直接進入老年代)。此時,eden 空間和 from 空間中的剩余對象就是垃圾對象,可以直接清空,to 空間則存放此次回收後的存活對象。這種改進的復制算法既保證了空間的連續性,又避免了大量的內存空間浪費。
標記-壓縮算法 (Mark-Compact)
復制算法的高效性是建立在存活對象少、垃圾對象多的前提下的。這種情況在年輕代經常發生,但是在老年代更常見的情況是大部分對象都是存活對象。如果依然使用復制算法,由於存活的對象較多,復制的成本也將很高。
標記-壓縮算法是一種老年代的回收算法,它在標記-清除算法的基礎上做了一些優化。也首先需要從根節點開始對所有可達對象做一次標記,但之後,它並不簡單地清理未標記的對象,而是將所有的存活對象壓縮到內存的一端。之後,清理邊界外所有的空間。這種方法既避免了碎片的產生,又不需要兩塊相同的內存空間,因此,其性價比比較高。
增量算法 (Incremental Collecting)
在垃圾回收過程中,應用軟件將處於一種 CPU 消耗很高的狀態。在這種 CPU 消耗很高的狀態下,應用程序所有的線程都會掛起,暫停一切正常的工作,等待垃圾回收的完成。如果垃圾回收時間過長,應用程序會被掛起很久,將嚴重影響用戶體驗或者系統的穩定性。
增量算法的基本思想是,如果一次性將所有的垃圾進行處理,需要造成系統長時間的停頓,那麼就可以讓垃圾收集線程和應用程序線程交替執行。每次,垃圾收集線程只收集一小片區域的內存空間,接著切換到應用程序線程。依次反復,直到垃圾收集完成。使用這種方式,由於在垃圾回收過程中,間斷性地還執行了應用程序代碼,所以能減少系統的停頓時間。但是,因為線程切換和上下文轉換的消耗,會使得垃圾回收的總體成本上升,造成系統吞吐量的下降。
分代 (Generational Collecting)
根據垃圾回收對象的特性,不同階段最優的方式是使用合適的算法用於本階段的垃圾回收,分代算法即是基於這種思想,它將內存區間根據對象的特點分成幾塊,根據每塊內存區間的特點,使用不同的回收算法,以提高垃圾回收的效率。以 Hot Spot 虛擬機為例,它將所有的新建對象都放入稱為年輕代的內存區域,年輕代的特點是對象會很快回收,因此,在年輕代就選擇效率較高的復制算法。當一個對象經過幾次回收後依然存活,對象就會被放入稱為老生代的內存空間。在老生代中,幾乎所有的對象都是經過幾次垃圾回收後依然得以幸存的。因此,可以認為這些對象在一段時期內,甚至在應用程序的整個生命周期中,將是常駐內存的。如果依然使用復制算法回收老生代,將需要復制大量對象。再加上老生代的回收性價比也要低於新生代,因此這種做法也是不可取的。根據分代的思想,可以對老年代的回收使用與新生代不同的標記-壓縮算法,以提高垃圾回收效率。
從不同角度分析垃圾收集器,可以將其分為不同的類型。
1. 按線程數分,可以分為串行垃圾回收器和並行垃圾回收器。串行垃圾回收器一次只使用一個線程進行垃圾回收;並行垃圾回收器一次將開啟多個線程同時進行垃圾回收。在並行能力較強的 CPU 上,使用並行垃圾回收器可以縮短 GC 的停頓時間。
2. 按照工作模式分,可以分為並發式垃圾回收器和獨占式垃圾回收器。並發式垃圾回收器與應用程序線程交替工作,以盡可能減少應用程序的停頓時間;獨占式垃圾回收器 (Stop the world) 一旦運行,就停止應用程序中的其他所有線程,直到垃圾回收過程完全結束。
3. 按碎片處理方式可分為壓縮式垃圾回收器和非壓縮式垃圾回收器。壓縮式垃圾回收器會在回收完成後,對存活對象進行壓縮整理,消除回收後的碎片;非壓縮式的垃圾回收器不進行這步操作。
4. 按工作的內存區間,又可分為新生代垃圾回收器和老年代垃圾回收器。
可以用以下指標評價一個垃圾處理器的好壞。
吞吐量:指在應用程序的生命周期內,應用程序所花費的時間和系統總運行時間的比值。系統總運行時間=應用程序耗時+GC 耗時。如果系統運行了 100min,GC 耗時 1min,那麼系統的吞吐量就是 (100-1)/100=99%。
垃圾回收器負載:和吞吐量相反,垃圾回收器負載指來記回收器耗時與系統運行總時間的比值。
停頓時間:指垃圾回收器正在運行時,應用程序的暫停時間。對於獨占回收器而言,停頓時間可能會比較長。使用並發的回收器時,由於垃圾回收器和應用程序交替運行,程序的停頓時間會變短,但是,由於其效率很可能不如獨占垃圾回收器,故系統的吞吐量可能會較低。
垃圾回收頻率:指垃圾回收器多長時間會運行一次。一般來說,對於固定的應用而言,垃圾回收器的頻率應該是越低越好。通常增大堆空間可以有效降低垃圾回收發生的頻率,但是可能會增加回收產生的停頓時間。
反應時間:指當一個對象被稱為垃圾後多長時間內,它所占據的內存空間會被釋放。
堆分配:不同的垃圾回收器對堆內存的分配方式可能是不同的。一個良好的垃圾收集器應該有一個合理的堆內存區間劃分。