0、前言
讀完本文,你將了解到:
一、為什么說Jabalpur語(yǔ)言是跨平臺(tái)的
二、Java虛擬機(jī)啟動(dòng)、加載類過程分析
三、類加載器有哪些?其組織結(jié)構(gòu)是怎樣的?
四、雙親加載模型的邏輯和底層代碼實(shí)現(xiàn)是怎樣的?
五、類加載器與Class? 實(shí)例的關(guān)系
六、線程上下文加載器
Java語(yǔ)言之所以說它是跨平臺(tái)的、可以在當(dāng)前絕大部分的操作系統(tǒng)平臺(tái)下運(yùn)行,是因?yàn)镴ava語(yǔ)言的運(yùn)行環(huán)境是在Java虛擬機(jī)中。
Java虛擬機(jī)消除了各個(gè)平臺(tái)之間的差異,只要操作系統(tǒng)平臺(tái)下安裝了Java虛擬機(jī),那么使用Java開發(fā)的東西都能在其上面運(yùn)行。如下圖所示:
Java虛擬機(jī)對(duì)各個(gè)平臺(tái)而言,實(shí)質(zhì)上是各個(gè)平臺(tái)上的一個(gè)可執(zhí)行程序。例如在windows平臺(tái)下,java虛擬機(jī)對(duì)于windows而言,就是一個(gè)java.exe進(jìn)程而已。
下面我將定義一個(gè)非常簡(jiǎn)單的java程序并運(yùn)行它,來逐步分析java虛擬機(jī)啟動(dòng)的過程。
[java]view plaincopy
packageorg.luanlouis.jvm.load;
importsun.security.pkcs11.P11Util;
/**
*?Created?by?louis?on?2016/1/16.
*/
publicclassMain{
publicstaticvoidmain(String[]?args)?{
System.out.println("Hello,World!");
ClassLoader?loader?=?P11Util.class.getClassLoader();
System.out.println(loader);
}
}
在windows命令行下輸入:
java??? org.luanlouis.jvm.load.Main
當(dāng)輸入上述的命令時(shí):
windows開始運(yùn)行{JRE_HOME}/bin/java.exe程序,java.exe 程序?qū)⑼瓿梢韵虏襟E:
1.? 根據(jù)JVM內(nèi)存配置要求,為JVM申請(qǐng)?zhí)囟ù笮〉膬?nèi)存空間;
2.? 創(chuàng)建一個(gè)引導(dǎo)類加載器實(shí)例,初步加載系統(tǒng)類到內(nèi)存方法區(qū)區(qū)域中;
3.創(chuàng)建JVM 啟動(dòng)器實(shí)例 Launcher,并取得類加載器ClassLoader;
4.? 使用上述獲取的ClassLoader實(shí)例加載我們定義的 org.luanlouis.jvm.load.Main類;
5.? 加載完成時(shí)候JVM會(huì)執(zhí)行Main類的main方法入口,執(zhí)行Main類的main方法;
6.? 結(jié)束,java程序運(yùn)行結(jié)束,JVM銷毀。
Step 1.根據(jù)JVM內(nèi)存配置要求,為JVM申請(qǐng)?zhí)囟ù笮〉膬?nèi)存空間
為了不降低本文的理解難度,這里就不詳細(xì)介紹JVM內(nèi)存配置要求的話題,今概括地介紹一下內(nèi)存的功能劃分。
JVM啟動(dòng)時(shí),按功能劃分,其內(nèi)存應(yīng)該由以下幾部分組成:
如上圖所示,JVM內(nèi)存按照功能上的劃分,可以粗略地劃分為方法區(qū)(Method Area)和堆(Heap),而所有的類的定義信息都會(huì)被加載到方法區(qū)中。
關(guān)于具體方法區(qū)里有什么內(nèi)容,讀者可以參考我的另一篇博文:
《Java虛擬機(jī)原理圖解》3、JVM運(yùn)行時(shí)數(shù)據(jù)區(qū)
Step 2. 創(chuàng)建一個(gè)引導(dǎo)類加載器實(shí)例,初步加載系統(tǒng)類到內(nèi)存方法區(qū)區(qū)域中;
JVM申請(qǐng)好內(nèi)存空間后,JVM會(huì)創(chuàng)建一個(gè)引導(dǎo)類加載器(Bootstrap Classloader)實(shí)例,引導(dǎo)類加載器是使用C++語(yǔ)言實(shí)現(xiàn)的,負(fù)責(zé)加載JVM虛擬機(jī)運(yùn)行時(shí)所需的基本系統(tǒng)級(jí)別的類,如java.lang.String, java.lang.Object等等。
引導(dǎo)類加載器(Bootstrap Classloader)會(huì)讀取{JRE_HOME}/lib下的jar包和配置,然后將這些系統(tǒng)類加載到方法區(qū)內(nèi)。
本例中,引導(dǎo)類加載器是用 {JRE_HOME}/lib加載類的,不過,你也可以使用參數(shù)-Xbootclasspath或 系統(tǒng)變量sun.boot.class.path來指定的目錄來加載類。
一般而言,{JRE_HOME}/lib下存放著JVM正常工作所需要的系統(tǒng)類,如下表所示:
文件名描述
rt.jar運(yùn)行環(huán)境包,rt即runtime,J2SE 的類定義都在這個(gè)包內(nèi)
charsets.jar字符集支持包
jce.jar是一組包,它們提供用于加密、密鑰生成和協(xié)商以及 Message Authentication Code(MAC)算法的框架和實(shí)現(xiàn)
jsse.jar安全套接字拓展包Java(TM) Secure Socket Extension
classlist該文件內(nèi)表示是引導(dǎo)類加載器應(yīng)該加載的類的清單
net.propertiesJVM 網(wǎng)絡(luò)配置信息
引導(dǎo)類加載器(Bootstrap ClassLoader)加載系統(tǒng)類后,JVM內(nèi)存會(huì)呈現(xiàn)如下格局:
引導(dǎo)類加載器將類信息加載到方法區(qū)中,以特定方式組織,對(duì)于某一個(gè)特定的類而言,在方法區(qū)中它應(yīng)該有運(yùn)行時(shí)常量池、類型信息、字段信息、方法信息、類加載器的引用,對(duì)應(yīng)class實(shí)例的引用等信息。
類加載器的引用,由于這些類是由引導(dǎo)類加載器(Bootstrap Classloader)進(jìn)行加載的,而 引導(dǎo)類加載器是有C++語(yǔ)言實(shí)現(xiàn)的,所以是無法訪問的,故而該引用為NULL
對(duì)應(yīng)class實(shí)例的引用, 類加載器在加載類信息放到方法區(qū)中后,會(huì)創(chuàng)建一個(gè)對(duì)應(yīng)的Class 類型的實(shí)例放到堆(Heap)中, 作為開發(fā)人員訪問方法區(qū)中類定義的入口和切入點(diǎn)。
小測(cè)試:
當(dāng)我們?cè)诖a中嘗試獲取系統(tǒng)類如java.lang.Object的類加載器時(shí),你會(huì)始終得到NULL:
[java]view plaincopy
System.out.println(String.class.getClassLoader());//null
System.out.println(Object.class.getClassLoader());//null
System.out.println(Math.class.getClassLoader());//null
System.out.println(System.class.getClassLoader());//null
Step 3. 創(chuàng)建JVM 啟動(dòng)器實(shí)例 Launcher,并取得類加載器ClassLoader
上述步驟完成,JVM基本運(yùn)行環(huán)境就準(zhǔn)備就緒了。接著,我們要讓JVM工作起來了:運(yùn)行我們定義的程序org.luanlouis,jvm.load.Main。
此時(shí),JVM虛擬機(jī)調(diào)用已經(jīng)加載在方法區(qū)的類sun.misc.Launcher的靜態(tài)方法getLauncher(),? 獲取sun.misc.Launcher實(shí)例:
[java]view plaincopy
sun.misc.Launcher?launcher?=?sun.misc.Launcher.getLauncher();//獲取Java啟動(dòng)器
ClassLoader?classLoader?=?launcher.getClassLoader();//獲取類加載器ClassLoader用來加載class到內(nèi)存來
sun.misc.Launcher使用了單例模式設(shè)計(jì),保證一個(gè)JVM虛擬機(jī)內(nèi)只有一個(gè)sun.misc.Launcher實(shí)例。
在Launcher的內(nèi)部,其定義了兩個(gè)類加載器(ClassLoader),分別是sun.misc.Launcher.ExtClassLoader和sun.misc.Launcher.AppClassLoader,這兩個(gè)類加載器分別被稱為拓展類加載器(Extension ClassLoader)和應(yīng)用類加載器(Application ClassLoader).如下圖所示:
圖例注釋:除了引導(dǎo)類加載器(Bootstrap Class Loader )的所有類加載器,都有一個(gè)能力,就是判斷某一個(gè)類是否被引導(dǎo)類加載器加載過,如果加載過,可以直接返回對(duì)應(yīng)的Class instance,如果沒有,則返回null.? 圖上的指向引導(dǎo)類加載器的虛線表示類加載器的這個(gè)有限的訪問 引導(dǎo)類加載器的功能。
此時(shí)的launcher.getClassLoader()方法將會(huì)返回AppClassLoader實(shí)例,AppClassLoader將ExtClassLoader作為自己的父加載器。
當(dāng)AppClassLoader加載類時(shí),會(huì)首先嘗試讓父加載器ExtClassLoader進(jìn)行加載,如果父加載器ExtClassLoader加載成功,則AppClassLoader直接返回父加載器ExtClassLoader加載的結(jié)果;如果父加載器ExtClassLoader加載失敗,AppClassLoader則會(huì)判斷該類是否是引導(dǎo)的系統(tǒng)類(即是否是通過Bootstrap類加載器加載,這會(huì)調(diào)用Native方法進(jìn)行查找);若要加載的類不是系統(tǒng)引導(dǎo)類,那么ClassLoader將會(huì)嘗試自己加載,加載失敗將會(huì)拋出“ClassNotFoundException”。
具體AppClassLoader的工作流程如下所示:
雙親委派模型(parent-delegation model):
上面討論的應(yīng)用類加載器AppClassLoader的加載類的模式就是我們常說的雙親委派模型(parent-delegation model).
對(duì)于某個(gè)特定的類加載器而言,應(yīng)該為其指定一個(gè)父類加載器,當(dāng)用其進(jìn)行加載類的時(shí)候:
1. 委托父類加載器幫忙加載;
2. 父類加載器加載不了,則查詢引導(dǎo)類加載器有沒有加載過該類;
3. 如果引導(dǎo)類加載器沒有加載過該類,則當(dāng)前的類加載器應(yīng)該自己加載該類;
4. 若加載成功,返回 對(duì)應(yīng)的Class 對(duì)象;若失敗,拋出異?!癈lassNotFoundException”。
請(qǐng)注意:
雙親委派模型中的"雙親"并不是指它有兩個(gè)父類加載器的意思,一個(gè)類加載器只應(yīng)該有一個(gè)父加載器。上面的步驟中,有兩個(gè)角色:
1. 父類加載器(parent classloader):它可以替子加載器嘗試加載類
2. 引導(dǎo)類加載器(bootstrap classloader): 子類加載器只能判斷某個(gè)類是否被引導(dǎo)類加載器加載過,而不能委托它加載某個(gè)類;換句話說,就是子類加載器不能接觸到引導(dǎo)類加載器,引導(dǎo)類加載器對(duì)其他類加載器而言是透明的。
一般情況下,雙親加載模型如下所示:
Step 4. 使用類加載器ClassLoader加載Main類
通過 launcher.getClassLoader()方法返回AppClassLoader實(shí)例,接著就是AppClassLoader加載 org.luanlouis.jvm.load.Main類的時(shí)候了。
[java]view plaincopy
ClassLoader?classloader?=?launcher.getClassLoader();//取得AppClassLoader類
classLoader.loadClass("org.luanlouis.jvm.load.Main");//加載自定義類
上述定義的org.luanlouis.jvm.load.Main類被編譯成org.luanlouis.jvm.load.Main class二進(jìn)制文件,這個(gè)class文件中有一個(gè)叫常量池(Constant Pool)的結(jié)構(gòu)體來存儲(chǔ)該class的常亮信息。常量池中有CONSTANT_CLASS_INFO類型的常量,表示該class中聲明了要用到那些類:
當(dāng)AppClassLoader要加載 org.luanlouis.jvm.load.Main類時(shí),會(huì)去查看該類的定義,發(fā)現(xiàn)它內(nèi)部聲明使用了其它的類: sun.security.pkcs11.P11Util、java.lang.Object、java.lang.System、java.io.PrintStream、java.lang.Class;org.luanlouis.jvm.load.Main類要想正常工作,首先要能夠保證這些其內(nèi)部聲明的類加載成功。所以AppClassLoader要先將這些類加載到內(nèi)存中。(注:為了理解方便,這里沒有考慮懶加載的情況,事實(shí)上的JVM加載類過程比這復(fù)雜的多)
加載順序:
1. 加載java.lang.Object、java.lang.System、java.io.PrintStream、java,lang.Class
AppClassLoader嘗試加載這些類的時(shí)候,會(huì)先委托ExtClassLoader進(jìn)行加載;而ExtClassLoader發(fā)現(xiàn)不是其加載范圍,其返回null;AppClassLoader發(fā)現(xiàn)父類加載器ExtClassLoader無法加載,則會(huì)查詢這些類是否已經(jīng)被BootstrapClassLoader加載過,結(jié)果表明這些類已經(jīng)被BootstrapClassLoader加載過,則無需重復(fù)加載,直接返回對(duì)應(yīng)的Class實(shí)例;
2. 加載sun.security.pkcs11.P11Util
此在{JRE_HOME}/lib/ext/sunpkcs11.jar包內(nèi),屬于ExtClassLoader負(fù)責(zé)加載的范疇。AppClassLoader嘗試加載這些類的時(shí)候,會(huì)先委托ExtClassLoader進(jìn)行加載;而ExtClassLoader發(fā)現(xiàn)其正好屬于加載范圍,故ExtClassLoader負(fù)責(zé)將其加載到內(nèi)存中。ExtClassLoader在加載sun.security.pkcs11.P11Util時(shí)也分析這個(gè)類內(nèi)都使用了哪些類,并將這些類先加載內(nèi)存后,才開始加載sun.security.pkcs11.P11Util,加載成功后直接返回對(duì)應(yīng)的Class實(shí)例;
3. 加載org.luanlouis.jvm.load.Main
AppClassLoader嘗試加載這些類的時(shí)候,會(huì)先委托ExtClassLoader進(jìn)行加載;而ExtClassLoader發(fā)現(xiàn)不是其加載范圍,其返回null;AppClassLoader發(fā)現(xiàn)父類加載器ExtClassLoader無法加載,則會(huì)查詢這些類是否已經(jīng)被BootstrapClassLoader加載過。而結(jié)果表明BootstrapClassLoader 沒有加載過它,這時(shí)候AppClassLoader只能自己動(dòng)手負(fù)責(zé)將其加載到內(nèi)存中,然后返回對(duì)應(yīng)的Class實(shí)例引用;
以上三步驟都成功,才表示classLoader.loadClass("org.luanlouis.jvm.load.Main")完成,上述操作完成后,JVM內(nèi)存方法區(qū)的格局會(huì)如下所示:
如上圖所示:
JVM方法區(qū)的類信息區(qū)是按照類加載器進(jìn)行劃分的,每個(gè)類加載器會(huì)維護(hù)自己加載類信息;
某個(gè)類加載器在加載相應(yīng)的類時(shí),會(huì)相應(yīng)地在JVM內(nèi)存堆(Heap)中創(chuàng)建一個(gè)對(duì)應(yīng)的Class,用來表示訪問該類信息的入口
Step 5. 使用Main類的main方法作為程序入口運(yùn)行程序
Step 6. 方法執(zhí)行完畢,JVM銷毀,釋放內(nèi)存
三、類加載器有哪些?其組織結(jié)構(gòu)是怎樣的?
類加載器(Class Loader):顧名思義,指的是可以加載類的工具。JVM自身定義了三個(gè)類加載器:引導(dǎo)類加載器(Bootstrap Class Loader)、拓展類加載器(Extension Class Loader )、應(yīng)用加載器(Application Class Loader)。當(dāng)然,我們有時(shí)候也會(huì)自己定義一些類加載器來滿足自身的需要。
引導(dǎo)類加載器(Bootstrap Class Loader): 該類加載器使JVM使用C/C++底層代碼實(shí)現(xiàn)的加載器,用以加載JVM運(yùn)行時(shí)所需要的系統(tǒng)類,這些系統(tǒng)類在{JRE_HOME}/lib目錄下。由于類加載器是使用平臺(tái)相關(guān)的底層C/C++語(yǔ)言實(shí)現(xiàn)的, 所以該加載器不能被Java代碼訪問到。但是,我們可以查詢某個(gè)類是否被引導(dǎo)類加載器加載過。我們經(jīng)常使用的系統(tǒng)類如:java.lang.String,java.lang.Object,java.lang*....... 這些都被放在 {JRE_HOME}/lib/rt.jar包內(nèi), 當(dāng)JVM系統(tǒng)啟動(dòng)的時(shí)候,引導(dǎo)類加載器會(huì)將其加載到 JVM內(nèi)存的方法區(qū)中。
拓展類加載器(Extension Class Loader): 該加載器是用于加載 java 的拓展類 ,拓展類一般會(huì)放在 {JRE_HOME}/lib/ext/ 目錄下,用來提供除了系統(tǒng)類之外的額外功能。拓展類加載器是是整個(gè)JVM加載器的Java代碼可以訪問到的類加載器的最頂端,即是超級(jí)父加載器,拓展類加載器是沒有父類加載器的。
應(yīng)用類加載器(Applocatoin Class Loader): 該類加載器是用于加載用戶代碼,是用戶代碼的入口。我經(jīng)常執(zhí)行指令 java?? xxx.x.xxx.x.x.XClass , 實(shí)際上,JVM就是使用的AppClassLoader加載 xxx.x.xxx.x.x.XClass 類的。應(yīng)用類加載器將拓展類加載器當(dāng)成自己的父類加載器,當(dāng)其嘗試加載類的時(shí)候,首先嘗試讓其父加載器-拓展類加載器加載;如果拓展類加載器加載成功,則直接返回加載結(jié)果Class instance,加載失敗,則會(huì)詢問是否引導(dǎo)類加載器已經(jīng)加載了該類;只有沒有加載的時(shí)候,應(yīng)用類加載器才會(huì)嘗試自己加載。由于xxx.x.xxx.x.x.XClass是整個(gè)用戶代碼的入口,在Java虛擬機(jī)規(guī)范中,稱其為 初始類(Initial Class).
用戶自定義類加載器(Customized Class Loader):用戶可以自己定義類加載器來加載類。所有的類加載器都要繼承java.lang.ClassLoader類。
四、雙親加載模型的邏輯和底層代碼實(shí)現(xiàn)是怎樣的?
上面已經(jīng)不厭其煩地講解什么是雙親加載模型,以及其機(jī)制是什么,這些東西都是可以通過底層代碼查看到的。
我們也可以通過JDK源碼看java.lang.ClassLoader的核心方法 loadClass()的實(shí)現(xiàn):
[java]view plaincopy
//提供class類的二進(jìn)制名稱表示,加載對(duì)應(yīng)class,加載成功,則返回表示該類對(duì)應(yīng)的Class?instance?實(shí)例
publicClass?loadClass(String?name)throwsClassNotFoundException?{
returnloadClass(name,false);
}
protectedClass?loadClass(String?name,booleanresolve)
throwsClassNotFoundException
{
synchronized(getClassLoadingLock(name))?{
//?首先,檢查是否已經(jīng)被當(dāng)前的類加載器記載過了,如果已經(jīng)被加載,直接返回對(duì)應(yīng)的Class實(shí)例
Class?c?=?findLoadedClass(name);
//初次加載
if(c?==null)?{
longt0?=?System.nanoTime();
try{
if(parent?!=null)?{
//如果有父類加載器,則先讓父類加載器加載
c?=?parent.loadClass(name,false);
}else{
//?沒有父加載器,則查看是否已經(jīng)被引導(dǎo)類加載器加載,有則直接返回
c?=?findBootstrapClassOrNull(name);
}
}catch(ClassNotFoundException?e)?{
//?ClassNotFoundException?thrown?if?class?not?found
//?from?the?non-null?parent?class?loader
}
//?父加載器加載失敗,并且沒有被引導(dǎo)類加載器加載,則嘗試該類加載器自己嘗試加載
if(c?==null)?{
//?If?still?not?found,?then?invoke?findClass?in?order
//?to?find?the?class.
longt1?=?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);
}
returnc;
}
}
相對(duì)應(yīng)地,我們可以整理出雙親模型的工作流程圖:
相信讀者看過這張圖后會(huì)對(duì)雙親加載模型有了非常清晰的脈絡(luò)。當(dāng)然,這是JDK自身默認(rèn)的加載類的行為,我們可以通過繼承復(fù)寫該方法,改變其行為。
五、類加載器與Class? 實(shí)例的關(guān)系
Java 任何一段代碼的執(zhí)行,都有對(duì)應(yīng)的線程上下文。如果我們?cè)诖a中,想看當(dāng)前是哪一個(gè)線程在執(zhí)行當(dāng)前代碼,我們經(jīng)常是使用如下方法:
[java]view plaincopy
Thread??thread?=?Thread.currentThread();//返回對(duì)當(dāng)當(dāng)前運(yùn)行線程的引用
相應(yīng)地,我們可以為當(dāng)前的線程指定類加載器。在上述的例子中, 當(dāng)執(zhí)行java??? org.luanlouis.jvm.load.Main的時(shí)候,JVM會(huì)創(chuàng)建一個(gè)Main線程,而創(chuàng)建應(yīng)用類加載器AppClassLoader的時(shí)候,會(huì)將AppClassLoader? 設(shè)置成Main線程的上下文類加載器:
[java]view plaincopy
publicLauncher()?{
Launcher.ExtClassLoader?var1;
try{
var1?=?Launcher.ExtClassLoader.getExtClassLoader();
}catch(IOException?var10)?{
thrownewInternalError("Could?not?create?extension?class?loader",?var10);
}
try{
this.loader?=?Launcher.AppClassLoader.getAppClassLoader(var1);
}catch(IOException?var9)?{
thrownewInternalError("Could?not?create?application?class?loader",?var9);
}
//將AppClassLoader設(shè)置成當(dāng)前線程的上下文加載器
Thread.currentThread().setContextClassLoader(this.loader);
//.......
}
線程上下文類加載器是從線程的角度來看待類的加載,為每一個(gè)線程綁定一個(gè)類加載器,可以將類的加載從單純的 雙親加載模型解放出來,進(jìn)而實(shí)現(xiàn)特定的加載需求。