
前言
上回說到垃圾收集機(jī)制和內(nèi)存分配,這回咱們來了解下虛擬機(jī)類加載機(jī)制。
“代碼編譯的結(jié)果從本地機(jī)器碼轉(zhuǎn)變?yōu)樽止?jié)碼,是存儲(chǔ)格式發(fā)展的一小步,卻是編程語言發(fā)展的一大步”
基本概念
類加載周期
加載、驗(yàn)證、準(zhǔn)備、解析、初始化、使用、卸載

虛擬機(jī)規(guī)范中規(guī)定有且只有5種情況必須立即對類進(jìn)行初始化。
1). 遇到new,getstatic,putstatic,invokestatic這4條字節(jié)碼指令時(shí),如果類沒有進(jìn)行過初始化,則需要先觸發(fā)其初始化。
2). 使用java.lang.reflect包的方法對類進(jìn)行反射調(diào)用
3). 當(dāng)初始化一個(gè)類式,發(fā)現(xiàn)其父類還沒有初始化,先初始化其父類
4). 虛擬機(jī)啟動(dòng)時(shí),用戶需要制定一個(gè)主類(main),虛擬機(jī)會(huì)先初始化這個(gè)主類
5). 當(dāng)使用JDK1.7動(dòng)態(tài)語言支持時(shí),如果一個(gè)java.lang.invoke.MethodHandle實(shí)例最后的解析結(jié)果是REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,則這個(gè)方法句柄對應(yīng)的類需要初始化。
我們通過代碼來驗(yàn)證下相關(guān)信息
a. 被動(dòng)使用類字段演示
子類調(diào)用父類靜態(tài)變量時(shí)
public class SuperClass {
static{
System.out.println("SuperClass init ...");
}
public static int value=123;
}
public class SubClass extends SuperClass{
static{
System.out.println("SubClass init ...");
}
}
public class NoInitialization {
/**
* -XX:+TraceClassLoading 查看類加載過程
* @param args
*/
public static void main(String[] args) {
System.out.println(SubClass.value);
}
}
運(yùn)行結(jié)果
SuperClass init ...
123
發(fā)現(xiàn),父類初始化了,至于要不要初始化子類,就要看虛擬機(jī)了。
b.通過定義數(shù)組引用類
public class NoInitialization {
/**
* -XX:+TraceClassLoading 查看類加載過程
* @param args
*/
public static void main(String[] args) {
SuperClass[] sc = new SuperClass[10];
}
}
發(fā)現(xiàn)父類,子類均不初始化
c.訪問常量不會(huì)導(dǎo)致類初始化
public class SuperClass {
static{
System.out.println("SuperClass init ...");
}
public static int value=123;
public final static String hello="hello,world";
}
public class NoInitialization {
/**
* -XX:+TraceClassLoading 查看類加載過程
* @param args
*/
public static void main(String[] args) {
System.out.println(SubClass.hello);
}
}
運(yùn)行結(jié)果
hello,world
原因是常量會(huì)在編譯階段存入調(diào)用類的常量池中,本質(zhì)上沒有直接引用到定義常量的類
類的加載過程
加載
類加載干了什么呢?通過類的全名來獲取定義該類的二進(jìn)制字節(jié)流,將字節(jié)流代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu),在內(nèi)存中生成代表這個(gè)類的java.lang.Class對象作為這個(gè)類各種數(shù)據(jù)的訪問入口,這三步。-
驗(yàn)證
驗(yàn)證是連接階段的第一步。
java語言之所以是相對安全的語言,是因?yàn)槭褂眉兇獾膉ava代碼無法實(shí)現(xiàn)如訪問數(shù)組邊界以外的數(shù)據(jù)、將一個(gè)對象轉(zhuǎn)型為它并未實(shí)現(xiàn)的類型、跳轉(zhuǎn)到不存在的代碼行之類的,編譯器會(huì)幫我們拒絕編譯。然而,Class文件并不僅僅是java編譯而來的,可以通過別的途徑也能實(shí)現(xiàn),如果虛擬機(jī)運(yùn)行時(shí)不驗(yàn)證的話,所謂的安全就要打折扣了,因此,虛擬機(jī)有了驗(yàn)證作為連接的第一步。- 文件格式驗(yàn)證
驗(yàn)證字節(jié)流是否符合Class文件格式的規(guī)范,并能被當(dāng)前版本虛擬機(jī)處理。 - 元數(shù)據(jù)驗(yàn)證
對字節(jié)碼描述的信息進(jìn)行語義分析,以保證其描述的信息符合java語言規(guī)范的要求,如,這個(gè)類是否有父類,這個(gè)類是否繼承了不允許被繼承的類,如果不是抽象類,是否實(shí)現(xiàn)其父類或接口中要求實(shí)現(xiàn)的所有方法等 - 字節(jié)碼驗(yàn)證
最復(fù)雜的一個(gè)階段,通過數(shù)據(jù)流和控制流分析,確定程序語義是合法的符合邏輯的。 - 符號(hào)引用驗(yàn)證
- 文件格式驗(yàn)證
準(zhǔn)備
準(zhǔn)備階段就是正式為類變量分配內(nèi)存并設(shè)置類變量初始值的階段,這些變量所使用的內(nèi)存都將在方法區(qū)中進(jìn)行分配。這個(gè)時(shí)候進(jìn)行的內(nèi)存分配僅包括類變量即靜態(tài)變量,而我們的實(shí)例變量時(shí)分配在堆中。初始值除了final修飾的外,一般是數(shù)據(jù)類型的零值。如果是final修飾的將直接賦結(jié)果值。解析
解析階段就是虛擬機(jī)將常量池內(nèi)的符號(hào)引用替換為直接引用的一個(gè)過程。先理解下符號(hào)引用和直接引用的概率。
符號(hào)引用:使用一組符號(hào)來描述所引用的目標(biāo),與虛擬機(jī)內(nèi)存布局無關(guān),引用的目標(biāo)不一定已經(jīng)加載到內(nèi)存,明確定義在java虛擬機(jī)規(guī)范的Class文件中。
直接引用:直接指向目標(biāo)的指針、相對偏移量或者一個(gè)能間接定位到目標(biāo)的句柄。與內(nèi)存布局相關(guān),一個(gè)引用在不同的虛擬機(jī)實(shí)例翻譯過來一般也不同,引用的目標(biāo)已經(jīng)在內(nèi)存中存在。-
初始化
加載類的最后一步。前面的類加載過程除了加載階段用戶通過自定義類加載器參與外,完全是虛擬機(jī)主導(dǎo)的,到了初始化階段才是真正執(zhí)行類中定義的java代碼。
初始過程是執(zhí)行類構(gòu)造器<clinit>()方法的過程。<clinit>()的特點(diǎn)如下- <clinit>()方法是由編譯器自動(dòng)收集類中的所有類變量的復(fù)制動(dòng)作和靜態(tài)代碼塊中的語句合并產(chǎn)生。語句是有先后順序的,如定義在靜態(tài)代碼塊之后的變量,靜態(tài)代碼塊可以進(jìn)行賦值,但是不能訪問。代碼如下
public class FieldResolution {
static{
i=8;
System.out.println(i);
}
static int i;
}
這段代碼編譯時(shí)會(huì)報(bào)Cannot reference a field before it is defined,非法向前引用。而去掉打印語句,發(fā)現(xiàn)程序不會(huì)報(bào)錯(cuò),編譯源碼顯示static int i=8; 我們可以認(rèn)為編輯器會(huì)第一時(shí)間尋找到int j 的變量,在靜態(tài)代碼塊中賦值,如果靜態(tài)代碼塊后面初始化過的話,會(huì)第二次賦值,這樣以程序在后面的為準(zhǔn)。
- 由于父類的<clinit>()方法會(huì)先執(zhí)行,這就意味著父類中定義的靜態(tài)代碼塊要優(yōu)先于子類的變量賦值操作。
- <clinit>()對于類或者接口來說不是必需的,如果一個(gè)類沒有靜態(tài)代碼塊也沒有對變量的賦值操作,那么編譯器可以不為這個(gè)類生成<clinit>()方法。
- <clinit>()會(huì)被多線程環(huán)境下加鎖,同步。
public class DeadLoopClass {
static{
//如果沒有if會(huì)報(bào)Initializer does not complete normally
if(true){
System.out.println(Thread.currentThread().getName()+"init...");
while(true){
}
}
}
}
class TestDemo{
public static void main(String[] args) {
Runnable run = new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println(Thread.currentThread()+"--start");
DeadLoopClass deap = new DeadLoopClass();
System.out.println(Thread.currentThread()+"--over");
}
};
Thread t1 = new Thread(run);
Thread t2 = new Thread(run);
t1.start();
t2.start();
}
}
運(yùn)行結(jié)果
Thread[Thread-0,5,main]--start
Thread[Thread-1,5,main]--start
Thread-0init...
類加載器
通過一個(gè)類的全限定名來獲取描述此類的二進(jìn)制字節(jié)流,這個(gè)動(dòng)作放到j(luò)ava虛擬機(jī)外部去實(shí)現(xiàn),以便讓應(yīng)用程序自己決定如何去獲取所需要的類。
public class ClassLoaderTest {
public static void main(String[] args) throws ClassNotFoundException {
ClassLoader myLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name)
throws ClassNotFoundException {
try {
String fileName = name.substring(name.lastIndexOf(".")+1)+".class";
InputStream is = getClass().getResourceAsStream(fileName);
if(is == null){
return super.loadClass(name);
}
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b,0,b.length);
} catch (Exception e) {
throw new ClassNotFoundException(name);
}
}
};
Object obj = myLoader.loadClass("com.classloading.ClassLoaderTest");
System.out.println(obj.getClass());
System.out.println(obj instanceof com.classloading.ClassLoaderTest);
}
}
運(yùn)行結(jié)果
class java.lang.Class
false
我們構(gòu)造了一個(gè)簡單的類加載器,加載同一個(gè)路徑下的Class文件,然后和系統(tǒng)應(yīng)用程序類加載器的去比較,發(fā)現(xiàn)是兩個(gè)獨(dú)立的類,因此,類加載器不同,類也不同。
- 雙親委托模型
從java虛擬機(jī)角度來講,只存在兩種類加載器:啟動(dòng)類加載器(Bootstrap ClassLoader),其他類加載器,獨(dú)立于虛擬機(jī)外部,且全部繼承自java.lang.ClassLoader。

