GC的基本問題
什么是GC?
GC 是 garbage collection 的縮寫,意思是垃圾回收——把內存(特別是堆內存)中不再使用的空間釋放掉;清理不再使用的對象。
為什么要GC?
堆內存是各個線程共享的空間,不能無節制的使用。服務器運行的時間通常都很長。累積的對象也會非常多。這些對象如果不做任何清理,任由它們數量不斷累加,內存很快就會耗盡。所以GC就是要把不使用的對象都清理掉,把內存空間空出來,讓項目可以持續運行下去。
如何判斷對象已死?(GC觸發的條件)
引用計數算法
簡單來說,引用計數算法就是在對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加一;當引用失效時,計數器值就減一;任何時刻計數器為零的對象就是不可能再被使用的。
但這時會出現一個問題,即當兩個對象相互進行引用時,也就意味著,它永遠都無法”死亡“,就無法進行回收。
Java虛擬機并不是通過引用計數算法來判斷對象是否存活的。
可達性分析算法
通過一系列被稱為 “GC Roots” 的對象作為起始點,從這些節點向下搜索,搜索走過的路徑叫引用鏈。當一個對象到 GC Roots 沒有任何引用鏈相連時,證明此對象不可用,可被判定為 “死亡” 。
在 Java 語言中,可作為 GC Roots 的對象包括:
-
在虛擬機棧(棧幀中的本地變量表)中引用的對象,譬如各個線程被調用的方法堆棧中使用到的 參數、局部變量、臨時變量等。
-
在方法區中類靜態屬性引用的對象,譬如Java類的引用類型靜態變量。
-
在方法區中常量引用的對象,譬如字符串常量池(String Table)里的引用。
-
在本地方法棧中JNI(即通常所說的Native方法)引用的對象。
-
Java虛擬機內部的引用,如基本數據類型對應的Class對象,一些常駐的異常對象(比如 NullPointExcepiton、OutOfMemoryError)等,還有系統類加載器。
-
所有被同步鎖(synchronized關鍵字)持有的對象。
-
反映Java虛擬機內部情況的JMXBean、JVMTI中注冊的回調、本地代碼緩存等。
再談引用
引用大至分為四種:
- 強引用:new出來的對象 內存溢出也不會進行回收
- 軟引用:只被軟引用關聯著的對象,在系統將要發生內存溢出異常前,會把這些對象列進回收范圍之中進行第二次回收,如果這次回收還沒有足夠的內存, 才會拋出內存溢出異常。
- 弱引用:只要有垃圾收集就會回收
- 虛引用:回收時收到系統通知
生存還是死亡?(是否真正進行回收)
即使在可達性分析算法中判定為不可達的對象,也不是“非死不可”的,這時候它們暫時還處于“緩 刑”階段,要真正宣告一個對象死亡,至少要經歷兩次標記過程:
- 第一次標記:是否有與GC Roots相連接的引用鏈
- 第二次篩選:此對象是否有必要執行finalize()方法(判斷對象能否通過finalize()方法實現自我拯救(避免被回收 )
?finalize()是java.lang.Object類的方法,所有對象默認繼承這個方法。方法體內是空的,說明如果子類不重寫這個方法,那么不執行任何邏輯。
回收方法區(永久代)
永久代的垃圾收集主要回收兩部分內容:廢棄常量和無用的類。
廢棄常量回收比較簡單,就是看是否仍有其他地方引用了此字面量,若無引用,則被回收。而判斷一個類是否需要被回收條件比較苛刻,需要同時滿足下面三個條件才能算是“無用的類”:
- 該類所有的實例都已經被回收,也就是java堆中不存在該類的任何實例
- 加載該類的ClassLoader已經被回收
- 該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法
分代收集理論
弱分代假說(新生代)
絕大多數對象都是朝生夕滅的。
強分代假說(老年代)
熬過越多次垃圾收集過程的對象就越難以消亡。
垃圾收集器的設計原則
將Java堆根據年齡(年齡即對象熬過垃圾收集過程的次數)劃分出不同的區域進行存儲。當一個對象存活時間越長,年齡越大,我們就將其移動到老年代區域,這樣將新生代和老年代分開管理。每次回收時只關注如何保留少量存活而不是去標記那些大量將要被回收的對 象,就能以較低代價回收到大量的空間;
?跨代引用假說
跨代引用相對于同代引用來說僅占極少數。
當新生代和老年代互相引用,是應該傾向于同時生存或者同時消亡的。如果某個新生代對象存在跨代引用,由于老年代對象難以 消亡,該引用會使得新生代對象在收集時同樣得以存活,進而在年齡增長之后晉升到老年代中,這時 跨代引用也隨即被消除了。
如何記錄每一個對象是否存在及存在哪些跨代引用?
在新生代上建立一個全局的數據結構(記憶集),這個結構把老年代劃分成若干小塊,標識出老年代的哪一塊內存會存在跨代引用。跨代引用的小塊內存里的對象才會被加入到GC Roots進行掃描。
垃圾收集算法
標記-清除算法(老年代)
首先標記出需要回收的對象,在標記完成后統一回收掉所有的被標記對象。
缺點:
- 執行效率不穩定:標記和清除兩個過 程的執行效率都隨對象數量增長而降低;
- 內存空間的碎片化問題:標記、清除之后會產生大 量不連續的內存碎片,空間碎片太多可能會導致當以后在程序運行過程中需要分配較大對象時無法找 到足夠的連續內存而不得不提前觸發另一次垃圾收集動作。?
標記-復制算法(新生代)
它將可用內存按需要分成兩塊(實際情況不一定2塊,塊的大小比例不一定),每次只使用其中的一塊。當這一塊的內存用完了,就將還存活著的對象復制到另外一塊上面,然后再把已使用過的內存空間一次清理掉。
新生代通常使用復制算法,因為新生代中對象大多 “朝生夕滅”,每次垃圾收集時有大量對象死去,只有少量存活,復制算法能高效地將少量存活對象復制到另一塊空間,實現快速回收。
優點:每次只對其中一塊進行GC,不用考慮內存碎片的問題,并且實現簡單,運行高效。(適用于新生代,效率高)
缺點:內存縮小了一半。復制的目標空間需要依賴其他空間進行分配擔保(將超出的部分直接放入到老年代區域中)。效率隨對象存活率升高而降低:當對象存活率較高時,需要進行較多復制操作,效率將會變低。
Appel式回收
把新生代分為一塊較大的Eden空間和兩塊較小的 Survivor空間(8:1:1),每次分配內存只使用Eden和其中一塊Survivor。
發生垃圾搜集時,將Eden和Survivor中仍 然存活的對象一次性復制到另外一塊Survivor空間上,然后直接清理掉Eden和已用過的那塊Survivor空間。
當Survivor空間不足以容納一次Minor GC之后存活的對象時,就需要依賴其他內存區域(將超出的部分直接放入到老年代區域中)。
標記-整理算法(老年代)
使用“標記-整理”算法:先標記,再把所有存活的對象向一端移動,然后直接清理端邊界意外的內存。
老年代中對象存活率高,沒有額外空間對其進行分配擔保。如果在老年代使用復制算法,由于存活對象多,復制操作會比較頻繁,效率低下且成本較高。
優點:不會像復制算法、效率隨對象存活率升高而變低。不會像標記-清除算法,產生內存碎片(因為清除前,進行了整理,存活對象都集中到空間一側)。
缺點:主要是效率問題:除像標記-清除算法的標記過程外,還多了需要整理的過程,效率更低。?