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

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

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