探祕Java類加載

Java是一門面向對象的編程語言。

面向對象以抽象爲基礎,有封裝、繼承、多態三大特性。

宇宙萬物,經過抽象,均可歸入相應的種類。不同種類之間,有着相對井然的分別。

Java中的類,便是基於現實世界中的類別抽象出來的。

類本身表示一類事物,是對這類事物共性的抽象與封裝。類封裝了一類事物的屬性和方法。

類與類之間,有着不同的層級。

以生物界中的分類爲例,遵循“界門綱目科屬種”的級別體系,人類(亦可稱爲“人種”)的層級體系是:動物界---脊索動物門---哺乳綱---靈長目---人科---人屬---人種。

從人種到動物界,依次繼承父類的共有屬性和方法,而且又獨具形態。

舉例來說,動物都需要吃東西來維持生命所需的能量,同是吃東西,不同種類的動物各有特點。

又譬如,動物界與植物界的一個關鍵區別是,能否移動。在動物界之中,都是移動,但是各子類的移動方式幾乎互不相同。

舉例來說,人通過走路、奔跑、攀爬等來移動,鳥通過飛翔、兩下肢等來移動,魚則通過在水中漂游來移動等。這使得動物的移動功能豐富多彩。

不僅如此,即便屬於同一種類的個體,在表現出來的公有功能方面,也是各不相同。

譬如,雖然同爲人類,普遍具備說話的功能,但是每個具體的個人在說話時,音色又各自不同。

我們生活的世界,就是這樣豐富多彩。既有共性的東西,又有具體不同的風格。

Java語言源於爲解決現實世界中各種各樣應用問題提供一整套解決方案。

所以,我們生活的現實世界,乃至整個宇宙,深深地映射入Java語言中。

世界與宇宙何其深邃與複雜,同樣,Java的博大精深不言而喻。

可以說,每個Java程序的運行,都是爲了解決某個或某種應用問題而生。

古人說“格物致知”,我們探祕Java程序運行的內在原理,有助於幫助我們深入認識Java世界的運行機制。

每個Java程序,都離不開類和對象。

所以,我們就從類加載說起。

一、類的生命週期

想象一下,你在Eclipse裏寫了一個Java程序,通過javac(Java編譯器),將Java源代碼編譯爲.class字節碼文件。

字節碼文件靜靜地躺在你的電腦磁盤裏,你要運行這個Java程序,就要去運行編譯後的字節碼文件。

加載.class字節碼文件到內存,形成供JVM使用的類,併到這個類從內存中銷燬,這便是類的生命週期。

總的來說,類的生命週期經過了如圖所示的階段:

 

 1.加載

關於加載,其實,就是根據.class文件找到類的信息將其加載到方法區中,然後在堆區中實例化一個java.lang.Class對象,作爲方法區中這個類信息的入口。

需要簡單科普一下的是:Java程序運行起來時成爲進程,操作系統需要爲該進程分配內存空間。Java程序的進程會將所分得的內存空間再予以分區,主要有棧區(存儲局部變量)、堆區(存儲創建的對象)、方法區(存儲類的方法代碼,以及類的靜態成員變量信息,還有常量池)、程序計數器(記錄線程的執行信息)、本地方法棧(與 操作系統底層交互時使用)。如圖所示:

2.鏈接

有的出處稱爲“連接”,若從英文單詞“linking”判斷,則翻譯爲“鏈接”比較合適。

鏈接一般會與加載階段和初始化階段交叉進行。

鏈接的過程由三部分組成:驗證、準備和解析。
(1)驗證:該階段是爲了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,並且不會危害虛擬機自身的安全。
(2)準備:主要是爲由static修飾的成員變量分配內存空間,並設置默認的初始值。默認初始值如下:

  ①8種基本數據類型的默認初始值是0。
  ②引用類型默認的初始值是null。
  ③對於有static final修飾的常量會直接賦值,例如:static final int x=10;則x默認就是10。
(3)解析:就是把常量池中的符號引用轉換爲直接引用,也就是說,JVM會將所有的類或接口名、字段名、方法名轉換爲具體的內存地址。

3.初始化
這是將靜態成員變量(也稱爲“類變量”)賦值的過程。

也就是說,只有static修飾的變量才能被初始化,執行的順序是:

父類靜態域(靜態成員變量)或者靜態代碼塊,然後是子類靜態域或者子類靜態代碼塊。

並非所有的類都會被初始化,只有那些被直接引用(主動引用)的類纔會被初始化。在Java中,類被直接引用的情況有:

  ①通過new關鍵字實例化對象、讀取或設置類的靜態變量、調用類的靜態方法;
  ②通過反射方式執行以上三種行爲;
     ③初始化子類的時候,會觸發父類的初始化;
     ④作爲程序入口直接運行時(也就是直接調用main方法);

除了以上4種情況,其他使用類的方式叫做被動引用,被動引用不會觸發類的初始化。

被動引用舉例:

(1)子類調用父類的靜態變量,子類不會被初始化,只有父類被初始化。對於靜態字段,只有直接定義這個字段的類纔會被初始化。

(2)通過數組定義來引用類,不會觸發類的初始化。

(3)訪問類的常亮,不會初始化類。

4.使用

類在使用過程中也存在三步:對象實例化、垃圾收集、對象終結。
(1)對象實例化:就是執行類中構造函數的內容,如果該類存在父類,JVM會通過顯式或者隱式的方式先執行父類的構造函數,在堆內存中爲父類的實例變量開闢空間,並賦予默認的初始值;然後,引用變量獲取對象的首地址,通過操作對象來調用實例變量和方法。
(2)垃圾收集:當對象不再被引用的時候,就會被JVM虛擬機標上特別的垃圾標識,在堆區中等待被GC回收。
(3)對象的終結:對象被GC回收後,對象就不再存在了,對象的生命也就走到了盡頭。
5.卸載
這是類的生命週期中最後的一步。

程序中不再有該類的引用,該類會被JVM執行垃圾回收,類在本次程序運行中的生命結束。

二、雙親委派

Java中的類加載存在層次性,一個重要的加載模型是雙親委派。

先來看Java中類加載器的層次體系:

什麼是類加載器呢?

簡而言之,類加載器可以將.class字節碼文件加載到JVM內存中的方法區形成類模板(或者稱爲該類的數據結構/鏡像),並在堆區中產生Class對象。

如果站在JVM的角度來看,只存在兩種類加載器:

1.啓動類加載器(Bootstrap ClassLoader):

也稱爲“根加載器”。由C++語言實現(針對HotSpot),負責將存放在<JAVA_HOME>\lib目錄或-Xbootclasspath參數指定的路徑中的類庫加載到內存中。

2.其他類加載器:

由Java語言實現,繼承自抽象類ClassLoader。如:
(1)擴展類加載器(Extension ClassLoader):負責加載<JAVA_HOME>\lib\ext目錄或java.ext.dirs系統變量指定的路徑中的所有類庫。
(2)應用程序類加載器(Application ClassLoader)。負責加載用戶類路徑(classpath)上的指定類庫,我們可以直接使用這個類加載器。一般情況下,如果我們沒有自定義類加載器,默認就是用這個加載器。通過在控制檯打印(System.out.println(System.getProperty("java.class.path"));),可以看到應用程序類加載器加載的路徑信息。如圖所示:

C:\Program Files\Java\jdk1.8.0_181\jre\lib\resources.jar;
C:\Program Files\Java\jdk1.8.0_181\jre\lib\rt.jar;
C:\Program Files\Java\jdk1.8.0_181\jre\lib\jsse.jar;
C:\Program Files\Java\jdk1.8.0_181\jre\lib\jce.jar;
C:\Program Files\Java\jdk1.8.0_181\jre\lib\charsets.jar;
C:\Program Files\Java\jdk1.8.0_181\jre\lib\jfr.jar;
C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\access-bridge-64.jar;
C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\cldrdata.jar;
C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\dnsns.jar;
C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\jaccess.jar;
C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\jfxrt.jar;
C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\localedata.jar;
C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\nashorn.jar;
C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\sunec.jar;
C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\sunjce_provider.jar;
C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\sunmscapi.jar;
C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\sunpkcs11.jar;
C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\zipfs.jar;
E:\workspace\eclipse\work_j2ee\java1_8\bin

雙親委派模型的工作過程是:

