背景與目的:在Java與C++之間有一個由內存動態(tài)分配與垃圾回收技術所組成的圍墻聂渊,我們通過學習這圍墻的知識可以更好的解決:出現各種內存溢出,內存泄漏時粟判,當垃圾收集成為系統(tǒng)達到更高并發(fā)量的瓶頸時的問題氏仗。
回收對象:Java內存運行時區(qū)域中的程序計數器,虛擬機棧葱椭,本地方法棧都是跟線程相同的生命周期,在線程結束后口四,內存自然也就隨著回收了。而Java堆與方法區(qū)不一樣秦陋,我們只有在運行時才能知道對象具體分配的內存大小蔓彩,內存的創(chuàng)建于回收都是動態(tài)的,因此垃圾收集器最主要關注的就是這部分的內存驳概。
既然垃圾收集器要將堆中的對象進行回收赤嚼,那就要判斷哪些對象是可以進行回收,哪些是不可以回收的顺又,即判斷哪些是不可能再被任何途徑所使用的對象更卒。
如何判斷對象可以回收
一,引用計數算法(Reference Counting)
算法原理:給對象添加一個引用計數器稚照,每當有一個地方引用它時蹂空,計數器值就添加1,反之減1果录;如果對象的引用計數器值為0上枕,說明該對象不被任何地方引用。
算法優(yōu)點:實現簡單弱恒,判斷效率高辨萍,可以立即回收垃圾(一個對象的引用計數歸零的那一刻即是它成為垃圾的那一刻,同時也是它被回收的那一刻)返弹,沒有暫停時間锈玉。
算法缺點:每次賦值操作的時候都要做相當大的計算爪飘,尤其這里面還有遞歸調用;無法解決對象相互引用的問題拉背,因此在主流的Java虛擬機中并沒有采用這種算法來管理內存悦施。
計數器的值的變化是由mutator引起的,比如以下代碼去团,該例子來源 (引用計數算法 https://zhuanlan.zhihu.com/p/27939756)
A objA = new A();
B objB = new B();
objA.ref = objB;
objA與objB的引用分析如圖
objA與objB的引用計數器本來均為1抡诞,但是 objA.ref也引用了objB,因此objB的計數器值為2.
如果我們添加這樣的代碼
objA.ref = null土陪;
那么objA.ref 沒有引用objB昼汗,objB的計數器值就改為為1
那如果說objB也有一個字段引用到了objA呢?
A objA = new A();
B objB = new B();
objA.ref = objB;
objB.ref = objA;
兩個的引用分析圖
那么這兩個對象的引用計數就都是1鬼雀,在使用引用計數算法時就無法將這連個對象回收顷窒,產生循環(huán)引用問題。
二源哩,可達性分析算法(Reachability Analysis)
在Java虛擬機中使用的是這種算法來進行垃圾收集鞋吉。
算法原理:將一系列稱為"GC Roots"的對象作為起始點,從這些節(jié)點開始往下搜索励烦,搜索走過的路徑稱為路徑鏈(Reference Chain)谓着,當一個對象到 GC Roots 沒有任何引用鏈連接,那么說明GC Roots到此對象不可達坛掠,說明此對象不可用赊锚。
比如圖中的Object5,Object6屉栓,Object7 是判定可以回收的對象舷蒲。(圖來源: 周志明的《深入理解Java虛擬機》)
在Java語言中,可作為GC Roots對象的由下面幾種:
①虛擬機棧(棧幀中的本地變量表)中引用的對象友多。
②方法區(qū)中的類靜態(tài)屬性引用的對象牲平。
③方法區(qū)中的常量引用對象。
④本地方法棧中JNI(Native方法)引用的對象域滥。
關于GC Roots的正確定義和在 Java可達性分析算法會不會出現循環(huán)引用問題纵柿?https://www.zhihu.com/question/29218534/answer/43580432 中 對 GC Roots有這樣的解釋:
GC Root在對象圖之外,是特別定義的“起點”骗绕,不可能被對象圖內的對象所引用藐窄。
一個常見的誤解是以為GC Root是一組對象。
實際情況是GC Root通常是一組特別管理的指針酬土,這些指針是tracing GC的trace的起點荆忍。它們不是對象圖里的對象,對象也不可能引用到這些“外部”的指針,所以題主想像的情況無法成立刹枉。
另外叽唱,tracing GC能正確處理循環(huán)引用,保證每個活對象只會被訪問一次就能確定其存活性微宝。對象圖里是否存在循環(huán)引用棺亭,tracing GC都能正確判斷對象的存活與否。
相對于引用計數算法蟋软,可達性分析算法的優(yōu)勢是避免了引用計數算法中的循環(huán)引用問題镶摘。
接下來,我們根據這兩種算法岳守,具體分析兩種算法是如何維護所有對象引用的
(參考文章https://www.zhihu.com/question/21539353/answer/95667088)
按照我們上面說的凄敢,采用引用計數算法需要在每個實例對象創(chuàng)建之初,通過更改計數器上的引用次數來判斷該對象是否可回收湿痢;而使用可達性分析算法時涝缝,每次GC時,需要遍歷整個GC根節(jié)點來判斷是否回收譬重。
public class Client {
public static void main(String[] args) {
GcObject obj1 = new GcObject(); // Step1
GcObject obj2 = new GcObject(); // Step2
obj1.instance = obj2; // Step3
obj2.instance = obj1; // Step4
obj1 = null; // Step5
obj2 = null; // Step6
}
}
class GcObject{
public Object instance = null;
}
根據我們上面說明的拒逮,如果采用引用計數算法,最終obj1 與 obj2 兩個實例相互引用臀规,引用計數器上都為1滩援,都不會被GC回收
使用引用計數算法 JVM分析圖
Step1:obj1指向堆中的 GcObject實例1,此時 GcObject實例1的引用計數器值由0變?yōu)?以现;
Step2:obj2指向堆中的 GcObject實例2狠怨,此時 GcObject實例2的引用計數器值由0變?yōu)?;
Step3:實例1中的instance指向 GcObject實例2邑遏, GcObject實例2的引用計數器值由1變?yōu)?;
Step4:實例2中的instance指向 GcObject實例1恰矩, GcObject實例1的引用計數器值由1變?yōu)?记盒;
執(zhí)行到這里,GcObject實例1和GcObject實例2的引用計數器值均為2外傅。
然后執(zhí)行Step5纪吮,Step6
Step5:obj1不再指向堆中的 GcObject實例1, GcObject實例1的引用計數器值由2變成1萎胰;
Step6:obj2不再指向堆中的 GcObject實例2碾盟, GcObject實例2的引用計數器值由2變成1;
到這一步技竟,GcObject實例1和GcObject實例2的引用計數器值均為1冰肴,GC無法回收。但是明顯這兩對象已經沒啥用處,是可以回收的熙尉,所以產生了循環(huán)引用問題联逻,嚴重還會導致內存泄漏的問題。
使用可達性分析算法 JVM分析圖
圖中reference1检痰,reference2包归,reference3都是GC Roots,根據路徑鏈(Reference Chain)來判斷:
reference1 ---> 對象實例1
reference2 ---> 對象實例2
reference3 ---> 對象實例4 ---> 對象實例6
明顯對象實例3铅歼,對象實例5之間雖然存在引用公壤,但是均與GC Roots無可達性,所以兩個依舊可以回收的垃圾對象椎椰。