《深入理解Java虛擬機器》讀書筆記-垃圾收集器與記憶體配置策略,
在堆裡存放著java世界中幾乎所有的對象執行個體,垃圾收集器在對堆進行回收前需要知道哪些對象還存活,哪些對象已經死去。那怎麼樣去判斷對象是否存活呢?
一、判斷對象是否存活演算法
1、引用計數法
實現思路:給對象添加一個引用計數器。每當有一個地方引用它時,計數器加1;引用失效時計數器減1。在任何時刻計數器為0的對象就是不可能再被使用的。
優點:實現簡單,效率高。
缺點:很難解決對象之間的相互循環參考。
2、可達性分析演算法
實現思路:通過GC Roots的對象作為起始點,從這些節點向下搜尋,搜尋走過的路徑成為引用鏈,當一個對象到GC Root沒有任何引用鏈相連時,則證明對象是停用。
優點:可以很好的解決對象相互循環參考的問題。
缺點:沒想到
二、在java中,哪些對象可以作為GC Roots呢?
1、虛擬機器棧(棧幀中的本地變數表)中引用的對象
2、方法區中靜態類屬性和常量引用的對象
3、本地方法棧中JNI(Native方法)引用的對象
三、對象標記回收過程
如果一個對象在可達性分析演算法中是不可達的,那是不是這個對象就一定會被回收呢?
答案是否定的,這些對象還有一次複活的機會。要真正宣告一個對象死亡,至少要經曆兩次標記過程:如果對象在進行可達性分析後發現沒有與任何的GC Roots相串連的引用鏈,那它會被第一次標記,且進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法。在什麼情況下不會執行對象的finalize方法呢?1、當對象沒有覆蓋finalize()方法。2、該對象的finalize()方法已經被虛擬機器調用過。如果一個對象被判定為有必要執行finalize方法,那這個對象會被放置在一個F-Queue的隊列中,等待虛擬機器自己建立的一個低優先順序的Finalizer線程去執行。finalize方法是這些對象逃脫死亡命運的最後一次機會,如果對象要在finalize中成功拯救自己,只要重新與引用鏈上的任何一個對象建立關聯即可,譬如把自己賦值給某個變數或者對象的成員變數。那在第二次標記是它將被移除“即將回收”集合。否則就只能等待著回收了。
但是,finalize方法運行代價高,不確定性大,無法保證各個對象的調用順序。在日常開發中強烈不建議使用這個方法,如果需要有“關閉外部資源”之類的工作,使用try-finally或者其他方式都可以做得更好更及時。
四、垃圾收集演算法
1、標記-清除演算法
實現思路:標記演算法實現很簡單,通過前面介紹的可達性分析演算法標記所有需要回收的對象,然後統一回收所有被標記的對象。它是最基礎的收集演算法,後續的收集演算法都是基於這種思路並對其不足進行改進而得到的。
缺點:效率不高、產生記憶體片段
2、複製演算法V1
實現思路:將記憶體按容量劃分為大小相等的兩塊,每次使用其中的一塊。當一塊的記憶體用完了,就將還存活的對象複製到另一塊上面,然後再把已使用過的記憶體空間一次清理掉。這樣每次都是針對整個半區的記憶體進行回收,不用考慮記憶體片段問題。
優點:簡單高效、不會有記憶體片段問題
缺點:記憶體會縮小為原來一半,代價高
3、複製演算法V2(新生代採用的演算法)
實現思路:替代原來將記憶體一分為二的方案,將記憶體分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次Eden使用其中的一塊Survivor。當回收時,講Eden和Survivor中還存活的對象一次性賦值到另外一塊Survivor空間上,最後清理掉Eden和剛才使用過的Survivor空間。HotSpot虛擬機器預設Eden和Survivor的大小比例是8:1(研究表明,新時代中98%的對象是朝生夕死的),也就是每次新時代中可用記憶體空間為整個新時代容量的90%。只有10%的記憶體會被浪費。但是,如果存活的對象佔用的記憶體大於新時代的10%怎麼辦?這就需要依賴其他記憶體(老年代)進行分配擔保了。
優點:改善了第二點中的缺點。
4、標記-整理演算法(老年代採用的演算法)
實現思路:過程與標記-清除演算法一樣,但後續步驟不是直接對可回收對象進行清理。而是讓所有的對象都向一端移動,然後直接清理掉端邊界以外的記憶體。