SE高階(10):類加載機制—類加載器、類初始化和URLClassLoader

關於類加載機制的知識,先簡要了解一下虛擬機(JVM)。當我們使用eclipse或者命令行調用命令javac.exe運行Java程序時,系統就會啓動一個虛擬機把類加載進內存中,類加載的過程就是需要了解的類加載機制。

虛擬機特點:

  • 每啓動一次Java程序,都會單獨啓動一個JVM進程來運行;
  • JVM是一個進程,JVM的數據不是共享的;
  • Java程序結束後,JVM進程也會結束,同時JVM內存區的所有狀態全部丟失,類存儲在內存區中的數據會迴歸原始狀態。

        內存區數據表示的是數據存儲在內存中,而非對象持久化或者進行數據庫操作那種,那兩種的數據都不是存儲在內存區。例如使用office編寫文檔,不保存的話,當電腦死機或者軟件崩潰時會導致文檔內容消失。

         在兩個主類中分別寫兩個死循環,依次運行,在任務管理器中能看到兩個JVM進程(javaw.exe)在運行。

測試JVM結束後內存區狀態是否丟失案例:

public class Test01 {
	public static int num = 10;
	public static void main(String[] args) {
		num = 20;
		System.out.println("Test01的num值:" + num);
	}
}
public class Test02 {
	public static void main(String[] args) {
		Test01 t1 = new Test01();
		System.out.println("" + Test01.num);
	}
}
  • 案例說明:創建兩個主類,先運行Test01類,對num再次賦值20,打印輸出20,執行結束,JVM進程結束。運行Test02類,調用Test01的num值,重新初始化Test01類,打印輸出10。該案例簡單驗證了JVM結束後內存區狀態會丟失。
以上僅僅是簡單瞭解虛擬機,要想深入瞭解請看《深入理解java虛擬機》。



