深刻探究jvm之GC的算法及種類

1、GC基本概念java

  GC(Garbage Collection)垃圾收集,1960年最先在List中使用。在Java中GC回收的對象是堆空間和永久區,能夠有效避免程序員人爲形成內存泄漏問題。將堆空間和永久區沒有做用的對象進行釋放和回收。程序員

2、GC算法算法

一、引用計數法:jvm

  是一種老牌的垃圾回收算法,經過引用計算來回收垃圾,被COM、ActionScript三、Python所使用。ide

  引用計數法的實現很簡單,對於一個對象A,只要有任何一個對象引用了A,那麼A的引用計數器就會+1,當引用失效時,引用計數器就會-1。只要對象A的引用計數器的值爲0,那麼對象A 就不可能再被使用。函數

  

  引用計數法存在的問題:性能

  1)引用和去引用伴隨着加法,程序運行時隨時都發生着引用和去引用,影響性能;測試

  2)很難處理循環引用的問題,以下圖:優化

   中間節點未被根節點引用,但通過一次循環後引用計數爲2,仍然不會被清除,實際上應該被清除。this

二、標記-清除法:

  標記-清除算法是現代垃圾回收算法的思想基礎。標記-清除算法將垃圾回收分爲兩個階段:標記階段和清除階段。一種可行的實現是,在標記階段,首先經過根節點,標記全部從根節點開始的可達對象。所以未被標記的對象就是未被引用的垃圾對象。而後在清除階段,清除全部未被標記的對象。

 

   灰色對象都是根節點的可達對象,黑色對象未被根節點引用(直接或間接),白色部分爲空閒空間。清理階段會將黑色對象(未被標記)清理掉。

三、標記-壓縮法:

  標記-壓縮算法適合存活對象比較多的場合,如老年代。它在標記-清除算法的基礎上作了一些優化。和標記-清除算法同樣,標記-壓縮算法也首先須要從根節點開始,對全部可達對象作一次標記,但以後,它並不只僅簡單的清理未標記的對象,而是將全部存活的對象壓縮到內存的一端。以後清理邊界外全部的對象。以下圖:

四、複製算法

  與標記-清除算法相比,複製算法是一種相對高效的回收算法,但不適用於存活對象較多的場合,如老年代。

  它主要實現方案是將原有的內存空間分爲兩塊,每次只使用其中的一塊,在垃圾回收時,將正在使用的內存中的存活對象複製到未被使用的內存塊中,以後,清除正在使用的內存塊中全部對象,交換兩個內存塊的角色,完成垃圾回收。

  

  因而可知,複製算法最大的問題是空間浪費。Java中實際應用作了一些優化(分代思想,下面介紹),示例圖以下:

  新生代總空間爲15M,但可用空間只有13824K。其中12288K爲eden區空間,主要存放新產生的對象,1536K爲新生代兩塊相同空間(倖存代中from區和to區)中其中一塊。

3、分代思想:

  一、依據對象的存活週期進行分類,短命對象歸爲新生代,長命對象歸爲老年代;

  二、根據不一樣代的特色,選取合適的GC算法,少許對象存活,適合複製算法;大量對象存活,適合標記清理或者標記壓縮。

 4、可觸及性:

  全部的算法須要可以識別一個垃圾對象,所以引入了可觸及性的概念。

  一、可觸及的:從根節點能夠觸及到的對象;

  二、可復活的:一旦全部引用都被釋放,就是可復活狀態;由於有可能在finalize()方法中可能復活該對象;

  三、不可觸及的:在finalize()方法後,可能會進入不可觸及狀態,不可觸及的對象不可能被複活,此時能夠被GC回收。

  對象狀態轉換以下:

  1)首先定義一個可復活對象,在finalize()方法中復活一個對象:

public class CanReliveObj {
	public static CanReliveObj obj;
	@Override
	protected void finalize() throws Throwable {
	    super.finalize();
	    System.out.println("CanReliveObj finalize called");
	    obj=this;
	}
	@Override
	public String toString(){
	    return "I am CanReliveObj";
	}
}

  2)主函數方法以下:

public static void main(String[] args) throws
     InterruptedException{
  obj=new CanReliveObj();
  obj=null;   //可復活
  System.gc();
  Thread.sleep(1000);
  if(obj==null){
      System.out.println("obj 是 null");
  }else{
      System.out.println("obj 可用");
  }
  System.out.println("第二次gc");
  obj=null;    //不可復活
  System.gc();
  Thread.sleep(1000);
  if(obj==null){
    System.out.println("obj 是 null");
  }else{
    System.out.println("obj 可用");
  }
}

  3)運行結果以下:

CanReliveObj finalize called
obj 可用
第二次gc
obj 是 null

  注意,若是在調用finalize()方法後忘記釋放內存,那麼可復活對象就會一直存在於堆內存中,很容易形成內存溢出,所以是有風險的。在編碼的時候要注意如下幾點:

  1)避免使用finalize()方法,操做不慎可能致使錯誤;

  2)優先級很低,由於咱們不知道也沒法明確GC何時發生,使用finalize()方法反而增長了程序的不肯定性;

  3)可使用try-catch-finally來代替finalize()方法。

5、根對象

  什麼是根對象呢?主要有如下三類:

  一、棧中引用的對象;

  二、方法區靜態成員或者常量引用的對象(全局變量);

  三、JNI方法棧中引用的對象。

6、STOP-THE-WORLD

  STOP-THE-WORLD是Java中一種全局停頓的現象,此時全部Java代碼中止運行,native方法能夠運行可是沒法與jvm發生交互,發生這種狀況多半是因爲GC引發的。另外Dump檢查、死鎖檢查、堆Dump也有可能引發。

  一、GC爲何會引發全局停頓?

  類比在聚會時打掃衛生,聚會時很亂,又會產生新的垃圾,房間永遠不會被打掃乾淨,只有暫停一下聚會纔會將房間打掃乾淨。

  二、全局停頓的危害

  長時間中止服務,沒有響應;對於HA系統可能會引發主備切換。

  三、寫一個測試demo驗證STOP-THE-WORLD的存在

  1)聲明一個線程,每過1s打印10條記錄;

public static class PrintThread extends Thread{
	public static final long starttime=System.currentTimeMillis();
	@Override
	public void run(){
		try{
			while(true){
				long t=System.currentTimeMillis()-starttime;
				System.out.println("time:"+t);
				Thread.sleep(100);
			}
		}catch(Exception e){
			
		}
	}
}

  2)聲明另一個線程消耗資源,用來觸發GC(不斷的佔用內存,不斷的釋放變量來觸發GC)

public static class MyThread extends Thread{
	HashMap<Long,byte[]> map=new HashMap<Long,byte[]>();
	@Override
	public void run(){
		try{
			while(true){
				if(map.size()*512/1024/1024>=450){
					System.out.println(「=====準備清理=====:"+map.size());
					map.clear();
				}
				
				for(int i=0;i<1024;i++){
					map.put(System.nanoTime(), new byte[512]);
				}
				Thread.sleep(1);
			}
		}catch(Exception e){
			e.printStackTrace();
		}
	}
}

  3)啓動JVM的參數爲512m堆空間、串行回收器

-Xmx512M -Xms512M -XX:+UseSerialGC -Xloggc:gc.log -XX:+PrintGCDetails  -Xmn1m -XX:PretenureSizeThreshold=50 -XX:MaxTenuringThreshold=1

  4)觀察控制檯輸出和GC日誌,開始是1s打印10條記錄,後來產生了全局停頓。

  GC發生的時間與全局停頓的時間是吻合的。

相關文章
相關標籤/搜索