類加載過程整體分析
當(dāng)我們用java命令運(yùn)行某個類的main函數(shù)啟動程序時,首先需要通過類加載器把主類加載到 JVM
public class Math {
public static final int initData = 666;
public static User user = new User();
public int compute() { //一個方法對應(yīng)一塊棧幀內(nèi)存區(qū)域
int a = 1;
int b = 2;
int c = (a + b) * 10;
return c;
}
public static void main(String[] args) {
Math math = new Math();
math.compute();
}
}
通過Java命令執(zhí)行代碼的大體流程如下:
從上圖我們可以看出發(fā)起調(diào)用的地方是操作系統(tǒng)底層幫我們實(shí)現(xiàn)的,引導(dǎo)類加載器也不是由java編寫的察藐。
在真正加載我們要運(yùn)行的類之前要做很多準(zhǔn)備工作,這其中很多地方都不是java語言所能處理的舟扎,因此不必做過多的探究分飞。
那么類加載在加載類的過程中發(fā)生了哪些事情呢?大概可以分為以下七個階段:
- 加載:在硬盤上查找并通過IO讀入字節(jié)碼文件睹限,使用到類時才會加載譬猫,例如調(diào)用類的main()方法,new對象等等羡疗,在加載階段會在內(nèi)存中生成一個代表這個類的java.lang.Class對象染服,作為方法區(qū)這個類的各種數(shù)據(jù)的訪問入口
- 驗(yàn)證:校驗(yàn)字節(jié)碼文件的正確性
- 準(zhǔn)備:給類的靜態(tài)變量分配內(nèi)存,并賦予默認(rèn)值
- 解析:將符號引用替換為直接引用叨恨,該階段會把一些靜態(tài)方法(符號引用柳刮,比如main()方法)替換為指向數(shù)據(jù)所存內(nèi)存的指針或句柄等(直接引用),這是所謂的靜態(tài)鏈接過程(類加載期間完成)痒钝,動態(tài)鏈接是在程序運(yùn)行期間完成的將符號引用替換為直接引用
- 初始化:對類的靜態(tài)變量初始化為指定的值秉颗,執(zhí)行靜態(tài)代碼塊
PS:類被加載到方法區(qū)中后主要包含 運(yùn)行時常量池、類型信息送矩、字段信息蚕甥、方法信息、類加載器的引用栋荸、對應(yīng)class實(shí)例的引用等信息菇怀。
類加載器的引用:這個類到類加載器實(shí)例的引用
對應(yīng)class實(shí)例的引用:類加載器在加載類信息放到方法區(qū)中后夷家,會創(chuàng)建一個對應(yīng)的Class 類型的對象實(shí)例放到堆(Heap)中, 作為開發(fā)人員訪問方法區(qū)中類定義的入口和切入點(diǎn)。
那么類是在jvm啟動時就全部加載了嗎敏释?
答案是否定的,事實(shí)上摸袁,主類在運(yùn)行過程中如果使用到其它類钥顽,會逐步加載這些類。jar包或war包里的類不是一次性全部加載的靠汁,是使用到時才加載蜂大。請看下面例子:
public class TestDynamicLoad {
static {
System.out.println("*************加載主啟動類************");
}
public static void main(String[] args) {
new A();
System.out.println("*******加載測試********");
B b = null;//B不會加載,除非這里執(zhí)行new B();
}
}
class A{
static {
System.out.println("*******加載A類********");
}
public A() {
System.out.println("*******初始化A類********");
}
}
class B{
static {
System.out.println("*******加載B類********");
}
public B() {
System.out.println("*******初始化B類********");
}
}
運(yùn)行結(jié)果:
*************加載主啟動類************
*******加載A類********
*******初始化A類********
*******加載測試********
類加載器和雙親委派機(jī)制
上面的類加載過程主要是通過類加載器來實(shí)現(xiàn)的蝶怔,Java里有如下幾種類加載器:
Bootstrp loader
Bootstrp加載器是用C++語言寫的奶浦,它是在Java虛擬機(jī)啟動后初始化的,它主要負(fù)責(zé)加載%JAVA_HOME%/jre/lib
,-Xbootclasspath
參數(shù)指定的路徑以及%JAVA_HOME%/jre/classes
中的類踢星。ExtClassLoader
Bootstrp loader加載ExtClassLoader,并且將ExtClassLoader的父加載器設(shè)置為Bootstrp loader.ExtClassLoader是用Java寫的澳叉,具體來說就是 sun.misc.Launcher$ExtClassLoader,ExtClassLoader主要加載%JAVA_HOME%/jre/lib/ext
沐悦,此路徑下的所有classes目錄以及java.ext.dirs
系統(tǒng)變量指定的路徑中類庫成洗。AppClassLoader
Bootstrp loader加載完ExtClassLoader后,就會加載AppClassLoader,并且將AppClassLoader的父加載器指定為 ExtClassLoader藏否。AppClassLoader也是用Java寫成的瓶殃,它的實(shí)現(xiàn)類是 sun.misc.Launcher$AppClassLoader,另外我們知道ClassLoader中有個getSystemClassLoader
方法,此方法返回的正是AppclassLoader.AppClassLoader主要負(fù)責(zé)加載classpath所指定的位置的類或者是jar文檔副签,它也是Java程序默認(rèn)的類加載器遥椿。
類加載器初始化過程:
參見類運(yùn)行加載全過程圖可知其中會創(chuàng)建JVM啟動器實(shí)例sun.misc.Launcher。 sun.misc.Launcher初始化使用了單例模式設(shè)計(jì)淆储,保證一個JVM虛擬機(jī)內(nèi)只有一個 sun.misc.Launcher實(shí)例冠场。 在Launcher構(gòu)造方法內(nèi)部,其創(chuàng)建了兩個類加載器遏考,分別是 sun.misc.Launcher.ExtClassLoader(擴(kuò)展類加載器)和sun.misc.Launcher.AppClassLoader(應(yīng) 用類加載器)慈鸠。 JVM默認(rèn)使用Launcher的getClassLoader()方法返回的類加載器AppClassLoader的實(shí)例加載我們 的應(yīng)用程序。
jdk源代碼如下:
//Launcher的構(gòu)造方法
public Launcher() {
Launcher.ExtClassLoader var1;
try {
//構(gòu)造擴(kuò)展類加載器灌具,在構(gòu)造的過程中將其父加載器設(shè)置為null
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
//構(gòu)造應(yīng)用類加載器青团,在構(gòu)造的過程中將其父加載器設(shè)置為ExtClassLoader,
//Launcher的loader屬性值是AppClassLoader咖楣,我們一般都是用這個類加載器來加載我們自己寫的應(yīng)用程序
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
Thread.currentThread().setContextClassLoader(this.loader);
雙親委派機(jī)制
前面說了督笆,java中有三個類加載器,問題就來了诱贿,碰到一個類需要加載時娃肿,它們之間是如何協(xié)調(diào)工作的咕缎,即java是如何區(qū)分一個類該由哪個類加載器來完成呢。 在這里java采用了委托模型機(jī)制料扰,這個機(jī)制簡單來講凭豪,就是“類裝載器有載入類的需求時,會先請示其Parent使用其搜索路徑幫忙載入晒杈,如果Parent 找不到,那么才由自己依照自己的搜索路徑搜索類”
下面舉一個例子來說明嫂伞,為了更好的理解,先弄清楚幾行代碼:
Public class Test{
Public static void main(String[] arg){
ClassLoader c = Test.class.getClassLoader(); //獲取Test類的類加載器
System.out.println(c);
ClassLoader c1 = c.getParent(); //獲取c這個類加載器的父類加載器
System.out.println(c1);
ClassLoader c2 = c1.getParent();//獲取c1這個類加載器的父類加載器
System.out.println(c2);
}
}
結(jié)果:
……AppClassLoader……
……ExtClassLoader……
Null
可以看出Test是由AppClassLoader加載器加載的拯钻,AppClassLoader的Parent
加載器是 ExtClassLoader,但是ExtClassLoader
的Parent
為 null
是怎么回事呵帖努,朋友們留意的話,前面有提到Bootstrap Loader是用C++語言寫的粪般,依java的觀點(diǎn)來看拼余,邏輯上并不存在Bootstrap Loader的類實(shí)體,所以在java
程序代碼里試圖打印出其內(nèi)容時亩歹,我們就會看到輸出為null
匙监。
我們來看下應(yīng)用程序類加載器AppClassLoader加載類的雙親委派機(jī)制源碼,AppClassLoader 的loadClass方法最終會調(diào)用其父類ClassLoader的loadClass方法小作,該方法的大體邏輯如下:
首先舅柜,檢查一下指定名稱的類是否已經(jīng)加載過,如果加載過了躲惰,就不需要再加載致份,直接 返回。
如果此類沒有加載過础拨,那么氮块,再判斷一下是否有父加載器;如果有父加載器诡宗,則由父加 載器加載(即調(diào)用parent.loadClass(name, false);).或者是調(diào)用bootstrap類加載器來加 載滔蝉。
如果父加載器及bootstrap類加載器都沒有找到指定的類,那么調(diào)用當(dāng)前類加載器的 findClass方法來完成類加載塔沃。
源代碼如下:
ClassLoader.java
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 檢查當(dāng)前類加載器是否已經(jīng)加載了該類
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) { //如果當(dāng)前加載器父加載器不為空則委托父加載器加載該類
c = parent.loadClass(name, false);
} else {//如果當(dāng)前加載器父加載器為空則委托引導(dǎo)類加載器加載該類
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();
//都會調(diào)用URLClassLoader的findClass方法在加載器的類路徑里查找并加載該類
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;
}
}
URLClassLoader.java
protected Class<?> findClass(final String name)
throws ClassNotFoundException
{
final Class<?> result;
try {
result = AccessController.doPrivileged(
new PrivilegedExceptionAction<Class<?>>() {
public Class<?> run() throws ClassNotFoundException {
//匹配被加載類的路徑和當(dāng)前類加載器的加載路徑蝠引,看能否匹配到
String path = name.replace('.', '/').concat(".class");
Resource res = ucp.getResource(path, false);
if (res != null) {
try {
//如果能匹配到,就進(jìn)行真正的類加載蛀柴,
//就會執(zhí)行前面說的類加載的幾個階段
return defineClass(name, res);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
} else {
return null;
}
}
}, acc);
} catch (java.security.PrivilegedActionException pae) {
throw (ClassNotFoundException) pae.getException();
}
if (result == null) {
throw new ClassNotFoundException(name);
}
return result;
}
那么為什么要設(shè)計(jì)雙親委派機(jī)制螃概?
主要有以下2點(diǎn)原因:
- 沙箱安全機(jī)制:自己寫的java.lang.String.class類不會被加載,這樣便可以防止核心 API庫被隨意篡改
- 避免類的重復(fù)加載:當(dāng)父親已經(jīng)加載了該類時鸽疾,就沒有必要子ClassLoader再加載一 次吊洼,保證被加載類的唯一性
Tomcat打破雙親委派機(jī)制
以Tomcat類加載為例,Tomcat 如果使用默認(rèn)的雙親委派類加載機(jī)制行不行制肮?
我們思考一下:Tomcat是個web容器冒窍, 那么它要解決什么問題:
一個web容器可能需要部署兩個應(yīng)用程序递沪,不同的應(yīng)用程序可能會依賴同一個第三方類庫的 不同版本,不能要求同一個類庫在同一個服務(wù)器只有一份综液,因此要保證每個應(yīng)用程序的類庫都是 獨(dú)立的款慨,保證相互隔離。
部署在同一個web容器中相同的類庫相同的版本可以共享谬莹。否則樱调,如果服務(wù)器有10個應(yīng)用程 序,那么要有10份相同的類庫加載進(jìn)虛擬機(jī)届良。
web容器也有自己依賴的類庫,不能與應(yīng)用程序的類庫混淆圣猎∈亢基于安全考慮,應(yīng)該讓容器的 類庫和程序的類庫隔離開來送悔。
web容器要支持jsp的修改慢显,我們知道,jsp 文件最終也是要編譯成class文件才能在虛擬機(jī)中 運(yùn)行欠啤,但程序運(yùn)行后修改jsp已經(jīng)是司空見慣的事情荚藻, web容器需要支持 jsp 修改后不用重啟。
再看看我們的問題:Tomcat 如果使用默認(rèn)的雙親委派類加載機(jī)制行不行洁段?
答案是不行的应狱。為什么?
第一個問題祠丝,如果使用默認(rèn)的類加載器機(jī)制疾呻,那么是無法加載兩個相同類庫的不同版本的,默認(rèn) 的類加器是不管你是什么版本的写半,只在乎你的全限定類名岸蜗,并且只有一份。
第二個問題叠蝇,默認(rèn)的類加載器是能夠?qū)崿F(xiàn)的璃岳,因?yàn)樗穆氊?zé)就是保證唯一性。
第三個問題和第一個問題一樣悔捶。
我們再看第四個問題铃慷,我們想我們要怎么實(shí)現(xiàn)jsp文件的熱加載,jsp 文件其實(shí)也就是class文 件蜕该,那么如果修改了枚冗,但類名還是一樣,類加載器會直接取方法區(qū)中已經(jīng)存在的蛇损,修改后的jsp 是不會重新加載的赁温。那么怎么辦呢坛怪?我們可以直接卸載掉這jsp文件的類加載器,所以你應(yīng)該想 到了股囊,每個jsp文件對應(yīng)一個唯一的類加載器袜匿,當(dāng)一個jsp文件修改了,就直接卸載這個jsp類加載 器稚疹。重新創(chuàng)建類加載器居灯,重新加載jsp文件。
Tomcat自定義加載器詳解
tomcat的幾個主要類加載器:
commonLoader:Tomcat最基本的類加載器内狗,加載路徑中的class可以被Tomcat容 器本身以及各個Webapp訪問怪嫌;
catalinaLoader:Tomcat容器私有的類加載器,加載路徑中的class對于Webapp不 可見柳沙;
sharedLoader:各個Webapp共享的類加載器岩灭,加載路徑中的class對于所有 Webapp可見,但是對于Tomcat容器不可見赂鲤;
WebappClassLoader:各個Webapp私有的類加載器噪径,加載路徑中的class只對當(dāng)前 Webapp可見,比如加載war包里相關(guān)的類数初,每個war包應(yīng)用都有自己的WebappClassLoader找爱,實(shí)現(xiàn)相互隔離,比如不同war包應(yīng)用引入了不同的spring版本泡孩, 這樣實(shí)現(xiàn)就能加載各自的spring版本车摄;
從圖中的委派關(guān)系中可以看出:
CommonClassLoader能加載的類都可以被CatalinaClassLoader和SharedClassLoader使用, 從而實(shí)現(xiàn)了公有類庫的共用仑鸥,而CatalinaClassLoader和SharedClassLoader自己能加載的類則 與對方相互隔離练般。
WebAppClassLoader可以使用SharedClassLoader加載到的類,但各個WebAppClassLoader 實(shí)例之間相互隔離锈候。
而JasperLoader的加載范圍僅僅是這個JSP文件所編譯出來的那一個.Class文件薄料,它出現(xiàn)的目的 就是為了被丟棄:當(dāng)Web容器檢測到JSP文件被修改時,會替換掉目前的JasperLoader的實(shí)例泵琳, 并通過再建立一個新的Jsp類加載器來實(shí)現(xiàn)JSP文件的熱加載功能摄职。
tomcat 這種類加載機(jī)制違背了java 推薦的雙親委派模型了嗎?答案是:違背了获列。
很顯然谷市,tomcat 不是這樣實(shí)現(xiàn),tomcat 為了實(shí)現(xiàn)隔離性击孩,沒有遵守這個約定迫悠,每個 webappClassLoader加載自己的目錄下的class文件,不會傳遞給父類加載器巩梢,打破了雙親委 派機(jī)制创泄。
關(guān)于類加載機(jī)制就分析到這里了艺玲,原創(chuàng)不易,覺得寫得不錯的話就點(diǎn)點(diǎn)贊關(guān)注關(guān)注唄鞠抑,我的微信公眾號:java時光