探祕Java類加載

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

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

宇宙萬物,通過抽象,都可納入相應的種類。不一樣種類之間,有着相對井然的分別。編程

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

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

類與類之間,有着不一樣的層級。安全

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

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

舉例來講,動物都須要吃東西來維持生命所需的能量,同是吃東西,不一樣種類的動物各有特色。編程語言

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

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

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

譬如,雖然同爲人類,廣泛具有說話的功能,可是每一個具體的我的在說話時,音色又各自不一樣。

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

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

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

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

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

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

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

因此,咱們就從類加載提及。

1、類的生命週期

想象一下,你在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執行垃圾回收,類在本次程序運行中的生命結束。

2、雙親委派

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類加載中採用的雙親委派模型。

3、自定義類加載器

 有的時候,咱們須要當前程序之外的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文件。

4、深刻講解反射

 反射是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操做程序動態運行中的類模板。

5、對象的生命週期

在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虛擬機就能夠直接將該對象回收了。

相關文章
相關標籤/搜索