以前上學的時候有這個一個梗,說在食堂裏吃飯,吃完把餐盤端走清理的,是 C++ 程序員,吃完直接就走的,是 Java 程序員。🤔程序員
確實,在 Java 的世界裏,彷佛咱們不用對垃圾回收那麼的專一,不少初學者不懂 GC,也依然能寫出一個能用甚至還不錯的程序或系統。但其實這並不表明 Java 的 GC 就不重要。相反,它是那麼的重要和複雜,以致於出了問題,那些初學者除了打開 GC 日誌,看着一堆0101的天文,啥也作不了。😯算法
今天咱們就從頭至尾完整地聊一聊 Java 的垃圾回收。bash
什麼是垃圾回收函數
垃圾回收(Garbage Collection,GC),顧名思義就是釋放垃圾佔用的空間,防止內存泄露。有效的使用可使用的內存,對內存堆中已經死亡的或者長時間沒有使用的對象進行清除和回收。spa
Java 語言出來以前,你們都在拼命的寫 C 或者 C++ 的程序,而此時存在一個很大的矛盾,C++ 等語言建立對象要不斷的去開闢空間,不用的時候又須要不斷的去釋放控件,既要寫構造函數,又要寫析構函數,不少時候都在重複的 allocated,而後不停的析構。因而,有人就提出,能不能寫一段程序實現這塊功能,每次建立,釋放控件的時候複用這段代碼,而無需重複的書寫呢?線程
1960年,基於 MIT 的 Lisp 首先提出了垃圾回收的概念,而這時 Java 尚未出世呢!因此實際上 GC 並非Java的專利,GC 的歷史遠遠大於 Java 的歷史!日誌
怎麼定義垃圾code
既然咱們要作垃圾回收,首先咱們得搞清楚垃圾的定義是什麼,哪些內存是須要回收的。cdn
引用計數算法對象
引用計數算法(Reachability Counting)是經過在對象頭中分配一個空間來保存該對象被引用的次數(Reference Count)。若是該對象被其它對象引用,則它的引用計數加1,若是刪除對該對象的引用,那麼它的引用計數就減1,當該對象的引用計數爲0時,那麼該對象就會被回收。
String m = new String("jack");
複製代碼
先建立一個字符串,這時候"jack"有一個引用,就是 m。
而後將 m 設置爲 null,這時候"jack"的引用次數就等於0了,在引用計數算法中,意味着這塊內容就須要被回收了。
m = null;
複製代碼
引用計數算法是將垃圾回收分攤到整個應用程序的運行當中了,而不是在進行垃圾收集時,要掛起整個應用的運行,直到對堆中全部對象的處理都結束。所以,採用引用計數的垃圾收集不屬於嚴格意義上的"Stop-The-World"的垃圾收集機制。
看似很美好,但咱們知道JVM的垃圾回收就是"Stop-The-World"的,那是什麼緣由致使咱們最終放棄了引用計數算法呢?看下面的例子。
public class ReferenceCountingGC {
public Object instance;
public ReferenceCountingGC(String name){}
}
public static void testGC(){
ReferenceCountingGC a = new ReferenceCountingGC("objA");
ReferenceCountingGC b = new ReferenceCountingGC("objB");
a.instance = b;
b.instance = a;
a = null;
b = null;
}
複製代碼
咱們能夠看到,最後這2個對象已經不可能再被訪問了,但因爲他們相互引用着對方,致使它們的引用計數永遠都不會爲0,經過引用計數算法,也就永遠沒法通知GC收集器回收它們。
可達性分析算法
可達性分析算法(Reachability Analysis)的基本思路是,經過一些被稱爲引用鏈(GC Roots)的對象做爲起點,從這些節點開始向下搜索,搜索走過的路徑被稱爲(Reference Chain),當一個對象到 GC Roots 沒有任何引用鏈相連時(即從 GC Roots 節點到該節點不可達),則證實該對象是不可用的。
經過可達性算法,成功解決了引用計數所沒法解決的問題-「循環依賴」,只要你沒法與 GC Root 創建直接或間接的鏈接,系統就會斷定你爲可回收對象。那這樣就引伸出了另外一個問題,哪些屬於 GC Root。
Java 內存區域
在 Java 語言中,可做爲 GC Root 的對象包括如下4種:
虛擬機棧(棧幀中的本地變量表)中引用的對象
方法區中類靜態屬性引用的對象
方法區中常量引用的對象
本地方法棧中 JNI(即通常說的 Native 方法)引用的對象
虛擬機棧(棧幀中的本地變量表)中引用的對象
此時的 s,即爲 GC Root,當s置空時,localParameter 對象也斷掉了與 GC Root 的引用鏈,將被回收。
public class StackLocalParameter {
public StackLocalParameter(String name){}
}
public static void testGC(){
StackLocalParameter s = new StackLocalParameter("localParameter");
s = null;
}
複製代碼
方法區中類靜態屬性引用的對象
s 爲 GC Root,s 置爲 null,通過 GC 後,s 所指向的 properties 對象因爲沒法與 GC Root 創建關係被回收。
而 m 做爲類的靜態屬性,也屬於 GC Root,parameter 對象依然與 GC root 創建着鏈接,因此此時 parameter 對象並不會被回收。
public class MethodAreaStaicProperties {
public static MethodAreaStaicProperties m;
public MethodAreaStaicProperties(String name){}
}
public static void testGC(){
MethodAreaStaicProperties s = new MethodAreaStaicProperties("properties");
s.m = new MethodAreaStaicProperties("parameter");
s = null;
}
複製代碼
方法區中常量引用的對象
m 即爲方法區中的常量引用,也爲 GC Root,s 置爲 null 後,final 對象也不會因沒有與 GC Root 創建聯繫而被回收。
public class MethodAreaStaicProperties {
public static final MethodAreaStaicProperties m = MethodAreaStaicProperties("final");
public MethodAreaStaicProperties(String name){}
}
public static void testGC(){
MethodAreaStaicProperties s = new MethodAreaStaicProperties("staticProperties");
s = null;
}
複製代碼
本地方法棧中引用的對象
任何 Native 接口都會使用某種本地方法棧,實現的本地方法接口是使用 C 鏈接模型的話,那麼它的本地方法棧就是 C 棧。當線程調用 Java 方法時,虛擬機會建立一個新的棧幀並壓入 Java 棧。然而當它調用的是本地方法時,虛擬機會保持 Java 棧不變,再也不在線程的 Java 棧中壓入新的幀,虛擬機只是簡單地動態鏈接並直接調用指定的本地方法。
未完,待續·············
note:我走得很慢,可是我從不後悔。