- JVM的垃圾收集器(Garbage Collector)管理的是Java堆和方法區的垃圾回收。
- GC需要完成三件事:哪些內存需要回收(即判定垃圾)?什麼時候回收?如何回收(垃圾收集算法)?
垃圾判定算法
引用計數算法
- 對指向對象的引用進行計數,當計數器值歸零時則刪除該對象。
- 但是當出現循環引用時,相關的對象永遠無法被判定爲垃圾。
可達性分析算法
該算法基本思路如下:
- 選定一系列對象作爲根對象“GC Roots”,根據引用關係向下搜索,搜索走過的路徑稱爲“引用鏈”
- 如果某個對象到“GC Roots”之間沒有任何引用鏈相連,則證明此對象是不可能再被使用的
固定選爲“GC Roots”的對象有以下幾類:
- 在虛擬機棧中引用的對象。(局部變量、臨時變量等)
- 在方法區中類靜態變量和常量引用的對象
- 在本地方法棧中JNI(即本地方法)引用的對象
- JVM內部的引用
- 所有被同步鎖持有的對象
除此以外,不同的垃圾收集器還有自己的選擇。
引用的類型:無論哪一種垃圾判定算法都與引用有關,在JDK1.2版本之後擴充了引用的概念:
- 強引用:即平時最常見的引用關係,有強引用的對象一定不會被回收
- 軟引用:在首次回收後如果還沒有足夠的內存,纔會回收只有軟引用的對象(
SoftReference
類)- 弱引用:比軟引用更弱,在垃圾收集器開始工作時,無論內存是否足夠,只有弱引用的對象都會被回收(
WeakReference
類)- 虛引用:最弱的引用關係,甚至無法從虛引用獲取對象實例,更無法對對象的生存時間構成影響。(
PhantomReference
類)
垃圾收集算法
分代收集理論
當前商業虛擬機的垃圾收集器大部分都基於“分代收集(Generational Collection)”的理論。分代收集理論基於以下假說:
- 弱分代假說(Weak Generational Hypothesis):絕大多數對象都是朝生夕滅的。
- 強分代假說(Strong Generational Hypothesis):熬過越多次垃圾收集過程的對象就越難以消亡。
- 跨代引用假說(Intergenerational Reference Hypothesis):跨代引用相對於同代引用來說僅佔極少數。
基於以上假說的垃圾收集器會將其管理的內存劃分爲不同的區域,並根據各區域的特點以不同的頻率各自執行不同的垃圾收集算法。具體來說,至少會有“新生代(Young Generation)”和“老年代(Old Generation)”兩類區域。在新生代中,每次垃圾收集時都會有大批對象被回收,存活的少量對象會逐漸被移動到老年代中。據此可以總結以下特點:
- 新生代中的大部分對象都很容易被回收,因此需要以較高的頻率執行垃圾收集算法;同時每次垃圾收集後的新生代區域中的對象會很稀疏。
- 老年代中的大部分對象難以消亡,因此垃圾回收的頻率比較低;並且每次垃圾收集後會產生較小的碎片。
因此在不同的區域中需要執行不同的垃圾收集算法。常見的垃圾收集算法有以下幾種。
標記-清除算法
- 標記-清除(Mark-Sweep)算法是最早最基礎的垃圾收集算法(算法示意圖如下)
- 過程:第一階段根據垃圾判定算法標記出需要回收(或需要存活)的對象;第二階段清除(或保留)被標記的對象。
- 缺點:一是執行效率不穩定,容易隨着對象數量增長而降低;二是內存會產生較多的碎片,導致觸發過多不必要的GC動作。
標記-複製算法
半區複製,Semispace Copying
- 過程:將內存分爲大小相等的兩半,每次只使用其中一半。當執行垃圾收集時,就把仍存活的對象依次複製到另一半,再把已使用過的一半直接清理掉。
- 缺點:一是如果內存中多數對象是存活的,就會產生大量的複製開銷;二是可用內存縮小了一半。
Appel式回收
根據IBM一項研究,新生代中的對象98%都熬不過第一輪收集,因此並不需要按照半區複製中1:1的比例來劃分新生代的內存空間。
- 爲了優化半區複製的空間代價,可以將新生代分爲一塊較大的Eden空間和兩塊較小的Survivor空間。每次分配內存只使用Eden和其中一塊Survivor,垃圾收集時將所有的存活對象複製到另一塊Survivor中,然後清理掉已使用的兩塊空間。
- HotSpot虛擬機默認的Eden和Survivor大小比例爲8:1,即只有10%的空間是被浪費的。
- 如果垃圾收集後有大於10%的對象存活,需要依賴其他內存區域(實際上大多就是老年代)保存多出來的存活對象。
標記-整理算法
- 由於標記-複製算法需要其他內存區域的擔保,因此老年代不能直接用這種算法。標記-整理(Mark-Compact)算法就是針對老年代提出的。
- 過程:標記階段後,讓所有存活的對象向內存空間的一端移動。
- 缺點:與簡單的標記-清除算法比,移動操作有較重的負擔,會延長GC的延遲。(相應的,由於沒有碎片,內存分配會更加簡單)。
- Compromise:在老年代上,平時多數時間都採用標記-清除算法;直到內存空間的碎片化程度已經大到影響對象分配時,在採用標記-整理算法收集一次以清理碎片。