我們知道class文件中存儲(chǔ)了類的描述信息和各種細(xì)節(jié)的數(shù)據(jù),在運(yùn)行Java程序時(shí),虛擬機(jī)需要先將類的這些數(shù)據(jù)加載到內(nèi)存中,并經(jīng)過校驗(yàn)、轉(zhuǎn)換、解析和初始化過后,最終形成可以直接使用的Java類型。
類從被加載到虛擬機(jī)內(nèi)存中開始,到卸載出內(nèi)存為止,它的整個(gè)生命周期包括:加載、驗(yàn)證、準(zhǔn)備、解析、初始化、使用和卸載7個(gè)階段。其中驗(yàn)證、準(zhǔn)備、解析3個(gè)部分統(tǒng)稱為連接。
類的加載機(jī)制實(shí)際上就是類的生命周期中加載、驗(yàn)證、準(zhǔn)備、解析、初始化5個(gè)過程。
加載
加載是類的加載過程的第一個(gè)階段,在加載階段,虛擬機(jī)需要完成以下3件事情:
- 通過一個(gè)類的全限定名來獲取定義此類的二進(jìn)制字節(jié)流;
- 將這個(gè)字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu);
- 在內(nèi)存中生成一個(gè)代表這個(gè)類的
java.lang.Class
對象,作為方法區(qū)這個(gè)類的各種數(shù)據(jù)的訪問入口。
通過全限定名來獲取二進(jìn)制流可以有很多種方式,比如從JAR、EAR、WAR文件包中讀取,從網(wǎng)絡(luò)獲取,也可以由其他文件來生成(jsp文件生成對應(yīng)的Servlet類),甚至還可以通過運(yùn)行時(shí)動(dòng)態(tài)生成(Java動(dòng)態(tài)代理)。
相比類加載過程的其他階段,加載階段是可控性最強(qiáng)的。因?yàn)殚_發(fā)者既可以利用系統(tǒng)提供的啟動(dòng)類加載器來完成,也可以通過自定義類加載去完成(重寫loadClass
方法,控制字節(jié)流的獲取方式)。
關(guān)于類加載器的詳細(xì)介紹將放在文章最后。
加載階段完成后,虛擬機(jī)外部的二進(jìn)制字節(jié)流就按照虛擬機(jī)所需的格式存儲(chǔ)在方法區(qū)之中。然后在內(nèi)存中實(shí)例化一個(gè)java.lang.Class
類的對象,這樣就可以通過這個(gè)對象來訪問方法區(qū)中的這些數(shù)據(jù)。
驗(yàn)證
驗(yàn)證是連接階段的第一步,這一階段的目的是為了確保class文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求,并且不會(huì)危害虛擬機(jī)自身的安全。驗(yàn)證階段大致上會(huì)完成下面4個(gè)階段的檢驗(yàn)動(dòng)作:文件格式驗(yàn)證、元數(shù)據(jù)驗(yàn)證、字節(jié)碼驗(yàn)證、符號引用驗(yàn)證。
- 文件格式驗(yàn)證: 驗(yàn)證字節(jié)流是否符合Class文件格式的規(guī)范,并且能被當(dāng)前版本的虛擬機(jī)處理。該驗(yàn)證階段的主要目的是保證輸入的字節(jié)流能正確地解析并存儲(chǔ)于方法區(qū)之內(nèi),格式上符合描述一個(gè)Java類型信息的要求。通過了這個(gè)階段的驗(yàn)證后,字節(jié)流才會(huì)進(jìn)入內(nèi)存的方法區(qū)中進(jìn)行存儲(chǔ),后面的
3個(gè)驗(yàn)證階段全部是基于方法區(qū)的存儲(chǔ)結(jié)構(gòu)進(jìn)行的,不會(huì)再直接操作字節(jié)流。 - 元數(shù)據(jù)驗(yàn)證: 對字節(jié)碼描述的信息進(jìn)行語義分析,以保證其描述的信息符合Java語言規(guī)范的要求。這個(gè)主要目的是對類的元數(shù)據(jù)信息進(jìn)行語義校驗(yàn),保證不存在不符合Java語言規(guī)范的元數(shù)據(jù)信息。
- 字節(jié)碼驗(yàn)證: 對類的方法體進(jìn)行校驗(yàn)分析,保證被校驗(yàn)類的方法在運(yùn)行時(shí)不會(huì)做出危害虛擬機(jī)安全的事件。
- 符號驗(yàn)證: 對類自身以外(常量池中的各種符號引用)的信息進(jìn)行匹配性校驗(yàn),這個(gè)階段發(fā)生在將符號引用轉(zhuǎn)化為直接引用的時(shí)候(解析階段中發(fā)生),目的是確保解析動(dòng)作能正常執(zhí)行。
準(zhǔn)備
準(zhǔn)備階段是正式為類變量(靜態(tài)變量)分配內(nèi)存并設(shè)置初始值的階段,這些類變量所使用的內(nèi)存都將在方法區(qū)中進(jìn)行分配。
這里有兩點(diǎn)需要注意:
- 成員變量不是在這里分配內(nèi)存的,成員變量是在類實(shí)例化對象的時(shí)候在堆中分配的。
- 這里設(shè)置初始值是指類型的零值(比如0,null,false等),而不是代碼中被顯示的賦予的值。
比如:
public class Test {
public int number = 111;
public static int sNumber = 111;
}
成員變量number
在這個(gè)階段就不會(huì)進(jìn)行內(nèi)存分配和初始化。而類變量sNunber
會(huì)在方法區(qū)中分配內(nèi)存,并設(shè)置為int類型的零值0而不是111,賦值為111是在初始化階段才會(huì)執(zhí)行。
但是呢,如果類變量如果是被final修飾,為靜態(tài)常量,那么在準(zhǔn)備階段也會(huì)在方法區(qū)中分配內(nèi)存,并且將其值設(shè)置為顯示賦予的值。
比如:
public class Test {
public static final int NUMBER = 111;
}
此時(shí),就會(huì)在準(zhǔn)備階段將NUMBER
的值設(shè)置為111。
解析
解析階段是虛擬機(jī)將常量池內(nèi)的符號引用替換為直接引用的過程。
- 符號引用: 符號引用以一組符號來描述所引用的目標(biāo),符號可以是任何形式的字面量,只要使用時(shí)能無歧義地定位到目標(biāo)即可。
- 直接引用: 直接引用可以是直接指向目標(biāo)的指針、相對偏移量或是一個(gè)能間接定位到目標(biāo)的句柄。
解析動(dòng)作主要就是在常量池中尋找類或接口、字段、類方法、接口方法、方法類型、方法句柄、調(diào)用點(diǎn)限定符等7類符號引用,把這些符號引用替換為直接引用。下面主要介紹下類或接口、字段、類方法、接口方法的解析:
- 類或接口解析: 假設(shè)當(dāng)前的類
A
通過符號X引用了類B
,虛擬機(jī)會(huì)把代表類B
的全限定名傳遞給A
的類加載器去加載B
,B
經(jīng)過加載、驗(yàn)證、準(zhǔn)備過程,在解析過程又可能會(huì)觸發(fā)B
引用的其他的類的加載過程,相當(dāng)于一個(gè)類引用鏈的遞歸加載過程,整個(gè)過程只要不出現(xiàn)異常,B
的就是一個(gè)加載成功的類或接口了,也就是可以獲取到代表B
的java.lang.Class
對象。在驗(yàn)證了A
具備對B
的訪問權(quán)限后,就將符號引用X替換為B
的直接引用。 - 字段解析: 解析未被解析過的字段,要先解析字段所屬的類或接口的符號引用。如果類本身就包含了簡單的名稱和字段描述與目標(biāo)字段相匹配,就直接返回這個(gè)字段引用;如果實(shí)現(xiàn)了接口,將會(huì)按照繼承關(guān)系從下往上遞歸搜索各個(gè)接口和它的父接口,如果接口中包含了簡單名稱和字段描述符都與目標(biāo)相匹配的字段,則返回這個(gè)字段;如果是繼承自其他類的話,將會(huì)按照繼承關(guān)系從下往上遞歸搜索其父類,如果在父類中包含了簡單名稱和字段描述符都與目標(biāo)相匹配的字段,則返回這個(gè)字段的直接引用。
- 類方法解析:類方法解析和字段解析的方式類似,也是依據(jù)繼承和實(shí)現(xiàn)關(guān)系從小到上搜索,只不過是先搜索類,后搜索接口。如果有簡單名稱和字段描述符都與目標(biāo)相匹配的字段,就返回字段引用。
- 接口的方法解析: 與類方法解析類似,從小到上搜索接口(接口沒有父類,只可能有父接口)。如果存在簡單名稱和字段描述符都與目標(biāo)相匹配的字段,就返回字段引用。
初始化
類的初始化類加載過程的最后一步,在前面的過中,除了在加載階段開發(fā)者可以自定義加載器之外,其余的動(dòng)作都是完全有虛擬機(jī)主導(dǎo)和控制完成。到了初始化階段,才真正開始執(zhí)行類中定義的Java代碼。
在準(zhǔn)備階段,類變量已經(jīng)設(shè)置了系統(tǒng)要求的零值,而在初始化階段,則根據(jù)程序員通過程序制定的主觀計(jì)劃去初始化類變量和其他資源,或者可以從另外一個(gè)角度來表達(dá):初始化階段是執(zhí)行類構(gòu)造器<clinit>()
方法的過程。
<clinit>()
方法是由編譯器自動(dòng)收集類中所有的類變量(static
變量)和靜態(tài)代碼塊(static{}
塊)中的語句合并生成的。編譯器收集的順序是由語句在源文件中出現(xiàn)的順序所決定的,靜態(tài)代碼塊中只能訪問到定義在靜態(tài)代碼塊之前的變量,定義在它之后的變量,在前面的靜態(tài)代碼塊可以賦值,但是不能訪問。
public class Test {
static {
number = 111; // 可以賦值
System.out.println(number); // 不能讀取,編輯器或報(bào)錯(cuò)Illegal forward reference
}
static int number;
}
<clinit>()
方法與類的構(gòu)造函數(shù)(或者說實(shí)例構(gòu)造器<init>()
方法)不同,它不需要顯式地調(diào)用父類的<clinit>()
方法,虛擬機(jī)會(huì)保證在子類的<clinit>()
方法執(zhí)行之前,父類的<clinit>()
方法已經(jīng)執(zhí)行完畢。所以,父類定義的靜態(tài)代碼塊要先與子類的賦值操作。
class Parent {
public static int A = 1;
static {
A = 2;
}
}
class Sub extends Parent {
public static int B = A;
public static void main(String[] args) {
System.out.println(Sub.B);
}
}
<clinit>()
方法對于類或接口來說并不是必需的,如果一個(gè)類中沒有靜態(tài)語句塊,也沒有對變量的賦值操作,那么編譯器可以不為這個(gè)類生成<clinit>()
方法。
接口中不能使用靜態(tài)語句塊,但仍然有變量初始化的賦值操作,因此接口與類一樣都會(huì)生成<clinit>()
方法。但接口與類不同的是,執(zhí)行接口的<clinit>()
方法不需要先執(zhí)行父接口的<clinit>()
方法。只有當(dāng)父接口中定義的變量使用時(shí),父接口才會(huì)初始化。另外,接口的實(shí)現(xiàn)類在初始化時(shí)也一樣不會(huì)執(zhí)行接口的<clinit>()
方法。
虛擬機(jī)會(huì)保證一個(gè)類的<clinit>()
方法在多線程環(huán)境中被正確地加鎖、同步,如果多個(gè)線程同時(shí)去初始化一個(gè)類,那么只會(huì)有一個(gè)線程去執(zhí)行這個(gè)類的<clinit>()
方法,其他線程都需要阻塞等待,直到活動(dòng)線程執(zhí)行<clinit>()
方法完畢。如果在一個(gè)類的<clinit>()
方法中有耗時(shí)很長的操作,就可能造成多個(gè)進(jìn)程阻塞。
類加載器
在之前的加載過程中,提到了類加載器通過一個(gè)類的全限定名來獲取描述此類的二進(jìn)制字節(jié)流,這個(gè)過程可以讓開發(fā)中自定義類加載器來決定如何獲取需要的字節(jié)流。那么,什么是類加載器呢?
對于任意一個(gè)Java類,都必須通過類加載器加載到方法區(qū),并生成java.lang.Class
對象才能使用類的各個(gè)功能,所以我們可以把類加載器理解為一個(gè)將class
類文件轉(zhuǎn)換為java.lang.Class
對象的工具。
對于任意一個(gè)類,都需要由加載它的類加載器和這個(gè)類本身一同確立其在Java虛擬機(jī)中的唯一性,每一個(gè)類加載器,都擁有一個(gè)獨(dú)立的類名稱空間。也就是說,如果兩個(gè)類“相等”,那么這兩個(gè)類必須是被同一個(gè)虛擬機(jī)中的同一個(gè)類加載器加載,并且來自同一個(gè)class
文件。
在Java當(dāng)中,已經(jīng)有3個(gè)預(yù)制的類加載器,分別是BootStrapClassLoader
、ExtClassLoader、AppClassLoader
。
- BootStrapClassLoader: 啟動(dòng)類加載器,它是由C++來實(shí)現(xiàn)的,在Java程序中不能顯氏的獲取到。它負(fù)責(zé)加載存放在JDK\jre\lib(JDK代表JDK的安裝目錄,下同)下的類。
- ExtClassLoader: 擴(kuò)展類加載器,它是由sun.misc.Launcher$ExtClassLoader實(shí)現(xiàn),負(fù)責(zé)加載JDK\jre\lib\ext目錄中,或者由java.ext.dirs系統(tǒng)變量指定的路徑中的所有類庫。開發(fā)者可以直接使用它。
- AppClassLoader: 應(yīng)用程序類加載器,由sun.misc.Launcher$AppClassLoader來實(shí)現(xiàn),它負(fù)責(zé)加載用戶類路徑(ClassPath)所指定的類,開發(fā)者可以直接使用該類加載器。一般來說,開發(fā)者自定義的類就是由應(yīng)用程序類加載器加載的。
ExtClassLoader
作為類加載器,但它也是一個(gè)Java類,是由BootStrapClassLoader
來加載的,所以,ExtClassLoader
的parent是BootStrapClassLoader
。但是由于BootStrapClassLoader
是c++
實(shí)現(xiàn)的,我們通過ExtClassLoader.getParent
獲取到的是null
。同樣地,AppClassLoader
是由ExtClassLoader
加載,AppClassLoader
的parent是ExtClassLoader
。
public class Test {
public static void main(String[] args) {
ClassLoader cl = Test.class.getClassLoader();
while (cl != null) {
System.out.println(cl);
cl = cl.getParent();
}
}
}
打印結(jié)果:
sun.misc.Launcher$AppClassLoader@232204a1
sun.misc.Launcher$ExtClassLoader@74a14482
同時(shí)我們可以定義自己的類加載器CustomClassLoader
,那么它的parent肯定就是AppClassLoader
了。類加載器的這種層次關(guān)系稱為雙親委派模型。
雙親委派模型
雙親委派模型要求除了頂層的啟動(dòng)類加載器外,其余的類加載器都應(yīng)當(dāng)有自己的父類加載器。這里類加載器之間的父子關(guān)系不是以繼承的關(guān)系來實(shí)現(xiàn),而是都使用遞歸的方式來調(diào)用父加載器的代碼。
雙親委派模型的工作過程是:如果一個(gè)類加載器收到了類加載的請求,它首先不會(huì)自己去嘗試加載這個(gè)類,而是把這個(gè)請求委派給父類加載器去完成,每一個(gè)層次的類加載器都是如此,因此所有的加載請求最終都應(yīng)該傳送到頂層的啟動(dòng)類加載器中,只有當(dāng)父加載器反饋?zhàn)约簾o法完成這個(gè)加載請求(它的搜索范圍中沒有找到所需的類)時(shí),子加載器才會(huì)嘗試自己去加載。
ClassLoader的源碼:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
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 thrown if class not found
// 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;
}
}
先檢查是否已經(jīng)被加載過,若沒有加載則調(diào)用父類加載器的loadClass()
方法,依次向上遞歸。若父類加載器為空則說明遞歸到啟動(dòng)類加載器了。如果從父類加載器到啟動(dòng)類加載器的上層次的所有加載器都加載失敗,則調(diào)用自己的findClass()
方法進(jìn)行加載。
使用雙親委派模型能使Java類隨著加載器一起具備一種優(yōu)先級的層次關(guān)系,保證同一個(gè)類只加載一次,避免了重復(fù)加載,同時(shí)也能阻止有人惡意替換加載系統(tǒng)類。
自定義類加載器
一般地,在ClassLoader
方法的loadClass
方法中已經(jīng)給開發(fā)者實(shí)現(xiàn)了雙親委派模型,在自定義類加載器的時(shí)候,只需要復(fù)寫findClass
方法即可。
public class CustomClassLoader extends ClassLoader {
private String root;
public CustomClassLoader(String root) {
this.root = root;
}
@Override
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 name) {
String fileName = root + File.separatorChar
+ name.replace('.', File.separatorChar)
+ ".class";
try {
InputStream ins = new FileInputStream(fileName);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 1024;
byte[] buffer = new byte[bufferSize];
int length;
while ((length = ins.read(buffer)) != -1) {
baos.write(buffer, 0, length);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
新建一個(gè)類com.xiao.U
,編譯成class文件,放到桌面,來測試一下:
public class Test {
public static void main(String[] args) {
CustomClassLoader customClassLoader = new CustomClassLoader("C:\\Users\\PC\\Desktop");
try {
Class clazz = customClassLoader.loadClass("com.xiao.U");
Object o = clazz.newInstance();
System.out.println(o.getClass().getClassLoader());
} catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
e.printStackTrace();
}
}
}
打印結(jié)果:
CustomClassLoader@1540e19d
自定義類加載器在可以實(shí)現(xiàn)服務(wù)端的熱部署,在移動(dòng)端比如android也可以實(shí)現(xiàn)熱更新。
參考: