深刻學習Java虛擬機——垃圾收集器與內存分配策略

垃圾回收操做的步驟:首先肯定對象是否死亡,而後進行回收java

1. 如何判斷對象是否死亡

1.1 引用計數法

    1.引用計數法:給對象添加一個引用計數器,每當有一個地方引用它時,計數器值就加1,當引用失效時就減1,任什麼時候刻,計數器爲0的對象是不可能在被使用的。算法

    2.優缺點:優勢是實現簡單,斷定效率高;缺點是很難解決對象間相互循環引用的問題,因此現在的主流Java虛擬機都沒使用該方法進行管理內存。好比如下代碼數組

/**
 * 
 * @ClassName:ReferenceCountGC
 * @Description:引用計數法沒法解決的對象間互相循環引用的問題
 * @author: 
 * @date:2018年7月29日
 */
public class ReferenceCountGC {
	public Object obj;
	public static void main(String[] args) {
		ReferenceCountGC a=new ReferenceCountGC();
		ReferenceCountGC b=new ReferenceCountGC();
		a.obj=b;
		b.obj=a;
		a=null;
		b=null;
		
		//假設此處進行GC,若虛擬機採用引用計數法,則沒法回收a,b兩個對象
		System.gc();
	}
}

1.2 可達性分析算法(根追蹤算法)

    1. 可達性分析算法:經過一系列的「GC Roots」的對象爲起點,從這些節點開始往下搜索,搜索所走過的路徑稱爲引用鏈,當一個對象到GC Roots沒有任何引用鏈相連,則證實該對象時不可用的。如下圖爲例,安全

對象obj1,2,3,4到GC Roots都是可達的,因此這四個對象都是不可回收的,而obj5,6,7雖然有引用關係,但沒法到達GC Roots,因此他們將會被斷定爲可回收對象。數據結構

    2. 可做爲GC Roots的對象包括如下幾種:多線程

(1)虛擬機棧中的引用的對象併發

(2)方法區中靜態屬性引用的對象app

(3)方法區中常量引用的對象ide

(4)本地方法棧引用的對象ui

1.3 再談引用

    1. 引用分類:

(1)強引用:相似於 Object obj=new Object() 這類的引用,只要強引用還存在,垃圾收集器永遠不會回收該類引用的對象。

(2)軟引用:用來描述一些好有用但並不是必需的對象,在系統將要發生內存溢出異常以前,會把這些對象進行回收,若是此次回收以後尚未足夠的內存就會發生內存溢出異常。JDK提供了SoftReference類來實現軟引用。

(3)弱引用:用來描述非必需對象,但它的強度比弱引用更弱一些,被弱引用關聯的對象只能生存到下一次垃圾收集發生以前,不管內存是否足夠,都會回收掉只被弱引用關聯的對象。JDK使用WeakReference類來實現弱引用。

(4)虛引用:他是最弱的一種引用關係,一個對象是否有虛引用的存在,徹底不會對其生存時間產生影響,也沒法經過一個虛引用來取得一個對象實例,一個對象設置虛引用關聯的惟一目的就是能在這個對象被收集器回收時收到一個系統通知。Jdk中使用PhantomReference類來實現虛引用。

1.4 對象是否死亡

    1. 即便在可達性算法分析中不可達的對象,也並非直接被斷定爲死亡,而是進行一次標記,要真正肯定一個對象是否死亡,至少須要兩次標記過程:若是對象在進行可達性分析後發現沒有與GC Roots相鏈接的引用鏈那麼他將會被第一次標記並進行第一次篩選,篩選的條件是該對象是否有必要執行finalize()方法,對象沒有覆蓋finalize()方法或finalize()方法已經被調用過,則都沒有必要執行;若是被斷定爲有必要執行,則該對象會被放置在一個隊列中,並在稍後由一個由虛擬機自動創建的、低優先級的finalizer線程去執行該隊列中全部對象的finalize()方法,虛擬機會執行該對象的finalize()方法,但不會保證等待它執行結束,由於若是執行對象的finalize()方法時很是緩慢或發生死循環就有可能致使該隊列中的其餘對象處於等待中,甚至致使虛擬機崩潰。finalize()方法的執行是對象逃脫死亡的最後一次機會,在執行finalize()方法後,GC將對隊列中的對象進行第二次小規模標記。若是在finalize()方法執行時從新創建與引用鏈上的任意一個對象創建關聯便可避免回收,不然若是在這次finalize()執行後仍沒有逃脫標記隊列,那麼基本就會被回收。對象自我拯救實例代碼以下