如果一個類加載器收到類加載的請求,它會先判斷這個類是否已經加載過,若已經加載過,就不再重複加載;若還未加載過,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器完成,若該類加載器無父類加載器,則將加載請求委派給根類加載器。每個類加載器都是如此(根類加載器除外)。只有當父類加載器在自己的搜索範圍內找不到指定的類時(即ClassNotFoundException),子類加載器纔會嘗試自己去加載。
Java在類加載中採用雙親委派模型有什麼好處呢?

使得Java類同其類加載器一起具備了一種帶優先級的層次關係,從而保證了程序運行中類的唯一性。

我們知道,程序運行起來時,每個類在堆內存中的Class對象僅有唯一的一個,不會引起程序運行中類的混亂,其根源在於Java類加載中採用的雙親委派模型。

三、自定義類加載器

 有的時候,我們需要當前程序以外的class文件,這時,我們就需要自定義類加載器,對相應的class文件進行加載。

自定義類加載器的步驟是:

1.繼承ClassLoader   

2.重寫findClass()方法

3.調用defineClass()方法

接下來自定義一個類加載器,加載E:/test下的Test2.class文件。

Test2.class文件的源代碼文件Test2.java:

package bwie2;

public class Test2 {	
	public void say() {
		System.out.println("Hello China");
	}	
}

 接着,創建自定義類加載器:

package bwie;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;

public class MyCloassLoader2 extends ClassLoader {
	private String classPath;// 要加載的類路徑

	public MyCloassLoader2(String classPath) {// 構造方法傳參
		this.classPath = classPath;
	}

	@Override
	protected Class<?> findClass(String name) throws ClassNotFoundException {// 查找類
		byte[] classData = getData(name);

		if (classData == null) {
			//若字節碼爲空,則拋出異常
			throw new ClassNotFoundException();
		} else {
			// defineClass,將字節碼轉化爲類
			return defineClass(name, classData, 0, classData.length);
		}
		//return super.findClass(name);
	}

	// 返回類的字節碼
	private byte[] getData(String className) {
		InputStream in = null;
		ByteArrayOutputStream out = null;
		String path = classPath + File.separatorChar + className.replace('.', File.separatorChar) + ".class";
		try {
			in = new FileInputStream(path);
			out = new ByteArrayOutputStream();
			byte[] buffer = new byte[1024];
			int len = 0;
			while ((len = in.read(buffer)) != -1) {
				out.write(buffer, 0, len);
			}
			in.close();
			out.close();
			return out.toByteArray();
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
		return null;
	}
}

 然後,通過測試類進行測試:

package bwie;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class Test {	
	public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, SecurityException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
		//自定義類加載器的加載路徑
		MyCloassLoader2 classLoader = new MyCloassLoader2("E:/test");
		
		//包名+類名
		Class<?> clazz = classLoader.loadClass("bwie2.Test2");		
		if(clazz!=null) {
			Object obj = clazz.newInstance();
			Method method = clazz.getMethod("say");
			method.invoke(obj);			
			System.out.println(clazz.getClassLoader().toString());
		}	
	}
}

 程序執行後,控制檯打印如圖所示:

可見,筆者使用自定義的類加載器MyCloassLoader2成功地加載了程序以外的class文件。

四、深入講解反射

 反射是Java語言中一個非常重要的機制。

程序員們一般都知道:通過反射,可以獲取類與對象的所有信息,執行若干操作(如創建對象,方法調用),還可以修改類的數據結構(如修改訪問權限)。

在Java中,反射對應的單詞是reflect。

提到反射,不免讓人霎時想起光的反射(Reflection of light)。

Java裏運用反射,是否與光的反射有關?這也涉及Java爲什麼要取名爲反射。

舉個例子來說,一個美女站在鏡子前,請問,鏡子裏的美女和鏡子前的美女,是否同一個美女?

答案是肯定的。

我們再來看Java程序的加載與運行。

一個被編譯爲.class字節碼文件的類,經過JVM的加載,在方法區中形成對應的類模板。

那麼請問,JVM加載出的類模板,與加載前的類,是不是同一個類?

答案是肯定的。

大家想一下:一個人站在鏡子前,通過光的反射,可以在鏡子裏產生一個鏡像。鏡像與鏡子前的人是同一個人。這是運用了光的反射規則。

實際上,我們能看到五彩繽紛的世界,一個重要原因是光的反射的存在。

光的反射外在表現爲一種現象,本質是一種機制和規則。