雙親委托模型除了頂部的啟動(dòng)類加載器,其他的都有自己的父加載器。這里類加載器之間的父子關(guān)系一般不會(huì)以繼承的關(guān)系來實(shí)現(xiàn),而都是使用組合關(guān)系來復(fù)用父加載器的代碼。
雙親委托模型的工作過程是:如果一個(gè)類加載器收到了類加載的請求,它首先不會(huì)自己去嘗試加載這個(gè)類,而是把這個(gè)請求委托給父類加載器去完成,每個(gè)層次的類加載器都是如此,最終傳遞到了啟動(dòng)類加載器,只有當(dāng)父加載器反饋?zhàn)约簾o法完成這個(gè)加載請求時(shí),子加載器才會(huì)嘗試自己去加載。
使用雙親委托機(jī)制的好處在于,Java類隨著它的類加載器一起具備優(yōu)先級(jí)的層次關(guān)系,如Object類,它存放在rt.jar中,無論哪個(gè)類加載器要加載這個(gè)類,最終都會(huì)委托給最頂級(jí)的類加載器去加載,這樣就不會(huì)造成系統(tǒng)中存在多個(gè)Object類。
- 破壞雙親委托模型
到目前為止出現(xiàn)過三次大規(guī)模的被破壞情況
a. 雙親委托模型是在JDK1.2之后引入的,ClassLoader在JDK1.0就存在了,為了向前兼容,在JDK1.2后加入了一個(gè)findClass()方法,之前用戶繼承ClassLoader類唯一目的是重寫loadClass方法,后來推薦把自己的類加載邏輯放到findClass方法中。
b. 模型自身缺陷,雙親委托很好的解決了各個(gè)類加載器的基礎(chǔ)類的統(tǒng)一問題,如果基礎(chǔ)類又要調(diào)用回用戶的代碼,這個(gè)時(shí)候就有問題了。如JNDI服務(wù),JNDI的目的是對資源進(jìn)行集中管理和查找,它需要調(diào)用由獨(dú)立廠商實(shí)現(xiàn)并部署在應(yīng)用程序的ClassPath的JNDI接口提供者的代碼,但啟動(dòng)類加載器不可能認(rèn)識(shí)這些,為了處理這個(gè)問題,引入了線程上下文加載器
c. 用戶對程序動(dòng)態(tài)性的追求而導(dǎo)致。比如熱部署啊,代碼熱替換。