public class FinalizeEscapeGC {
	public static FinalizeEscapeGC SAVE_HOOK=null;
	public void isAlive(){
		System.out.println("Object is alive");
	}
	//重寫finalize()方法,第一次標記時使虛擬機斷定該對象須要執行finalize()方法
	@Override
	protected void finalize() throws Throwable {
		// TODO Auto-generated method stub
		super.finalize();
		System.out.println("finalize() excute");
		FinalizeEscapeGC.SAVE_HOOK=this;//此行代碼對該對象進行的拯救
	}
	public static void main(String[] args) throws InterruptedException {
		SAVE_HOOK=new FinalizeEscapeGC();
		
		//初次拯救:成功
		SAVE_HOOK=null;
		System.gc();
		Thread.sleep(1000);
		if(SAVE_HOOK!=null){
			SAVE_HOOK.isAlive();
		}else{
			System.out.println("Object is dead");
		}
		
		//第二次拯救:失敗
		SAVE_HOOK=null;
		System.gc();
		Thread.sleep(1000);
		if(SAVE_HOOK!=null){
			SAVE_HOOK.isAlive();
		}else{
			System.out.println("Object is dead");
		}
	}
}

運行結果:
finalize() excute
Object is alive
Object is dead

注意:每一個對象的finalize() 方法只會被自動調用一次,在下一次回收時不會被執行,因此在上述代碼中第一次拯救成功,第二次拯救失敗。對於finalize()這個方法不推薦使用,或者說禁止使用,更推薦使用try-finally或者其餘方式。

1.5 回收方法區

    1. 方法區(在某些虛擬機中被稱之爲永久代)的垃圾收集主要回收兩部份內容:廢棄常量與無用的類。

(1)回收廢棄常量與回收堆中的普通對象相似,以常量池中字面量的回收爲例,假如一個字符串「abc」進入了常量池,但當前系統中沒有任何一個String對象是叫作「abc」的,換句話說,也就是沒有任何String對象引用常量池中的「abc」常量,也沒有其餘地方引用這個字面量,若是此時發生內存回收,並且有必要的話,這個「abc」就會被清理出常量池。常量池中其餘的類(接口),方法,字段的符合引用也與此相似。

擴展:關於String的建立

(1)String str = "abc"建立對象的過程

1 首先在常量池中查找是否存在內容爲"abc"字符串對象

2 若是不存在則在常量池中建立"abc",並讓str引用該對象

3 若是存在則直接讓str引用該對象

至 於"abc"是怎麼保存,保存在哪?常量池屬於類信息的一部分,而類信息反映到JVM內存模型中是對應存在於JVM內存模型的方法區,也就是說這個類信息 中的常量池概念是存在於在方法區中,而方法區是在JVM內存模型中的堆中由JVM來分配的,因此"abc"能夠說存在於堆中。通常這種狀況下,"abc"在編譯時就被寫入字節碼中,因此class被加載時,JVM就爲"abc"在常量池中 分配內存,因此和靜態區差很少。

(2)String str = new String("abc")建立實例的過程

1 首先在堆中(不是常量池)建立一個指定的對象"abc",並讓str引用指向該對象

2 在字符串常量池中查看,是否存在內容爲"abc"字符串對象

3 若存在,則將new出來的字符串對象與字符串常量池中的對象聯繫起來

4 若不存在,則在字符串常量池中建立一個內容爲"abc"的字符串對象,並將堆中的對象與之聯繫起來

(3)String str1 = "abc"; String str2 = "ab" + "c"; str1==str2是ture

