類加載
類的加載指的是將類的.class文件中的二進制數據讀入到內存中,將其放在運行時數據區的方法區內,然後在堆區創建一個java.lang.Class對象,用來封裝類在方法區內的數據結構。類的加載的最終產品是位於堆區中的Class對象,Class對象封裝了類在方法區內的數據結構,並且向Java程序員提供了訪問方法區內的數據結構的接口。
過程如下圖:
類的生命週期
它的整個生命週期包括:加載(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸載(Unloading)7個階段。類加載只包括前面五個階段。其中準備、驗證、解析3個部分統稱爲連接(Linking)。
加載,驗證,準備,初始化這幾個階段是按照順序開始的,解析則不一定,因爲當一個方法動態綁定時,那麼該方法是在運行階段完成的解析(也就是我們所說的多態)。同時這幾個階段只是按照順序開始,並不意味着當一個階段完成之後才能執行另一個階段,有時可能當一個階段還在執行之中就下一個階段就開始。比如在加載的過程中往往伴隨着驗證的過程。
加載
查找並加載.class文件中的二進制數據。
加載時類加載過程的第一個階段,在加載階段,虛擬機需要完成以下三件事情:
1、通過一個類的全限定名來獲取其定義的二進制字節流。
2、將這個字節流所代表的靜態存儲結構轉化爲方法區的運行時數據結構。
3、在Java堆中生成一個代表這個類的java.lang.Class對象,作爲對方法區中這些數據的訪問入口。
虛擬機並沒有指定.class文件的數據來源所以可以從下面幾個方面獲取:
1、 通過網絡加載.class文件
2、從zip,jar等歸檔文件中加載.class文件
3、從專有數據庫中提取.class文件
4、從磁盤中獲取
5、運行時生成,使用最多的就是動態代理技術
6、有其他文件生成,常見的就是jsp技術
驗證
驗證主要是驗證加載過來的二進制數據符合虛擬機的要求,並且不會損害虛擬機自身的安全。
驗證主要是從以下幾個方面進行驗證:
1、文件格式的驗證
驗證字節流是否符合Class文件的規範,同時還可以被當前版本的虛擬機所處理
2、元數據驗證
對加載到方法區的類的元數據進行語義分析。
3、字節碼驗證
通過對數據流和控制流進行分析,確保程序語義合法,合乎邏輯。
4、符號引用驗證
對常量池中的符號引用進行驗證。這個階段的驗證發生在解析的時候。
注意:
對於虛擬機的類加載機制來說,驗證階段是一個非常重要的、但不一定是必要(因爲對程序運行時沒有影響)的階段。如果所運行的全部代碼(包括自己編寫的以及第三方的包中)已經被反覆使用和驗證過,可以使用-Xverify:none參數來關閉大部分類的驗證,以縮短虛擬機類加載的時間。
準備
爲類變量分配內存並賦予初始值。
需要注意的是這裏指的變量是類變量(被static修飾的變量),類變量是存儲與方法區中,而不是實例變量。實例變量將會伴隨着對象的初始化在堆中分配內存。
其次,這裏值的賦予的初始值是零值。
不妨,我們來看看下面這個例子
public static int value = 123;
在準備階段被賦予的值是0而不是123,被賦予123是在初始化階段之後。
但是也會存在特殊的情況,某一個類變量被final關鍵字修飾,那麼這個字段的屬性表中就會存在ConstantValue屬性,那麼準備階段就會賦予ConstantValue指定的值,比如
public static final int value = 123;
那麼準備階段過後value的值是123。
解析
虛擬機將常量池中的符號引用轉換爲直接引用的過程。
解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符號引用進行。符號引用就是一組符號來描述目標,可以是任何字面量。這一個階段比較的特殊它可以發生在類加載階段,也可以發生運行時。類加載階段發生的解析我們稱作靜態解析,在類加載階段發生的解析叫做動態綁定。
初始化
根據程序員所寫的代碼來初始化類變量和其他靜態資源。
類初始化時機:只有當對類的主動使用的時候纔會導致類的初始化,類的主動使用包括以下幾種。
(1) 遇到new、getstatic、putstatic或invokestatic這4條字節碼指令時,如果類沒有進行過初始化,則需要先觸發其初始化。生成這4條指令的最常見的Java代碼場景是:使用new關鍵字實例化對象的時候,讀取或設置一個類的靜態字段(被final修飾、已在編譯期把結果放入常量池的靜態字段除外)的時候,以及調用一個類的靜態方法的時候。
(2) 使用java.lang.reflect包的方法對類進行反射調用的時候,如果類沒有進行過初始化,則需要先觸發其初始化。
(3) 當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。
(4)當虛擬機啓動時,用戶需要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類。
需要注意的是,如果在多線程環境下對類進行初始化,那麼其中一個線程對類進行初始化的時其他的線程會進入等待狀態。所以類的初始化階段是線程安全的。
類加載器
類加載器通過類的全限定名將.Class文件中的數據加載到了方法區之中。
可能有人就有知道我們平時寫代碼的時候類加載,通過下面這個例子來看,平時我們寫代碼的時候類加載器的層次結構。
public class TestClassLoader {
public static void main(String[] args) {
ClassLoader loader = Thread.currentThread().getContextClassLoader();
System.out.println(loader);
System.out.println(loader.getParent());
System.out.println(loader.getParent().getParent());
}
}
輸出結果:
sun.misc.Launcher$AppClassLoader
sun.misc.Launcher$ExtClassLoader
null
AppClassLoader加載我們所寫的程序的.class文件(classpath環境下的.class文件),ExtClassLoader是AppClassLoader的父類加載器,但是ExtClassLoader的父類是null,原因是啓動類加載器(BootStrapClassLoader)是用c++實現的找不到一個確定的返回父Loader的方式,於是就返回null。同時需要注意的是,這裏的父子關係不使用繼承關係實現的,而是使用組合。
這幾種類加載器的層次關係如下圖所示:
(1) Bootstrap ClassLoader : 將存放於\lib目錄中的,或者被-Xbootclasspath參數所指定的路徑中的,並且是虛擬機識別的(僅按照文件名識別,如 rt.jar 名字不符合的類庫即使放在lib目錄中也不會被加載)類庫加載到虛擬機內存中。啓動類加載器無法被Java程序直接引用。
(2) Extension ClassLoader : 將\lib\ext目錄下的,或者被java.ext.dirs系統變量所指定的路徑中的所有類庫加載。開發者可以直接使用擴展類加載器。
(3) Application ClassLoader : 負責加載用戶類路徑(ClassPath)上所指定的類庫,開發者可直接使用。
(4)用戶自定義類加載器:繼承java.lang.ClassLoader可以加載用戶指定位置的.class文件。
注意,除了BootStrapClassLoader是使用C++實現的之外,其餘的類加載器都繼承了java.lang.ClassLoader類。
雙親委派模型
如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把請求委託給父加載器去完成,依次向上,因此,所有的類加載請求最終都應該被傳遞到頂層的啓動類加載器中,只有當父加載器在它的搜索範圍中沒有找到所需的類時,即無法完成該加載,子加載器纔會嘗試自己去加載該類。
好處:
-保證不同的類加載環境下基類的唯一性,例如,我們寫一個java.lang.String類放入classpath環境下,將無法被加載。
-保證Java程序安全穩定運行。
ClassLoader源碼分析
public Class<?> loadClass(String name)throws ClassNotFoundException {
return loadClass(name, false);
}
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
//首先檢查該類是否被加載
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
//如果父類加載器不爲空首先有父類加載嘗試加載該類
c = parent.loadClass(name, false);
} else {
//由啓動類加載器加載
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 加載不成功就拋出ClassNotFoundException異常
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
//當父類加載器無法加載的時候,由當前類加載器來加載
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
自定義類加載器
public class MyClassLoader extends ClassLoader {
private String baseDir;
public MyClassLoader(String baseDir) {
this.baseDir = baseDir;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String fileName = baseDir + File.separatorChar
+ name.replace('.', File.separatorChar) + ".class";
byte[] bytes = readBytesFromFile(fileName);
return this.defineClass(name, bytes, 0, bytes.length);
}
private byte[] readBytesFromFile(String filename){
InputStream in = null;
ByteArrayOutputStream bos = null;
try {
in = new FileInputStream(filename);
bos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len = 0;
while ((len = in.read(buffer)) != -1){
bos.write(buffer,0,len);
}
return bos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
}finally {
if(bos != null){
try {
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(in != null){
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return null;
}
public static void main(String[] args) throws ClassNotFoundException {
MyClassLoader loader = new MyClassLoader("G:\\code");
System.out.println(loader.findClass("com.fjh.Test").getName());
}
}
輸出結果:
com.fjh.Test
注意:
1、最好不要重寫loadClass方法,因爲這樣容易破壞雙親委託模式。
2、不能把com.fjh.Test類放在ClassPath環境下,否則就會有AppClassLoader來加載該類,就輪不到我們自定義的類加載器來加載該類了。