垃圾收集器與內存分配策略(深入理解Java虛擬機筆記)

目錄

概述

那些java堆對象需要回收?

引用計數算法和可達性回收算法

StrongReference、SoftReference、WeakReference、PhantomReference

不可達對象一定要“死”嗎

回收方法區

垃圾回收算法

標記-清除算法

複製算法

標記-整理算法

分代收集算法

垃圾收集器

HotSpot垃圾收集算法實現的注意事項

內存分配和回收策略


概述

程序計數器、虛擬機棧(棧幀:局部變量表、操作數棧)、本地方法隨線程而生滅;棧幀隨方法的出入棧而生滅。這幾個區域的內存和回收都具有確定性,不需要過多考慮回收問題。

而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個苛刻的條件:

  1. java堆中不存在該類的實例
  2. 加載該類的ClassLoader已被回收
  3. 沒有任何地方引用Class對象和該Class對象對應的其他反射成員

使用反射,動態代理,GCLib等的框架,都需要方法區內存的動態回收,以便方法區內存溢出。

垃圾回收算法

標記-清除算法

首先對象的標記判定階段由引用計數爲0和可達性算法完成,然後進入一一清除階段。不足有2:

  1. 標記和清除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時交換使用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章