類的加載機制

 
        系統可能在第一次使用某個類時加載該類,也可能採用預加載機制來加載某個類。當主動使用某個類時,該類還未被加載到內存中,系統會通過加載、連接、初始化三個 階段對該類進行初始化。 類的生命週期從被加載到虛擬機內存中開始,到卸載出內存結束。其中的過程分爲七個階段,加載階段到初始化階段屬於類加載。生命週期如下:
  • 加載---->驗證---->準備---->解析----->初始化---->使用----->卸載(驗證、準備、解析屬於連接階段

加載階段

  • 通過一個類的全限定名來獲取定義此類的二進制字節流;
  • 將這個字節流所代表的靜態存儲結構轉化爲方法區的運行時數據結構;
  • 在java堆中生成一個代表這個類的Class對象,作爲訪問方法區中這些數據的入口(反射機制就需要使用Class實例)
  • 因爲沒有指明從哪裏獲取以及怎樣獲取類的二進制字節流,所以可以自定義類加載器來控制加載階段

連接階段

  • 類加載之後,系統會生成一個對應的Class對象,接着進入連接階段,該階段爲類變量分配內存和設置默認初始值,這個內存分配發生在方法區中。
  • 該階段還沒有對實例變量進行內存分配,實例變量會在對象實例化時隨着對象一起分配在JAVA堆中。

初始化階段

  • 初始化階段是對類變量進行初始化,如果聲明類變量時沒有指定初始值,那使用系統分配的默認值;
  • 類變量初始化有兩種方式:1、聲明類變量時指定值,2、在靜態代碼塊中爲類變量指定值;
  • 初始化一個類時,會先對該類的父類初始化,然後依次類推,直到所有父類資源都被初始化完成。所以Object類總是第一個初始化。

注意:靜態初始化塊和聲明類變量都是初始化語句,執行順序是誰在前面誰先執行,但兩者都是屬於類本身,所以比類實例化優先執行。

初始化語句執行順序案例:

public class Test01 extends Father{
	public static void main(String[] args) {
		new Test01();
	}
}
class Father{
	protected static int a;//未指定值
	public Father() {System.out.println("執行了父類構造器...");}
	//普通代碼塊
	{
		System.out.println("執行了父類普通代碼塊...");
	}
	//靜態初始化塊
	static {
		System.out.println("a的值:" + a);
		a = 10;
		System.out.println("靜態初始化塊之後:" + a);
	}
}
//a的值:0
//靜態初始化塊之後:10
//執行了父類普通代碼塊...
//執行了父類構造器...
  • 案例說明:如果不創建Test01()實例,那麼只有父類靜態代碼塊和類變量被初始化。Test01實例化則會調用父類構造器,這會導致Father類的初始化。輸出結果是先執行普通代碼塊,然後執行父類構造器(普通代碼塊在實例化中執行順序最優先,調用父類構造器時不會創建父類對象)

類初始化的時機

  • 創建類的實例:1.使用new來創建實例,2.使用反射方式來創建實例,3.通過反序列化生成實例;
  • 調用類方法(靜態方法);
  • 訪問類變量或者接口的類變量,或者爲其賦值;
  • 初始化某個類的子類,也會導致該類被初始化;
  • 調用java.exe命令運行某個類,該類會被優先初始化。

初始化注意點:1.訪問常量不會導致初始化,因爲常量在編譯時就已經確定值了。但如果常量的值是運行時才被確定,就會導致類初始化;2.使用反射獲取某個類或者接口的Class實例會導致類初始化。例如Class.forName()方法對數據庫驅動類初始化。

個人理解:執行的操作需要以類存在作爲前提就會導致類初始化。例如執行對象實例化、調用類變量、反序列化等操作,如果類不存在就會出錯。


類加載器

       類生命週期中的加載階段就是由類加載器來完成,是類加載機制的核心。類加載器負責從文件系統、網絡或任何其它資源中加載class文件到內存中,並生成對應的Class實例,只要一個類被加載到JVM中,就不會重複載入。注意:同一個類使用不同的類加載器加載,這兩個類是不相同、互不兼容的。
Java虛擬機運行時,默認有三個類加載器:
  1. Bootstrap   ClassLoader(原生類加載器)-----------加載路徑:JRE/lib/rt.jar
  2. Extension   ClassLoader(擴展類加載器)-----------加載路徑:JRE/lib/ext/*.jar或者任何指向java.ext.dirs的路徑
  3. Application ClassLoader(應用類加載器)-----------加載路徑:classpath環境變量指定的jar或目錄。未指定classpath則使用當前類路徑。
       除了默認的三種類加載器,我們還可以通過繼承ClassLoader類創建類加載器,自定義類加載器的父加載器是Application ClassLoader,應用類加載器的父加載器是擴展類加載器,依次向上類推。
      ExtClassLoader和AppClassLoader都是Java實現的,而Bootstrap類加載器是C/C++語言編寫的,內嵌到虛擬機中,它是Java的頂級類加載器,所以獲取Bootstrap加載器是返回null。

獲取默認類加載器案例:

	public static void main(String[] args) {
		//獲取當前類
		System.out.println("Test02的類加載器:" + Test02.class.getClassLoader());
		ClassLoader cl = ClassLoader.getSystemClassLoader();
		//遞歸獲取所有類加載器
		while(cl != null) {
			System.out.println(cl);
			cl = cl.getParent();
		}
	}
  • 案例說明:打印輸出ExtClassLoader和AppClassLoader,但沒有Bootstrap加載器,因爲Bootstrap不是Java實現的,返回null值。

加載器遵循原則:

  1. 父類委託:類加載器加載一個類時,會委託它的上層類加載器來對其加載,一直往上委託到Bootstrap類加載器,當所有上層加載器都不能加載該類時,纔會從自身的類路徑中去加載類文件
  2. 全盤負責:某個類被類加載器加載成功後,該類中引用的其他類也會被同一個類加載器所加載。保證加載的類屬於同一個。
  3. 單一性:父加載器加載過的類不能被子加載器加載第二次。

注意:自定義類加載器可以重寫loadClass(),但違反了父類委託和單一性原則,不要這樣做。一般重寫的是findClass(),通過ClassLoader的源碼也可以看出該方法就是用來重寫的。

URLClassLoader類

除了三個默認類加載器和自定義加載器,Java還提供一個URLClassLoader類,該類能從遠程主機獲取Class文件加載類,也能通過本地來加載類。URLClassLoader加載Class文件無需包名,而使用Class.forName()加載類時需要加入包名。

使用URLClassLoader加載JDBC案例:
public class Test {
	private static Connection con = null;
	public static Connection getJDBC(String user, String pwd, String database) throws Exception {
		//定位本地驅動jar包
		URL[] urls = {new URL("file:///E://sqljdbc4.jar")};
		//屬性類設置數據庫的賬號密碼
		Properties pro = new Properties();
		pro.setProperty("user", user);
		pro.setProperty("password", pwd);
		//創建URLClassLoader對象
		URLClassLoader ucl = new URLClassLoader(urls);
		//加載驅動類(初始化),之後返回驅動類Class實例
		Class cla = ucl.loadClass("com.microsoft.sqlserver.jdbc.SQLServerDriver");
		//把該類對象轉成Driver
		Driver driver = (Driver)cla.newInstance();
		//連接驅動,返回Connection對象
		con = driver.connect(database, pro);
		System.out.println("連接成功!");
		return con; 
	}
	public static void main(String[] args) throws Exception {
		String url = "jdbc:sqlserver://localhost:1433;DatabaseName=TestDB"; //連接的數據庫
		Connection con = Test.getJDBC("sa", "system", url);
	}
}

關於Class.forName()的使用和了解     

Class.forName()加載的類要包含包名,不然找不到加載的類。
Class.forName()常用於加載JDBC的驅動類,除了之外還可以用於獲取一個類的Class實例。 
Class.forName()和使用加載器的loadClass()有所區別:類加載器的loadClass()方法只加載類,不會導致初始化,除非執行了引發初始化的操作。而Class.forName()會對類加載並初始化。





發佈了61 篇原創文章 · 獲贊 24 · 訪問量 6萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章