天天看點

Java基礎篇——GC垃圾回收機制

Java垃圾回收機制概念

       new的本質為malloc,即在記憶體中(注意是在記憶體中!)申請一段空間,申請之後必須釋放,否則會産生大量未被回收的記憶體碎片,進而導緻軟體崩潰。在C語言中存在有free()函數,C++中存在有析構函數,這兩種都為回收這樣的記憶體碎片提供了工具,但Java中有一樣更為NB的機制,也被js,GO等超靈活超優雅的程式語言争相模仿,即大名鼎鼎的垃圾回收機制,又稱GC(Garbage Collection)。剛剛入門的基礎Coder知道有這麼個東西就好,等你摸清了Java的基本使用發方法之後可以嘗試看一看GO語言實作JVM核心的方法。

        說句題外話,這在我心裡一直是一項非常非常有價值的技術,他将回收垃圾時要考慮的超多問題以某種基礎機制自動解決,足以展現程式語言設計團隊的博大人文情懷,而且其中涉及的知識、代碼結構、算法遠非常人可及,是以作為一種“工具”,使用時的注意事項就尤為重要,畢竟有利就有弊,就算是GC也不可能完美。

GC的執行時機

      目前有這麼幾種說法: 當new出的執行個體對象使用結束,方法中局部變量、局部對象在該方法被調用結束時可能會執行的方法(為什麼會是可能,下文會解釋)和 當記憶體警告與程式即将結束時執行,不頻繁,是以占用cpu時間片段不多。這之前科普一下finalize方法,每個對象都有這樣一個方法,使用時隻需覆寫即可。這是被gc調用的留給程式員的最後一根指揮棒,用來控制程式最後的動作。

       下面給一段代碼:

public class PreClass {
	private int num1;
	
	public PreClass() {
	}
	
	public void showStr(String str) {
		int length = str.length();
		System.out.println("這句話擁有[" + length + "]個字," + str);
	}
	
	public PreClass add(int num2) {
		this.num1 += num2;
		System.out.println(this.num1);
		
		return this;
	}

	@Override
	protected void finalize() throws Throwable {
		Thread.sleep(3000);
		System.out.println(this + "已被回收");
	}
}
           

        最後添加了一個3s的延時,如果該finalize被使用時就會有一個3s的延遲,之後便會輸出“xxx已被回收”的語句。

public class Demo {
	public static void main(String[] args) {
		PreClass pre = new PreClass();
		
		pre.add(12).add(15);
		pre.showStr("這居然是非常正好的十五字你敢信");
	}
}
           

        下面為輸出:

12
27
這句話擁有[15]個字,這居然是非常正好的十五字你敢信
           

        (注意:下面是我之前的看法)可見,"xxx被回收"的語句并未出現,這并不是說明不是所有的程式在運作之後變量都會被回收。而是在GC之後,Java的console視窗一樣被回收,導緻并不是沒被回收,而是回收了你也看不到。若想看到結果需要滿足一定條件,下面改一下Demo:

public class Demo {
	public static void main(String[] args) {
		PreClass pre = new PreClass();
		
		pre.add(12).add(15);
		pre.showStr("這居然是非常正好的十五字你敢信");
		System.out.println(pre.num1);
		pre = null;                        //人為将pre空間回收
		System.gc();
		
		try {
			Thread.sleep(6000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		
		System.out.println("6s後");        //在GC之後,看他會不會執行
	}
}
           
12
27
這句話擁有[15]個字,這居然是非常正好的十五字你敢信
[email protected]已被回收
6s後
           

        顯而易見,我之前的想法是錯誤的,并不是console也一樣被回收了,那麼又是怎麼一回事呢?經過研究查找資料後我打算這樣驗證:

private static void fun1() {
		PreClass pre = new PreClass();                //不解除對pre的引用
		pre.showStr("就是為了消除警告");        
		System.gc();
	}
	
	private static void fun2() {
		PreClass pre = new PreClass();
		pre.showStr("就是為了消除警告");
		
		pre = null;                                    //解除對pre引用
		System.gc();
	}
	
	private static void fun3() {
		{
			PreClass pre = new PreClass();
			pre.showStr("就是為了消除警告");        //經過一個{}後pre的壽命已經結束,如果此時釋放GC說明GC不是對應堆
		}
		
		System.gc();
	}
	
	private static void fun4() {
		{
			PreClass pre = new PreClass();        
			pre.showStr("就是為了消除警告");
		}
		
		int a = 2;                                    //pre生命周期結束後被a的指派替代,如果GC失敗說明棧幀中不存在覆寫關系,即
		System.gc();                                  //pre的生命周期結束後棧幀負責它的區域仍存活且仍舊被pre占據
	}
	
	private static void fun5() {
		fun1();                                        //進行對fun1()的調用,觀察棧幀壽命與什麼有關
                
		System.gc();
	}
           

結果

fun1();			//GC失敗
		fun2();			//GC
		fun3();			//GC失敗
		fun4();			//GC
		fun5();			//GC
           

       需要介紹一下 棧幀的概念:每當Java開啟一個線程時,JVM會為其開辟出一個Java棧,每當其調用一個方法時就向該棧壓入一個棧幀,即棧幀是Java棧的基本組成機關。熟悉棧這種資料結構的朋友都知道,棧,先入後出,存在棧頂指針ebp和棧底指針

esp,棧頂為高位址,每當需要釋放空間時将棧頂指針上移即可,棧幀也是如此釋放。

        棧幀中存在三部分,局部變量幀,操作數棧和幀資料區。局部變量幀負責存儲局部變量,按位元組存儲,即根據變量的聲明方式判斷其資料類型并壓入對應Java棧。操作數棧負責存儲臨時資料,如int a = 2等。幀資料區負責存儲用來進行常量池解析、方法傳回及異常派發等的基本資料。

        fun1中存在了對PreClass的引用,就需要在棧幀的局部變量幀壓入該變量且後期沒有釋放,便一直占據,導緻GC失敗,畢竟GC的本質是回收沒用的空間,棧還在用着他當然不會回收。fun2在結束了pre的引用後将其指派為null,即人為回收,GC判定無用,收回。是以這裡做兩點結論:

                1.變量不用後将其指派為null是最為安全的回收方法,無論GC不GC;

                2.GC的一種實作方法應該就是判定目前引用是否為null。

        fun3由于在引用後并未釋放pre導緻GC失敗,是以說明該棧仍被pre資料塊使用。fun4釋放成功是因為在pre資料塊運作結束後出現了新的int指派,導緻覆寫了目前資料塊,聯想棧幀結構,操作數棧可以覆寫局部變量區。fun5中調用fun1方法,fun1方法使用後壽命結束,無傳回值出棧,空間無用,是以GC成功。