1、什麼是類的加載
類的加載指的是将類的.class檔案中的二進制資料讀入到記憶體中,将其放在運作時資料區的方法區内,然後在堆區建立一個java.lang.Class對象,用來封裝類在方法區内的資料結構。類的加載的最終産品是位于堆區中的Class對象,Class對象封裝了類在方法區内的資料結構,并且向Java程式員提供了通路方法區内的資料結構的接口。
類加載器并不需要等到某個類被“首次主動使用”時再加載它,JVM規範允許類加載器在預料某個類将要被使用時就預先加載它,如果在預先加載的過程中遇到了.class檔案缺失或存在錯誤,類加載器必須在程式首次主動使用該類時才報告錯誤(LinkageError錯誤)如果這個類一直沒有被程式主動使用,那麼類加載器就不會報告錯誤
加載.class檔案的方式
– 從本地系統中直接加載
– 通過網絡下載下傳.class檔案
– 從zip,jar等歸檔檔案中加載.class檔案
– 從專有資料庫中提取.class檔案
– 将Java源檔案動态編譯為.class檔案
2、類的生命周期
其中類加載的過程包括了加載、驗證、準備、解析、初始化五個階段。在這五個階段中,加載、驗證、準備和初始化這四個階段發生的順序是确定的,而解析階段則不一定,它在某些情況下可以在初始化階段之後開始,這是為了支援Java語言的運作時綁定(也成為動态綁定或晚期綁定)。另外注意這裡的幾個階段是按順序開始,而不是按順序進行或完成,因為這些階段通常都是互相交叉地混合進行的,通常在一個階段執行的過程中調用或激活另一個階段。
加載:查找并加載類的二進制資料
加載時類加載過程的第一個階段,在加載階段,虛拟機需要完成以下三件事情:
1、通過一個類的全限定名來擷取其定義的二進制位元組流。
2、将這個位元組流所代表的靜态存儲結構轉化為方法區的運作時資料結構。
3、在Java堆中生成一個代表這個類的java.lang.Class對象,作為對方法區中這些資料的通路入口。
相對于類加載的其他階段而言,加載階段(準确地說,是加載階段擷取類的二進制位元組流的動作)是可控性最強的階段,因為開發人員既可以使用系統提供的類加載器來完成加載,也可以自定義自己的類加載器來完成加載。
加載階段完成後,虛拟機外部的 二進制位元組流就按照虛拟機所需的格式存儲在方法區之中,而且在Java堆中也建立一個java.lang.Class類的對象,這樣便可以通過該對象通路方法區中的這些資料。
連接配接
– 驗證:確定被加載的類的正确性
驗證是連接配接階段的第一步,這一階段的目的是為了確定Class檔案的位元組流中包含的資訊符合目前虛拟機的要求,并且不會危害虛拟機自身的安全。驗證階段大緻會完成4個階段的檢驗動作:
檔案格式驗證:驗證位元組流是否符合Class檔案格式的規範;例如:是否以0xCAFEBABE開頭、主次版本号是否在目前虛拟機的處理範圍之内、常量池中的常量是否有不被支援的類型。
中繼資料驗證:對位元組碼描述的資訊進行語義分析(注意:對比javac編譯階段的語義分析),以保證其描述的資訊符合Java語言規範的要求;例如:這個類是否有父類,除了java.lang.Object之外。
位元組碼驗證:通過資料流和控制流分析,确定程式語義是合法的、符合邏輯的。
符号引用驗證:確定解析動作能正确執行。
驗證階段是非常重要的,但不是必須的,它對程式運作期沒有影響,如果所引用的類經過反複驗證,那麼可以考慮采用-Xverifynone參數來關閉大部分的類驗證措施,以縮短虛拟機類加載的時間。
– 準備:為類的靜态變量配置設定記憶體,并将其初始化為預設值
準備階段是正式為類變量配置設定記憶體并設定類變量初始值的階段,這些記憶體都将在方法區中配置設定。對于該階段有以下幾點需要注意:
1、這時候進行記憶體配置設定的僅包括類變量(static),而不包括執行個體變量,執行個體變量會在對象執行個體化時随着對象一塊配置設定在Java堆中。
2、這裡所設定的初始值通常情況下是資料類型預設的零值(如0、0L、null、false等),而不是被在Java代碼中被顯式地賦予的值。
假設一個類變量的定義為:public static int value = 3;
那麼變量value在準備階段過後的初始值為0,而不是3,因為這時候尚未開始執行任何Java方法,而把value指派為3的putstatic指令是在程式編譯後,存放于類構造器<clinit>()方法之中的,是以把value指派為3的動作将在初始化階段才會執行。
這裡還需要注意如下幾點:
- 對基本資料類型來說,對于類變量(static)和全局變量,如果不顯式地對其指派而直接使用,則系統會為其賦予預設的零值,而對于局部變量來說,在使用前必須顯式地為其指派,否則編譯時不通過
- 對于同時被static和final修飾的常量,必須在聲明的時候就為其顯式地指派,否則編譯時不通過;而隻被final修飾的常量則既可以在聲明時顯式地為其指派,也可以在類初始化時顯式地為其指派,總之,在使用前必須為其顯式地指派,系統不會為其賦予預設零值。
- 對于引用資料類型reference來說,如數組引用、對象引用等,如果沒有對其進行顯式地指派而直接使用,系統都會為其賦予預設的零值,即null。
- 如果在數組初始化時沒有對數組中的各元素指派,那麼其中的元素将根據對應的資料類型而被賦予預設的零值。
3、如果類字段的字段屬性表中存在ConstantValue屬性,即同時被final和static修飾,那麼在準備階段變量value就會被初始化為ConstValue屬性所指定的值。
假設上面的類變量value被定義為: public static final int value = 3;
編譯時Javac将會為value生成ConstantValue屬性,在準備階段虛拟機就會根據ConstantValue的設定将value指派為3。回憶上一篇博文中對象被動引用的第2個例子,便是這種情況。我們可以了解為static final常量在編譯期就将其結果放入了調用它的類的常量池中
– 解析:把類中的符号引用轉換為直接引用
解析階段是虛拟機将常量池内的符号引用替換為直接引用的過程,解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符号引用進行。符号引用就是一組符号來描述目标,可以是任何字面量。
直接引用就是直接指向目标的指針、相對偏移量或一個間接定位到目标的句柄。
初始化
初始化,為類的靜态變量賦予正确的初始值,JVM負責對類進行初始化,主要對類變量進行初始化。在Java中對類變量進行初始值設定有兩種方式:
①聲明類變量是指定初始值
②使用靜态代碼塊為類變量指定初始值
JVM初始化步驟
1、假如這個類還沒有被加載和連接配接,則程式先加載并連接配接該類
2、假如該類的直接父類還沒有被初始化,則先初始化其直接父類
3、假如類中有初始化語句,則系統依次執行這些初始化語句
類初始化時機
隻有當對類的主動使用的時候才會導緻類的初始化,類的主動使用包括以下六種:
– 建立類的執行個體,也就是new的方式
– 通路某個類或接口的靜态變量,或者對該靜态變量指派
– 調用類的靜态方法
– 反射(如Class.forName(“com.shengsiyuan.Test”))
– 初始化某個類的子類,則其父類也會被初始化
– Java虛拟機啟動時被标明為啟動類的類(Java Test),直接使用java.exe指令來運作某個主類
結束生命周期
•在如下幾種情況下,Java虛拟機将結束生命周期
– 執行了System.exit()方法
– 程式正常執行結束
– 程式在執行過程中遇到了異常或錯誤而異常終止
– 由于作業系統出現錯誤而導緻Java虛拟機程序終止
3、類加載器
尋找類加載器,先來一個小例子
package com.neo.classloader;
public class ClassLoaderTest {
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@64fef26a
sun.misc.Launcher$ExtClassLoader@1ddd40f3
null
從上面的結果可以看出,并沒有擷取到ExtClassLoader的父Loader,原因是Bootstrap Loader(引導類加載器)是用C語言實作的,找不到一個确定的傳回父Loader的方式,于是就傳回null。
這幾種類加載器的層次關系如下圖所示:

注意:這裡父類加載器并不是通過繼承關系來實作的,而是采用組合實作的。
站在Java虛拟機的角度來講,隻存在兩種不同的類加載器:啟動類加載器:它使用C++實作(這裡僅限于Hotspot,也就是JDK1.5之後預設的虛拟機,有很多其他的虛拟機是用Java語言實作的),是虛拟機自身的一部分;所有其他的類加載器:這些類加載器都由Java語言實作,獨立于虛拟機之外,并且全部繼承自抽象類java.lang.ClassLoader,這些類加載器需要由啟動類加載器加載到記憶體中之後才能去加載其他的類。
站在Java開發人員的角度來看,類加載器可以大緻劃分為以下三類:
啟動類加載器:Bootstrap ClassLoader,負責加載存放在JDK\jre\lib(JDK代表JDK的安裝目錄,下同)下,或被-Xbootclasspath參數指定的路徑中的,并且能被虛拟機識别的類庫(如rt.jar,所有的java.*開頭的類均被Bootstrap ClassLoader加載)。啟動類加載器是無法被Java程式直接引用的。
擴充類加載器:Extension ClassLoader,該加載器由sun.misc.Launcher$ExtClassLoader實作,它負責加載DK\jre\lib\ext目錄中,或者由java.ext.dirs系統變量指定的路徑中的所有類庫(如javax.*開頭的類),開發者可以直接使用擴充類加載器。
應用程式類加載器:Application ClassLoader,該類加載器由sun.misc.Launcher$AppClassLoader來實作,它負責加載使用者類路徑(ClassPath)所指定的類,開發者可以直接使用該類加載器,如果應用程式中沒有自定義過自己的類加載器,一般情況下這個就是程式中預設的類加載器。
應用程式都是由這三種類加載器互相配合進行加載的,如果有必要,我們還可以加入自定義的類加載器。因為JVM自帶的ClassLoader隻是懂得從本地檔案系統加載标準的java class檔案,是以如果編寫了自己的ClassLoader,便可以做到如下幾點:
1)在執行非置信代碼之前,自動驗證數字簽名。
2)動态地建立符合使用者特定需要的定制化建構類。
3)從特定的場所取得java class,例如資料庫中和網絡中。
JVM類加載機制
•全盤負責,當一個類加載器負責加載某個Class時,該Class所依賴的和引用的其他Class也将由該類加載器負責載入,除非顯示使用另外一個類加載器來載入
•父類委托,先讓父類加載器試圖加載該類,隻有在父類加載器無法加載該類時才嘗試從自己的類路徑中加載該類
•緩存機制,緩存機制将會保證所有加載過的Class都會被緩存,當程式中需要使用某個Class時,類加載器先從緩存區尋找該Class,隻有緩存區不存在,系統才會讀取該類對應的二進制資料,并将其轉換成Class對象,存入緩存區。這就是為什麼修改了Class後,必須重新開機JVM,程式的修改才會生效
4、類的加載
類加載有三種方式:
1、指令行啟動應用時候由JVM初始化加載
2、通過Class.forName()方法動态加載
3、通過ClassLoader.loadClass()方法動态加載
例子:
public class loaderTest {
public static void main(String[] args) throws ClassNotFoundException {
ClassLoader loader = HelloWorld.class.getClassLoader();
System.out.println(loader);
//使用ClassLoader.loadClass()來加載類,不會執行初始化塊
loader.loadClass("Test2");
//使用Class.forName()來加載類,預設會執行初始化塊
// Class.forName("Test2");
//使用Class.forName()來加載類,并指定ClassLoader,初始化時不執行靜态塊
// Class.forName("Test2", false, loader);
}
demo類
public class Test2 {
static {
System.out.println("靜态初始化塊執行了!");
分别切換加載方式,會有不同的輸出結果。
Class.forName()和ClassLoader.loadClass()差別
Class.forName():将類的.class檔案加載到jvm中之外,還會對類進行解釋,執行類中的static塊;
ClassLoader.loadClass():隻幹一件事情,就是将.class檔案加載到jvm中,不會執行static中的内容,隻有在newInstance才會去執行static塊。
注:
Class.forName(name, initialize, loader)帶參函數也可控制是否加載static塊。并且隻有調用了newInstance()方法采用調用構造函數,建立類的對象 。
5、雙親委派模型
雙親委派模型的工作流程是:如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把請求委托給父加載器去完成,依次向上,是以,所有的類加載請求最終都應該被傳遞到頂層的啟動類加載器中,隻有當父加載器在它的搜尋範圍中沒有找到所需的類時,即無法完成該加載,子加載器才會嘗試自己去加載該類。
雙親委派機制
1、當AppClassLoader加載一個class時,它首先不會自己去嘗試加載這個類,而是把類加載請求委派給父類加載器ExtClassLoader去完成。
2、當ExtClassLoader加載一個class時,它首先也不會自己去嘗試加載這個類,而是把類加載請求委派給BootStrapClassLoader去完成。
3、如果BootStrapClassLoader加載失敗(例如在$JAVA_HOME/jre/lib裡未查找到該class),會使用ExtClassLoader來嘗試加載;
4、若ExtClassLoader也加載失敗,則會使用AppClassLoader來加載,如果AppClassLoader也加載失敗,則會報出異常ClassNotFoundException。
ClassLoader源碼分析:
public Class<?> loadClass(String name)throws ClassNotFoundException {
return loadClass(name, false);
protected synchronized Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException {
// 首先判斷該類型是否已經被加載
Class c = findLoadedClass(name);
if (c == null) {
//如果沒有被加載,就委托給父類加載或者委派給啟動類加載器加載
try {
if (parent != null) {
//如果存在父類加載器,就委派給父類加載器加載
c = parent.loadClass(name, false);
} else {
//如果不存在父類加載器,就檢查是否是由啟動類加載器加載的類,通過調用本地方法native Class findBootstrapClass(String name)
c = findBootstrapClass0(name);
}
} catch (ClassNotFoundException e) {
// 如果父類加載器和啟動類加載器都不能完成加載任務,才調用自身的加載功能
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
return c;
}
雙親委派模型意義:
-系統類防止記憶體中出現多份同樣的位元組碼
-保證Java程式安全穩定運作
6、自定義類加載器
通常情況下,我們都是直接使用系統類加載器。但是,有的時候,我們也需要自定義類加載器。比如應用是通過網絡來傳輸 Java 類的位元組碼,為保證安全性,這些位元組碼經過了加密處理,這時系統類加載器就無法對其進行加載,這樣則需要自定義類加載器來實作。自定義類加載器一般都是繼承自 ClassLoader 類,從上面對 loadClass 方法來分析來看,我們隻需要重寫 findClass 方法即可。下面我們通過一個示例來示範自定義類加載器的流程:
import java.io.*;
public class MyClassLoader extends ClassLoader {
private String root;
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = loadClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
return defineClass(name, classData, 0, classData.length);
private byte[] loadClassData(String className) {
String fileName = root + File.separatorChar
+ className.replace('.', File.separatorChar) + ".class";
try {
InputStream ins = new FileInputStream(fileName);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 1024;
byte[] buffer = new byte[bufferSize];
int length = 0;
while ((length = ins.read(buffer)) != -1) {
baos.write(buffer, 0, length);
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
return null;
public String getRoot() {
return root;
public void setRoot(String root) {
this.root = root;
public static void main(String[] args) {
MyClassLoader classLoader = new MyClassLoader();
classLoader.setRoot("E:\\temp");
Class<?> testClass = null;
testClass = classLoader.loadClass("com.neo.classloader.Test2");
Object object = testClass.newInstance();
System.out.println(object.getClass().getClassLoader());
} catch (ClassNotFoundException e) {
} catch (InstantiationException e) {
} catch (IllegalAccessException e) {
自定義類加載器的核心在于對位元組碼檔案的擷取,如果是加密的位元組碼則需要在該類中對檔案進行解密。由于這裡隻是示範,我并未對class檔案進行加密,是以沒有解密的過程。這裡有幾點需要注意:
1、這裡傳遞的檔案名需要是類的全限定性名稱,即com.paddx.test.classloading.Test格式的,因為 defineClass 方法是按這種格式進行處理的。
2、最好不要重寫loadClass方法,因為這樣容易破壞雙親委托模式。
3、這類Test 類本身可以被 AppClassLoader 類加載,是以我們不能把 com/paddx/test/classloading/Test.class 放在類路徑下。否則,由于雙親委托機制的存在,會直接導緻該類由AppClassLoader 加載,而不會通過我們自定義類加載器來加載。
熬夜不易,點選請老王喝杯烈酒!!!!!!!