是由於String str2 = "ab" + "c"會查找常量池中時候存在內容爲"abc"字符串對象,如存在則直接讓str2引用該對象,顯然String str1 = "abc"的時候,上面說了,會在常量池中建立"abc"對象,因此str1引用該對象,str2也引用該對象,因此str1==str2

(4)String str1 = "abc"; String str2 = "ab"; String str3 = str2 + "c"; str1==str3是false

是由於String str3 = str2 + "c"涉及到變量(不全是常量)的相加,因此會生成新的對象,其內部實現是先new一個StringBuilder,而後 append(str2),append("c");而後讓str3引用toString()返回的對象

(2)回收無用類:類須要知足3個條件才能算是無用類,

  1. 該類的全部的實例都已經被回收,也就是說堆中不存在該類以及其子類的任何對象
  2. 加載該類的ClassLoader已經被回收
  3. 該類對應的java.lang.Class對象沒有在任何地方被引用,沒有在任何地方經過反射來訪問該類

知足以上三個條件就是無用類,此時虛擬機能夠對其進行回收,但不是必須回收。在大量使用反射、動態代理、CGLib等頻繁定義ClassLoader的場景都須要虛擬機具有類卸載功能,保證永久代不會溢出。

2. 對象的回收——垃圾收集算法

2.1 標記-清除算法(Mark-Sweep)

    1. 算法思想:分爲兩個階段,標記和清除;首先對要進行回收的對象進行標記,而後清除。

    2. 缺點:

(1)效率低,不管是標記過程仍是清除過程效率都很低。

(2)浪費內存空間,標記清除後會形成大量的不連續的內存碎片,致使沒法爲後續分配較大內存的對象時沒法分配,從而引發又一次的垃圾清理動做。

2.2 複製算法(Copying)

    1. 算法思想:將內存分爲大小相等的兩塊,每次只使用其中一塊。當正在使用的這塊內存即將用完時,就將全部存貨的對象複製到另外一塊內存中,而後將使用過的上一塊內存所有清空。

    2. 優缺點:優勢是效率相較於標記-清除算法較高,也不會存在大量內存碎片的狀況,只需移動堆頂指針,順序分配內存便可,實現簡單。

缺點是對內存空間消耗大,可以使用內存僅爲原來的一半,內存代價高。

    3. 應用:商業虛擬機大多選用該算法對堆中的新生代中的對象進行回收。對於新生代區域中的對象,幾乎98%都是「朝生夕死」的,因此不須要按照1比1劃份內存空間,而是將新生代的內存空間劃分爲較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor。當回收時,將Eden區和Survivor區存活的對象一次性複製到另外一塊Survivor空間上,最後清理掉Eden和剛纔用過的Survivor空間。HotSpot虛擬機默認的Eden和Survivor區的大小比例時8:1,也就是說新生代中可用內存空間爲整個新生代的90%。若是另外一塊Survivor空間沒有足夠的內存空間存放上一次垃圾回收重新生代中存活的對象時,這些對象將直接經過分配擔保機制進入老年代。

2.3 標記-整理算法(Mark-Compact)

對於新生代所採用的的複製算法,在對象存活率較高時由於須要大量的複製操做而致使效率變低,而且還須要額外的空間進行分配擔保避免浪費50%的新生代內存空間。而爲了應對老年代區域存活率較高的特色,甚至是被使用內存中全部對象都所有存活的極端狀況,因此不採用此算法。

    1. 算法思想:首先是標記全部的可回收對象,而後將全部的存活的對象向同一端移動,保證全部的存活對象所佔內存空間都是連續的時候,直接清理邊界之外的內存。

2.4 分代收集算法

    1. 算法思想:也就是依據對象的存活週期將內存分爲幾塊。通常是吧Java堆分爲新生代、老年代和永久代,永久代不作討論。對於新生代和老年代分別採用不一樣的收集算法,以此保證垃圾收集的高效性。好比新生代中每次垃圾收集時都會有大量的對象死去,只有少許對象存活,那麼就用複製算法。而老年代中對象存活率高並且沒有額外空間對它進行分配擔保,因此就必須使用標記-整理或標記-清除算法。

3.  垃圾收集器——對GC相關算法的實現

