目錄
StrongReference、SoftReference、WeakReference、PhantomReference
概述
程序計數器、虛擬機棧(棧幀:局部變量表、操作數棧)、本地方法隨線程而生滅;棧幀隨方法的出入棧而生滅。這幾個區域的內存和回收都具有確定性,不需要過多考慮回收問題。
而java堆區和 方法區,只有在運行期才決定分配了那些了內存?哪些內存需要回收?需要在何時回收?如何回收?
那些java堆對象需要回收?
引用計數算法和可達性回收算法
引用計數算法回收引用計數爲0的對象,但對循環引用無效。
從而補充可達性回收算法:
可作爲GCRoots的算法有以下幾種:
棧幀中局部變量表中引用的對象;方法區中類靜態屬性引用的對象;方法區中常量引用的對象;本地方法棧中引用的對象。
StrongReference、SoftReference、WeakReference、PhantomReference
滿足以上算法的可回收對象裏有一些“雞肋(食之無味,棄之可惜)”的對象,這些對象被回收我們感到真的有點可惜,所以題目中的引用“破土而出”:
強引用:類似這樣“Object obj = new Object()”的obj引用。
軟引用:它引用的對象在內存不夠用時回收。
弱引用:它引用的對象只能生存到下一次垃圾收集發生之前,且回收時內存是否足夠。
虛引用:它引用的唯一目的就是對象被回收時能收到一個系統通知。
不可達對象一定要“死”嗎
滿足回收算法的不可達對象要經歷2次標記纔會“死”去。
第一次:算法發現沒有與GC Roots相連接的引用鏈,它會被第一次標記,並接着篩選;
篩選:篩選條件是有無必要執行finalize方法;有,則加入F-Queue隊列,稍後虛擬機會自動建立低優先級的Finalizer線程去執行它(對象的finalize方法只會被jvm系統執行一次);沒有覆蓋finalize方法的、已執行過finalize方法的對象都是沒必要執行的對象。
第二次:會對第一次標記過的對象,包括F-Queue中的對象,做第二次標記,標記後被回收。
我們可以在finalize方法裏讓this對象再次關聯GC Roots類型的引用,可以從GC中“拯救”回來,例如:
public class FinalizeEscapeGC {
public static FinalizeEscapeGC SAVE_HOOK = null;
public void isAlive() {
System.out.println("yes, i am still alive :)");
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize mehtod executed!");
FinalizeEscapeGC.SAVE_HOOK = this;
}
public static void main(String[] args) throws Throwable {
SAVE_HOOK = new FinalizeEscapeGC();
//對象第一次成功拯救自己
SAVE_HOOK = null;
System.gc();
// 因爲Finalizer方法優先級很低,暫停0.5秒,以等待它
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("no, i am dead :(");
}
// 下面這段代碼與上面的完全相同,但是這次自救卻失敗了
SAVE_HOOK = null;
System.gc();
// 因爲Finalizer方法優先級很低,暫停0.5秒,以等待它
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("no, i am dead :(");
}
}
}
運行結果:
finalize mehtod executed!
yes, i am still alive :
no, i am dead :(
回收方法區
方法區的垃圾收集率低於java堆區。
回收的主要內容有:廢棄常量和無用的類。
方法區中的常量池裏的常量有字面量和符號引用,沒有任何地方引用這個字面量(例如“abc”這個字符串字面量,沒有任何String對象和其他地方引用),符號引用也通此理。
回收無用的類有3個苛刻的條件:
- java堆中不存在該類的實例
- 加載該類的ClassLoader已被回收
- 沒有任何地方引用Class對象和該Class對象對應的其他反射成員
使用反射,動態代理,GCLib等的框架,都需要方法區內存的動態回收,以便方法區內存溢出。
垃圾回收算法
標記-清除算法
首先對象的標記判定階段由引用計數爲0和可達性算法完成,然後進入一一清除階段。不足有2:
- 標記和清除2階段的效率都不高;
- 標記清除之後產生大量內存碎片,致使分配較大對象時無法滿足,從而導致一次垃圾回收行爲。
可能的算法執行過程如圖:
複製算法
爲了解決回收效率,將內存分爲等量的2分,一次使用其中一份,這份使用完時,把存活的對象複製到另一份上,再清除掉原來的那份以便下次使用。
優點:沒有內存碎片,實現簡單,運行高效;
不足:運行時內存縮小爲了原來的一半。
可能的算法執行過程如圖:
另,現在商用虛擬機都採用這種類似的複製算法來回收新生代,新生代中的大量對象(98%)存活時間很短,所以GC時(98%對象“已死”)需要複製到的空間(緊接着講到的Survivor2)並不需要太大,所以一般會把內存劃分爲Eden、Survivor1和Survivor2,它們依次佔比8:1:1,這樣運行時內存佔90%。
當然,我們不能保證每次都回收不多於10%的存活對象,所以多於10%的話,我們開啓擔保機制,讓本次多於10%的存活對象進入老年代,具體執行規則見後文內存分配和回收策略部分。
標記-整理算法
如果對象存活率高的話,複製算法會有較多的複製操作,從而gc效率會降低,一般不適用於老年代區域。因此根據老年代特點,本節算法“破冰而出”。
標記過程和“標記-清除”算法的標記過程一樣,但接着不直接清除標記的對象,而是所有存活的對象向內存的一端移動挨個排列,然後清除存活對象邊界外的內存。
可能的算法執行過程如圖:
分代收集算法
該算法把內存分爲新生代和老年代,且新生代劃分爲Eden、Survivor1和Survivor2幾個區域。複製算法可以在新生代 “施展拳腳”,老年代對象存活率高,無額外空間擔保分配,可以採用“標記-清除”和“標記-整理”算法回收。
垃圾收集器
以上垃圾收集算法是內存回收的方法論,而垃圾收集器是內存回收的具體實現。
Hotspot包含的垃圾收集器,如圖:
上圖的連線說明了7種收集器之中幾種可以搭配在新生代和老年代上進行內存回收。CMS和G1較複雜,其中G1可以同時收集新生代和老年代的垃圾。
具體原理見書。
HotSpot垃圾收集算法實現的注意事項
可達性對象判活算法中需要枚舉GC Roots對象,而GC Roots對象只要在全局性引用(常量對象或類型靜態對象)上和棧幀中的局部變量表中。而棧中的方法很多的情況下,站裏的引用也多,那麼要枚舉GC Roots對象必然會消耗很多時間,爲了解決這種情況,OopMap數據結構被設計出來承載GC Roots對象的引用,在類加載完成的時候,HotSpot就把對象內什麼偏移量上是什麼類型的數據計算出來,在JIT編譯過程中,也會在特定的位置記錄下棧裏和寄存器裏哪些位置是引用,這樣GC在掃描時就可以直接通過OopMap得知這些GC Roots對象了。
GC時,會“Stop The World”,至少在枚舉GC Roots時“Stop The World”。
要想GC,就需要讓所有用戶線程“跑”到安全點上暫停,這樣才能讓對象引用關係處於不再變化的情況,從而讓GC可以枚舉所有GC Roots找出不可達對象。
如果用戶線程處於sleep或blocked狀態,是無法讓他們“跑”到安全點上暫停的,所以針對這種情況引入安全區域的概念。
安全區域是指一段代碼片段之中,引用關係不會發生變化。
在用戶線程要離開Safe Region時,它要檢查GC是否已經完成了根節點枚舉(或者是整個GC過程),如果完成了,那線程就繼續執行,否則它就必須等待直到收到可以安全離開Safe Region的信號爲止。
內存分配和回收策略
新生代GC,例如,大多數情況下,對象在新生代Eden區中分配,當Eden區內存不夠時,發生Minor GC(新生代GC)。
老年代GC,英文稱:Major GC或Full GC,顧名生意發生在老年代。
JVM的啓動參數-XX:PretenureSizeThreshold意思是當對象需要分配的內存大於該值時,對象直接分配到老年代,該參數只對Serial和ParNew兩款收集器有效。
在第一次GC時,對象只會在Eden區和Survivor1區,Survivor2區是空的,緊接着Eden區中所有存活的對象都會被複制到Survivor2區, Survivor1區存活對象也會被複制到Survivor2區,並設置Survivor2區所有存活對象的年齡爲1,然後清理Eden區和Survivor1區。
下一次GC時,在Eden區存活的對象被複制到Survivor1區,而此時Survivor2區所有存活的對象根據他們的年齡值(此時年齡爲1)來決定去向,年齡達到一定值(年齡閾值,可以通過JVM啓動參數-XX:MaxTenuringThreshold來設置)的對象會被移動到年老代中,沒有達到閾值的對象會被複制到Survivor1區域,並設置Survivor1區所有存活對象的年齡爲原來年齡+1,最後清理Eden區和Survivor2區。若此時若MaxTenuringThreshold不等於1,Survivor2區所有存活的對象就都被複制到Survivor1區域。
同理,Survivor1區和Survivor2區在每次GC時交換使用。