本章節主要介紹垃圾回收策略與算法
一、概述
程序計數器、虛擬機棧、本地方發棧這三個區域都是線程獨有的,隨線程而生,也隨線程而滅;棧幀隨着方法的開始而入棧,隨着方法的結束而出棧。這幾個區域的內存分配和回收都具備確認性,在這幾個區域內不需要過多考慮回收的問題,因爲方法結束或者線程結束時,內存自然就跟隨着回收了。
而對於 Java 堆和方法區,我們只有在程序運行期間才能知道會創建哪些對象,這部分內存的分配和回收都是動態的,垃圾收集器所關注的正是這部分內存。
哪些內存需要進行回收:Java 堆和方法區。
二、判定對象是否存活
什麼時候回收對象:若一個對象不被任何對象或變量引用,那麼它就是無效對象,需要被回收
1、引用計數法
在對象頭維護着一個 引用計數器,對象被引用一次則計數器 +1;若引用失效則計數器 -1。當計數器爲 0 時,就認爲該對象無效了。
引用計數算法的實現簡單,判定效率也很高,在大部分情況下它都是一個不錯的算法。但是主流的 Java 虛擬機裏沒有選用引用計數算法來管理內存,主要是因爲它很難解決對象之間循環引用的問題。
注意:對象 objA 和 objB 都有字段 instance,令 objA.instance = objB 並且 objB.instance = objA,由於它們互相引用着對方,導致它們的引用計數都不爲 0,於是引用計數算法無法通知 GC 收集器回收它們。
引用計數法的特點:
- 需要單獨的字段存儲計數器,增加了存儲空間的開銷;
- 每次賦值都需要更新計數器,增加了時間開銷;
- 垃圾對象便於辨識,只要計數器爲0,就可作爲垃圾回收;
- 及時回收垃圾,沒有延遲性;
- 不能解決循環引用的問題;
2、可達性分析法(主流)
在Java中,是通過可達性分析(Reachability Analysis)來判定對象是否存活的。該算法的基本思路就是通過一些被稱爲引用鏈(GC Roots)的對象作爲起點,從這些節點開始向下搜索,搜索走過的路徑被稱爲(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連時(即從GC Roots節點到該節點不可達),則證明該對象是不可用的。
GC Roots 是指:
- Java 虛擬機棧(棧幀中的本地變量表)中引用的對象
- 本地方法棧中引用的對象
- 方法區中常量引用的對象
- 方法區中類靜態屬性引用的對象
GC Roots 並不包括堆中對象所引用的對象,這樣就不會有循環引用的問題。
三、引用類型
判定對象是否存活與“引用”有關。在 JDK 1.2 以前,Java 中的引用定義很傳統,一個對象只有被引用或者沒有被引用兩種狀態,我們希望能描述這一類對象:當內存空間還足夠時,則保留在內存中;如果內存空間在進行垃圾手收集後還是非常緊張,則可以拋棄這些對象。很多系統的緩存功能都符合這樣的應用場景。
在 JDK 1.2 之後,Java 對引用的概念進行了擴充,將引用分爲了以下四種。不同的引用類型,主要體現的是對象不同的可達性狀態reachable
和垃圾收集的影響。
1、強引用(Strong Reference)
類似 "Object obj = new Object()" 這類的引用,就是強引用,只要強引用存在,垃圾收集器永遠不會回收被引用的對象。但是,如果我們錯誤地保持了強引用,比如:賦值給了 static 變量,那麼對象在很長一段時間內不會被回收,會產生內存泄漏。也就是說,寧願出現內存溢出,也不會回收這些對象。
2、軟引用(Soft Reference)
軟引用是一種相對強引用弱化一些的引用,可以讓對象豁免一些垃圾收集,只有當 JVM 認爲內存不足時,纔會去試圖回收軟引用指向的對象。JVM 會確保在拋出 OutOfMemoryError 之前,清理軟引用指向的對象。軟引用通常用來實現內存敏感的緩存,如果還有空閒內存,就可以暫時保留緩存,當內存不足時清理掉,這樣就保證了使用緩存的同時,不會耗盡內存。比如網頁緩存、圖片緩存等。
import java.lang.ref.SoftReference;
public class Main {
public static void main(String[] args) {
SoftReference<String> sr = new SoftReference<String>(new String("hello"));
System.out.println(sr.get());
}
}
3、弱引用(Weak Reference)
弱引用的強度比軟引用更弱一些;被弱引用引用的對象只能生存到下一次垃圾收集前,一旦發生垃圾收集,被弱引用所引用的對象就會被清掉
import java.lang.ref.WeakReference;
public class Main {
public static void main(String[] args) {
WeakReference<String> sr = new WeakReference<String>(new String("hello"));
System.out.println(sr.get());
System.gc(); //通知JVM的gc進行垃圾回收
System.out.println(sr.get());
}
}
注意:在使用軟引用和弱引用的時候,我們可以顯示地通過System.gc()來通知JVM進行垃圾回收,但是要注意的是,雖然發出了通知,JVM不一定會立刻執行,也就是說這句是無法確保此時JVM一定會進行垃圾回收的。
4、虛引用(Phantom Reference)
虛引用也稱幽靈引用或者幻影引用,它是最弱的一種引用關係。一個對象是否有虛引用的存在,完全不會對其生存時間構成影響。它僅僅是提供了一種確保對象被 finalize 以後,做某些事情的機制,比如,通常用來做所謂的 Post-Mortem 清理機制。
要注意的是,虛引用必須和引用隊列關聯使用,當垃圾回收器準備回收一個對象時,如果發現它還有虛引用,就會把這個虛引用加入到與之 關聯的引用隊列中。程序可以通過判斷引用隊列中是否已經加入了虛引用,來了解被引用的對象是否將要被垃圾回收。如果程序發現某個虛引用已經被加入到引用隊列,那麼就可以在所引用的對象的內存被回收之前採取必要的行動。
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
public class Main {
public static void main(String[] args) {
ReferenceQueue<String> queue = new ReferenceQueue<String>();
PhantomReference<String> pr = new PhantomReference<String>(new String("hello"), queue);
System.out.println(pr.get());
}
}
注意:虛引用中有一個構造函數,可以看出,其必須和一個引用隊列一起存在。get()方法永遠返回null,因爲虛引用永遠不可達
四、宣告對象死亡的兩次標記過程
即使在可達性分析算法中不可達的對象,也並非是“非死不可”的,這時候它們暫時處於“緩刑”階段,要真正宣告一個對象死亡,至少要經歷再次標記過程。標記的前提是對象在進行可達性分析後發現沒有與GC Roots相連接的引用鏈。
1、第一次標記並進行一次篩選。
篩選的條件是此對象是否有必要執行finalize()方法;當對象沒有覆蓋finalize方法,或者finzlize方法已經被虛擬機調用過,虛擬機將這兩種情況都視爲“沒有必要執行”,對象被回收。
2、 第二次標記
如果這個對象被判定爲有必要執行finalize()方法,那麼這個對象將會被放置在一個名爲:F-Queue的隊列之中,並在稍後由一條虛擬機自動建立的、低優先級的Finalizer線程去執行。這裏所謂的“執行”是指虛擬機會觸發這個方法,但並不承諾會等待它運行結束。這樣做的原因是,如果一個對象finalize()方法中執行緩慢,或者發生死循環(更極端的情況),將很可能會導致F-Queue隊列中的其他對象永久處於等待狀態,甚至導致整個內存回收系統崩潰。finalize()方法是對象脫逃死亡命運的最後一次機會,稍後GC將對F-Queue中的對象進行第二次小規模標記,如果對象要在finalize()中成功拯救自己----只要重新與引用鏈上的任何的一個對象建立關聯即可,譬如把自己賦值給某個類變量或對象的成員變量,那在第二次標記時它將移除出“即將回收”的集合。如果對象這時候還沒逃脫,那基本上它就真的被回收了。
注意:
- 任何一個對象的 finalize() 方法只會被系統自動調用一次,如果對象面臨下一次回收,它的 finalize() 方法不會被再次執行,想繼續在 finalize() 中自救就失效了
finalize()
方法能做的,try-finally 都能做,所以忘了這個方法吧!
五、方法區的垃圾回收
方法區中存放生命週期較長的類信息、常量、靜態變量,每次垃圾收集只有少量的垃圾被清除。方法區中主要清除兩種垃圾:
- 廢棄常量:例如一個“abc”字符串常量存在常量池中,但沒有任何String類型對象引用常量池中的這個常量,也沒有其它地方引用了這個常量,如果必要的話,這個常量也會被系統清理出常量池;常量池中的其他類(接口)、方法、字段的符號引用也與此類似
- 無用的類:同時滿足以下 3 個條件的類。
- 該類的所有實例已被回收,Java 堆中不存在該類的任何實例;
- 加載該類的 Classloader 已被回收;
- 該類的 Class 對象沒有被任何地方引用,即無法在任何地方通過反射訪問該類的方法。
注意:
- 虛擬機是可以對滿足上述3個條件的類進行回收,僅僅是“可以”,並不是和對象一樣,不使用了就必然回收。是否對類進行回收,HotSpot虛擬機提供了-Xnoclassgc參數進行控制
- 在大量使用反射、動態代理、CGLib等ByteCode框架、動態生成JSP以及OSGi這類頻繁自定義ClassLoader的場景都需要虛擬機具備類卸載的功能,以保證永久代不會溢出。
六、垃圾回收算法
1、標記-清除算法
標記:遍歷所有的 GC Roots
,然後將所有 GC Roots
可達的對象標記爲存活的對象。
清除:將遍歷堆中所有的對象,將沒有標記的對象全部清除掉。與此同時,清除那些被標記過的對象的標記,以便下次的垃圾回收。
不足:
- 效率問題:標記和清除兩個過程的效率都不高。
- 空間問題:標記清除之後會產生大量不連續的內存碎片,碎片太多可能導致以後需要分配較大對象時,無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集動作。
2、複製算法(新生代)
爲了解決效率問題,“複製”收集算法出現了。它將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊內存用完,需要進行垃圾收集時,就將存活者的對象複製到另一塊上面,然後將第一塊內存全部清除。這種算法有優有劣:
- 優點:不會有內存碎片的問題。
- 缺點:內存縮小爲原來的一半,浪費空間。
爲了解決空間利用率問題,可以將內存分爲三塊: Eden、From Survivor、To Survivor,比例是 8:1:1,每次使用 Eden 和其中一塊 Survivor。回收時,將 Eden 和 Survivor 中還存活的對象一次性複製到另外一塊 Survivor 空間上,最後清理掉 Eden 和剛纔使用的 Survivor 空間。這樣只有 10% 的內存被浪費。但是我們無法保證每次回收都只有不多於 10% 的對象存活,當 Survivor 空間不夠,需要依賴其他內存(指老年代)進行分配擔保。
分配擔保:爲對象分配內存空間時,如果 Eden+Survivor 中空閒區域無法裝下該對象,會觸發 MinorGC 進行垃圾收集。但如果 Minor GC 過後依然有超過 10% 的對象存活,這樣存活的對象直接通過分配擔保機制進入老年代,然後再將新對象存入 Eden 區。
3、標記-整理算法(老年代)
標記:它的第一個階段與標記/清除算法是一模一樣的,均是遍歷 GC Roots
,然後將存活的對象標記。
整理:移動所有存活的對象,且按照內存地址次序依次排列,然後將末端內存地址以後的內存全部回收。因此,第二階段才稱爲整理階段。
這是一種老年代的垃圾收集算法。老年代的對象一般壽命比較長,因此每次垃圾回收會有大量對象存活,如果採用複製算法,每次需要複製大量存活的對象,效率很低。
4、分代收集算法
根據對象存活週期的不同,將內存劃分爲幾塊。一般是把 Java 堆分爲新生代和老年代,針對各個年代的特點採用最適當的收集算法。
- 新生代: GC 過後只有少量對象存活 —— 複製算法
- 老年代: GC 過後對象存活率高 —— 標記 - 整理算法