3.1 可達性分析算法中枚舉根節點

    1. 在可達性分析算法中須要從GC Roots節點找引用鏈,而能夠做爲GC Roots的節點主要是在全局性的引用(常量,靜態屬性等)以及執行上下文(棧楨中的本地變量表)中。但若是有方法區達到幾百兆內存,此時在逐個檢查引用,那麼將會消耗大量時間。

    另外,GC耗時的另外一個體現爲GC停頓,在GC工做正在進行時,Java虛擬機必須終止其餘全部的Java執行線程,隨着堆的擴大這個暫停時間也會越久,由於可達性分析工做必須在一個能確保一致性的快照中進行,「一致性」指的是在整個分析期間不能夠出現對象引用關係處於不斷變化的狀況。因此對於 System.gc()方法時禁止在程序中使用的,由於顯式聲明是作堆內存全掃描,也就是 Full GC,是須要中止全部的活動的,也就是上面所說的終止其餘全部線程,對於程序是沒法接受的。

    2. HotSpot虛擬機使用一組稱爲OopMap的數據結構來直接得知哪些地方存放對象引用,在類加載完成的時候,HotSpot就把對象內什麼偏移量上是什麼類型的數據計算出來,在JIT編譯過程當中,也會在特定位置記錄下棧和寄存器中哪些位置是引用。

3.2 垃圾收集器

    1. 收集算法是內存回收的方法論,而垃圾收集器就是內存回收的具體實現。對於不一樣的虛擬機可能會有不一樣的收集器實現,對於HotSpot虛擬機,其總共有6種垃圾收集器用於不一樣分代的垃圾收集。以下圖所示,兩個收集器之間的連線表示能夠搭配使用,所處區域表示用於老年代或新生代。

    沒有最好的收集器,也沒有萬能的收集器,只有最合適的收集器。

並行垃圾收集器:指多條垃圾收集線程並行工做,但用戶線程處於等待狀態

併發垃圾收集器:指用戶線程與垃圾收集線程併發執行或並行執行,用戶程序繼續執行,而垃圾收集線程運行在另外一個CPU上

    2. Serial收集器:單線程收集器,也就是說當它在進行垃圾收集時必須暫停其餘全部線程,直到該收集器的線程執行結束,該動做由虛擬機後臺自動執行,用戶不可見。

(1)採用算法:新生代使用複製算法,老年代使用標記-整理算法。

(2)優缺點:在單線程狀況下(也就是隻有垃圾收集器線程執行),該收集器具備簡單高效的特色,但問題就是GC時致使全部應用程序的暫停,因此在後來的收集器就出現了併發收集器,使暫停時間儘可能縮短,但沒法徹底消除。

    3. ParNew收集器:Serial收集器的並行版,可使用多條線程收集垃圾,其他行爲與Serial收集器徹底相同,好比GC暫停,控制參數設置,應用的收集算法,對象分配策略和回收策略等。只有此收集器能夠與CMS收集器配合工做。

(1)優缺點:與Serial收集器相比,其最大的優勢就是可使用多條線程進行垃圾回收,在單線程中其效率不會比Serial收集器更好,因爲線程交互的開銷,在CPU較少的狀況下,都沒法保證能夠超越erial收集器。可是隨着CPU數量的增長,其效率確定要更好。缺點與Serial收集器相同,會發生GC時暫停現象。

    4. Parallel Scavenge收集器:專用於新生代的收集器,使用複製算法,而且是並行的多線程收集器,用於達到一個可控制的  用戶代碼運行時間/(垃圾收集時間+用戶程序運行時間),即吞吐量。虛擬機會依據當前系統運行狀態自動調整Parallel Scavenge收集器的控制參數,好比停頓時間或最大的吞吐量,這種調節被稱爲GC自適應調節策略,而該策略也是Parallel Scavenge收集器與PreNew收集器的區別。

    5. Serial Old收集器:該收集器是Serial收集器的老年代版本,即針對老年代進行收集。一樣爲單線程收集器,使用標記-整理算法。能夠與Parallel Scavenge收集器搭配使用或做爲CMS的後備方案。

    6. Parallel Old收集器:Parallel Scavenge收集器的老年代版本,使用標記-整理算法,並行收集器。

    7. CMS收集器:該收集器以獲取最短停頓時間爲目標的收集器,是併發收集器,使用標記-清除算法,分爲4個步驟,初始標記,併發標記,從新標記,併發清除,其中初始標記和從新標記仍會發生GC暫停。初始標記是進行標記GC Roots直接關聯的對象,併發標記是進行GC Roots根追蹤,從新標記是修正併發標記期間用戶程序繼續運行而致使的標記變更的對象的標記記錄。