同樣,一個表現爲.class字節碼文件的類,經過JVM中的類加載器加載,在方法區中形成類模板,也相當於類的“鏡像”。

大家再想下:Java中,加載前、表現爲.class字節碼文件的類,與加載後、在方法區中形成的類模板,同屬於一個類,這與光的反射是不是有異曲同工之妙?

這也就是Java爲什麼將類加載後、在內存的方法區中形成類模板的機制,稱爲反射的緣由。

看來,Java語言的締造者不愧是大牛,將技術比喻得那麼貼切,又那麼接近生活!

大家還會看到,上圖中,堆區裏有個Class對象,類加載時會在堆區中產生Class對象。

程序加載運行時,一個類在內存中的Class對象與類模板都是唯一的。

程序中通過Class對象操作類模板。

可以說,程序中要運用反射,就離不開Class對象。那麼,Class對象究竟是什麼?

如果我們把JVM看作是人的話,對於程序員來說,通過閱讀Java源代碼,能夠了解一個類的數據結構,那麼,Java程序在運行中,JVM又是如何讀懂類的數據結構的呢?

這要歸功於類加載器加載class文件在方法區生成該類的模板。如果說,class文件靜態地存儲了類信息,類加載器加載出來的類模板相當於類在動態運行環境中的數據結構,JVM就是通過這個類模板來認識與操作這個類的。

編程語言實現了人機交互。Java語言也是如此。

我們要操控JVM虛擬機去操作內存中的某個類,應該怎麼辦呢?Java語言爲所有Java數據類型(基本數據類型與引用數據類型)均提供了class屬性,通過該屬性可以返回Class對象,這個Class對象是我們在程序中運用反射機制,是我們與JVM交互、指揮JVM去操作類模板的接口性工具。

機器懂的,我們未必懂。怎麼辦呢?找個中間人,通過中間人操作機器。這就好比,我們通過操作系統去操作電腦硬件那樣。

我們通過Class對象,指揮JVM操作程序動態運行中的類模板。

五、對象的生命週期

在Java中,對象的生命週期包括以下幾個階段:

1.  創建階段(Created)
2.  應用階段(In Use)
3.  不可見階段(Invisible)
4.  不可達階段(Unreachable)
5.  收集階段(Collected)
6.  終結階段(Finalized)
7.  對象空間重分配階段(De-allocated) 

如圖所示:

1.創建階段(Created)
在創建階段系統通過下面的幾個步驟來完成對象的創建過程:
    l  爲對象分配存儲空間
    l  開始構造對象
    l  從超類到子類對static成員進行初始化
    l  超類成員變量按順序初始化,遞歸調用超類的構造方法
    l  子類成員變量按順序初始化,子類構造方法調用
一旦對象被創建,並被分派給某些變量賦值,這個對象的狀態就切換到了應用階段。

2.應用階段(In Use)
對象至少被一個強引用持有着。

3.不可見階段(Invisible)
當一個對象處於不可見階段時,說明程序本身不再持有該對象的任何強引用,雖然這些引用仍然是存在着的。
簡單來說,就是程序的執行已經超出了該對象的作用域了。

比如,在使用某個局部變量count時,已經超出該局部變量的作用域(不可見),那麼就稱該變量count處於不可見階段。這種情況下,編譯期在編譯階段通常就會提示與報錯。
4.不可達階段(Unreachable)
對象處於不可達階段是指該對象不再被任何強引用所持有。
與“不可見階段”相比,“不可達階段”是指程序不再持有該對象的任何強引用,這種情況下,該對象仍可能被JVM等系統下的某些已裝載的靜態變量或線程或JNI等強引用持有着,這些特殊的強引用被稱爲”GC root”。這些GC root可能會導致對象的內存泄露,使得對象無法被回收。


5.可收集階段、終結階段與釋放階段

這是對象生命週期的最後一個階段:可收集階段、終結階段與釋放階段。

當對象處於這個階段的時候,可能處於下面三種情況:

(1)垃圾回收器發現該對象已經不可到達,則對象進入“可收集階段”。

(2)finalize方法已經被執行,則對象空間等待被垃圾回收器進行回收,即“終結階段”。

(3)對象空間已被重用,即“對象空間重新分配階段”。

當對象處於上面的三種情況時,該對象就處於可收集階段、終結階段與釋放階段了。JVM虛擬機就可以直接將該對象回收了。

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