類加載過程整體分析
當我們用java命令運行某個類的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)底層幫我們實現(xiàn)的,引導(dǎo)類加載器也不是由java編寫的。
在真正加載我們要運行的類之前要做很多準備工作,這其中很多地方都不是java語言所能處理的,因此不必做過多的探究。
那么類加載在加載類的過程中發(fā)生了哪些事情呢?大概可以分為以下七個階段:

- 加載:在硬盤上查找并通過IO讀入字節(jié)碼文件,使用到類時才會加載,例如調(diào)用類的main()方法,new對象等等,在加載階段會在內(nèi)存中生成一個代表這個類的java.lang.Class對象,作為方法區(qū)這個類的各種數(shù)據(jù)的訪問入口
- 驗證:校驗字節(jié)碼文件的正確性
- 準備:給類的靜態(tài)變量分配內(nèi)存,并賦予默認值
- 解析:將符號引用替換為直接引用,該階段會把一些靜態(tài)方法(符號引用,比如main()方法)替換為指向數(shù)據(jù)所存內(nèi)存的指針或句柄等(直接引用),這是所謂的靜態(tài)鏈接過程(類加載期間完成),動態(tài)鏈接是在程序運行期間完成的將符號引用替換為直接引用
- 初始化:對類的靜態(tài)變量初始化為指定的值,執(zhí)行靜態(tài)代碼塊
類加載過程.png
PS:類被加載到方法區(qū)中后主要包含 運行時常量池、類型信息、字段信息、方法信息、類加載器的引用、對應(yīng)class實例的引用等信息。
類加載器的引用:這個類到類加載器實例的引用
對應(yīng)class實例的引用:類加載器在加載類信息放到方法區(qū)中后,會創(chuàng)建一個對應(yīng)的Class 類型的對象實例放到堆(Heap)中, 作為開發(fā)人員訪問方法區(qū)中類定義的入口和切入點。
那么類是在jvm啟動時就全部加載了嗎?
答案是否定的,事實上,主類在運行過程中如果使用到其它類,會逐步加載這些類。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類********");
}
}
運行結(jié)果:
*************加載主啟動類************
*******加載A類********
*******初始化A類********
*******加載測試********
類加載器和雙親委派機制
上面的類加載過程主要是通過類加載器來實現(xiàn)的,Java里有如下幾種類加載器:
Bootstrp loader
Bootstrp加載器是用C++語言寫的,它是在Java虛擬機啟動后初始化的,它主要負責加載%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寫成的,它的實現(xiàn)類是 sun.misc.Launcher$AppClassLoader,另外我們知道ClassLoader中有個getSystemClassLoader方法,此方法返回的正是AppclassLoader.AppClassLoader主要負責加載classpath所指定的位置的類或者是jar文檔,它也是Java程序默認的類加載器。
類加載器初始化過程:
參見類運行加載全過程圖可知其中會創(chuàng)建JVM啟動器實例sun.misc.Launcher。 sun.misc.Launcher初始化使用了單例模式設(shè)計,保證一個JVM虛擬機內(nèi)只有一個 sun.misc.Launcher實例。 在Launcher構(gòu)造方法內(nèi)部,其創(chuàng)建了兩個類加載器,分別是 sun.misc.Launcher.ExtClassLoader(擴展類加載器)和sun.misc.Launcher.AppClassLoader(應(yīng) 用類加載器)。 JVM默認使用Launcher的getClassLoader()方法返回的類加載器AppClassLoader的實例加載我們 的應(yīng)用程序。
jdk源代碼如下:
//Launcher的構(gòu)造方法
public Launcher() {
Launcher.ExtClassLoader var1;
try {
//構(gòu)造擴展類加載器,在構(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);
雙親委派機制
前面說了,java中有三個類加載器,問題就來了,碰到一個類需要加載時,它們之間是如何協(xié)調(diào)工作的,即java是如何區(qū)分一個類該由哪個類加載器來完成呢。 在這里java采用了委托模型機制,這個機制簡單來講,就是“類裝載器有載入類的需求時,會先請示其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的觀點來看,邏輯上并不存在Bootstrap Loader的類實體,所以在java程序代碼里試圖打印出其內(nèi)容時,我們就會看到輸出為null。
我們來看下應(yīng)用程序類加載器AppClassLoader加載類的雙親委派機制源碼,AppClassLoader 的loadClass方法最終會調(diào)用其父類ClassLoader的loadClass方法,該方法的大體邏輯如下:
首先,檢查一下指定名稱的類是否已經(jīng)加載過,如果加載過了,就不需要再加載,直接 返回。
如果此類沒有加載過,那么,再判斷一下是否有父加載器;如果有父加載器,則由父加 載器加載(即調(diào)用parent.loadClass(name, false);).或者是調(diào)用bootstrap類加載器來加 載。
如果父加載器及bootstrap類加載器都沒有找到指定的類,那么調(diào)用當前類加載器的 findClass方法來完成類加載。
源代碼如下:
ClassLoader.java
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 檢查當前類加載器是否已經(jīng)加載了該類
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) { //如果當前加載器父加載器不為空則委托父加載器加載該類
c = parent.loadClass(name, false);
} else {//如果當前加載器父加載器為空則委托引導(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 {
//匹配被加載類的路徑和當前類加載器的加載路徑,看能否匹配到
String path = name.replace('.', '/').concat(".class");
Resource res = ucp.getResource(path, false);
if (res != null) {
try {
//如果能匹配到,就進行真正的類加載,
//就會執(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è)計雙親委派機制?
主要有以下2點原因:
- 沙箱安全機制:自己寫的java.lang.String.class類不會被加載,這樣便可以防止核心 API庫被隨意篡改
- 避免類的重復(fù)加載:當父親已經(jīng)加載了該類時,就沒有必要子ClassLoader再加載一 次,保證被加載類的唯一性
Tomcat打破雙親委派機制
以Tomcat類加載為例,Tomcat 如果使用默認的雙親委派類加載機制行不行?
我們思考一下:Tomcat是個web容器, 那么它要解決什么問題:
一個web容器可能需要部署兩個應(yīng)用程序,不同的應(yīng)用程序可能會依賴同一個第三方類庫的 不同版本,不能要求同一個類庫在同一個服務(wù)器只有一份,因此要保證每個應(yīng)用程序的類庫都是 獨立的,保證相互隔離。
部署在同一個web容器中相同的類庫相同的版本可以共享。否則,如果服務(wù)器有10個應(yīng)用程 序,那么要有10份相同的類庫加載進虛擬機。
web容器也有自己依賴的類庫,不能與應(yīng)用程序的類庫混淆?;诎踩紤],應(yīng)該讓容器的 類庫和程序的類庫隔離開來。
web容器要支持jsp的修改,我們知道,jsp 文件最終也是要編譯成class文件才能在虛擬機中 運行,但程序運行后修改jsp已經(jīng)是司空見慣的事情, web容器需要支持 jsp 修改后不用重啟。
再看看我們的問題:Tomcat 如果使用默認的雙親委派類加載機制行不行?
答案是不行的。為什么?
第一個問題,如果使用默認的類加載器機制,那么是無法加載兩個相同類庫的不同版本的,默認 的類加器是不管你是什么版本的,只在乎你的全限定類名,并且只有一份。
第二個問題,默認的類加載器是能夠?qū)崿F(xiàn)的,因為他的職責就是保證唯一性。
第三個問題和第一個問題一樣。
我們再看第四個問題,我們想我們要怎么實現(xiàn)jsp文件的熱加載,jsp 文件其實也就是class文 件,那么如果修改了,但類名還是一樣,類加載器會直接取方法區(qū)中已經(jīng)存在的,修改后的jsp 是不會重新加載的。那么怎么辦呢?我們可以直接卸載掉這jsp文件的類加載器,所以你應(yīng)該想 到了,每個jsp文件對應(yī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只對當前 Webapp可見,比如加載war包里相關(guān)的類,每個war包應(yīng)用都有自己的WebappClassLoader,實現(xiàn)相互隔離,比如不同war包應(yīng)用引入了不同的spring版本, 這樣實現(xiàn)就能加載各自的spring版本;
從圖中的委派關(guān)系中可以看出:
CommonClassLoader能加載的類都可以被CatalinaClassLoader和SharedClassLoader使用, 從而實現(xiàn)了公有類庫的共用,而CatalinaClassLoader和SharedClassLoader自己能加載的類則 與對方相互隔離。
WebAppClassLoader可以使用SharedClassLoader加載到的類,但各個WebAppClassLoader 實例之間相互隔離。
而JasperLoader的加載范圍僅僅是這個JSP文件所編譯出來的那一個.Class文件,它出現(xiàn)的目的 就是為了被丟棄:當Web容器檢測到JSP文件被修改時,會替換掉目前的JasperLoader的實例, 并通過再建立一個新的Jsp類加載器來實現(xiàn)JSP文件的熱加載功能。
tomcat 這種類加載機制違背了java 推薦的雙親委派模型了嗎?答案是:違背了。
很顯然,tomcat 不是這樣實現(xiàn),tomcat 為了實現(xiàn)隔離性,沒有遵守這個約定,每個 webappClassLoader加載自己的目錄下的class文件,不會傳遞給父類加載器,打破了雙親委 派機制。

關(guān)于類加載機制就分析到這里了,原創(chuàng)不易,覺得寫得不錯的話就點點贊關(guān)注關(guān)注唄,我的微信公眾號:java時光