(1)優缺點:

缺點有

  • CMS對CPU資源很是敏感
  • 沒法處理浮動垃圾致使可能出現「Concurrent Model Failure」失敗而致使另外一次Full GC的發生。因爲用戶程序的不斷運行,那麼就會有垃圾產生,但若是部分垃圾出如今標記以後就會致使CMS沒法處理,只有在下一次GC時進行處理,這一部分垃圾就被稱爲浮動垃圾。
  • 標記-清除算法致使的內存空間碎片化。

優勢是響應速度快,系統停頓時間短。

    8. 理解GC日誌:

    最前方的數字(好比  「 88.11:」)表示GC活動發生的時間,即從虛擬機啓動以來通過得秒數;

    緊跟的「GC」或「Full GC」表示此處垃圾回收的停頓類型,「Full GC」表示會暫停其餘全部用戶程序的線程活動,而用戶程序中顯示調用System.gc()方法也會致使Full GC,因此不建議調用該方法;

    而接下來的 [DefNew,[Tenured,[Perm分別表示在新生代,老年代或持久代進行的垃圾回收,但對於不一樣的收集器,對於對象分帶的名稱也可能不一樣;對於具體年代方括號內的如「333k->3k」表示GC前該內存區域使用量—>GC後該內存區域使用量,後再跟在這個區域GC所用時間;    

    而在方括號以外的表示GC前堆已使用空間—>GC後堆已使用空間。

4 內存分配與垃圾回收策略

4.1 內存分配

    對象的內存分配,絕大部分在堆上分配,主要分配與新生代的Eden區,若是啓動了T本地線程分配緩衝,按線程優先分配在TLAB上。少數狀況下分配在老年代,沒有絕對肯定的分配規則。其細節取決於當前使用的是哪種垃圾收集器組合。

        1. 如下有幾種較爲廣泛的對象分配策略:

  • 絕大部分新對象優先在新生代中的Eden區分配,當Eden沒有足夠空間進行分配時,虛擬機則會進行一次新生代GC。
  • 大對象直接進入老年代,大對象指大量連續內存空間的Java對象,好比極長的字符串或數組,在程序中更應該避免大量的」朝生夕死」的大對象。
  • 虛擬機爲每一個對象定義了一個對象年齡計數器,若是對象在Eden區出生並通過第一次新生代GC後仍然存活,則對象年齡就會加1,而且該對象能被Survivor區容納的話,就將還存活的對象將被複制到 Survivor 區(兩個中的一個),當對象每熬過一次新生代GC後,年齡就會加1,當對象年齡超過15(默認,能夠經過控制參數設置)時,將被複制「年老區(Tenured)」。
  • 動態對象年齡判斷,即虛擬機並不必定要求對象年齡必須達到最大年齡才能晉升老年代,當新生代中的相同年齡的存活對象的大小總和大於Survivor區的空間的一半時(也就是說正在使用的Survivor1區或Survivor2區內存空間不足時),年齡大於或等於該年齡的能夠直接進入老年代。
  • 空間分配擔保,在進行新生代GC以前,虛擬機會檢查老年代最大可用連續內存空間是否大於新生代全部對象總空間,若是成立那麼新生代GC就是安全的。可是可能會出現新生代GC後新生代中有大量的對象存活,致使Survivor區沒法容納,此時就須要老年代分配擔保,把Survivor區沒法容納的對象直接進入老年代,但前提是老年代自己具備足夠的空間,而是否採用這種方式承擔風險是能夠經過控制參數設置的。
相關文章
相關標籤/搜索