文章目錄
Java 執行代碼的大致流程
我們先回顧下Java 執行代碼的大致流程
假設要執行A類的main方法
- 啓動虛擬機 (C++負責創建) 【windows : bin/java.exe調用 jvm.dll Linux : java 調用 libjvm.so 】
- 創建一個引導類加載器實例 (C++實現)
- C++ 調用Java代碼,創建JVM啓動器,實例sun.misc.Launcher 【這貨由引導加載器負責加載創建其他類加載器】
-
sun.misc.Launcher.getLauncher() 獲取運行類自己的加載器ClassLoader --> 是AppClassLoader , 通過上圖源碼可知
-
獲取到ClassLoader後調用loadClass(“A”)方法加載運行的類A
-
加載完成執行A類的main方法
-
程序運行結束
-
JVM銷燬
類加載loadClass的步驟
其中最核心的方法 loadClass ,其實現我們常說的雙親委派機制 ,我們後面展開。
我們先白話一下類加載的幾個步驟
加載 ----> 驗證 ----> 準備 ----> 解析 ----> 初始化 ----> 使用 ----> 卸載
談及比較多的是前五個 ,我們來捋一捋哈 ,不要嘗試死記硬背,嘗試去理解它的邏輯
- 加載: 我們說jvm執行的java字節碼,編譯後在磁盤上,總得讀取這個字節碼文件吧 ,通過啥讀 IO唄 , 所以第一步肯定是加載字節碼文件
- 驗證 : JVM總不能說讀到啥就直接運行了吧,你外面有個A.class 裏面是一堆JVM規範不認識的內容,也執行不了啊 。 符合JVM規範才能執行後續的步驟,所以第二步是 校驗字節碼文件的正確性
- 準備 : 給類的靜態變量分配內存,並賦予默認值。 我們的類裏,可能會包含一些靜態變量吧 。 比如
public static final int a = 12;
得給a分配個默認值 0 ,再比如public static User user = new User();
給 static的變量 User分配內存,並賦默認值null (final修飾的常量,直接賦值) - 解析 : 這個地方不是很好理解, 解析是什麼意思呢?將符號引用替換爲直接引用。 符號引用 ? 直接引用? what ? ------------- 我們的類的靜態方法 比如main方法 其實在Java中有個叫法 都是叫符號 。 這個階段就會吧 一些靜態方法(符號引用,比如剛纔說的main方法)替換爲指向數據所存內存的指針或者句柄等(直接引用)【找到具體在內存中的位置】。 這個就是靜態鏈接過程(在類加載期間完成)。 動態鏈接是在程序運行期間完成的將符號引用替換爲直接引用 (比如某個普通方法的調用)
- 初始化: 上面的步驟完事兒以後,這一步主要是對類的靜態變量初始化爲指定的值,執行靜態代碼塊。 比如剛纔第二步的
public static final int a = 12;
,第二步給static變量賦了默認值,這一步就該把12賦值給它了。 還有 static的 Userpublic static User user = new User();
實例化User
類加載器和雙親委派機制
剛纔說了類加載器中loadClass方法實現了雙親委派的機制,那我們需要先了解下有哪幾種類加載器
主要有4種
- 引導類加載器:負責加載支撐JVM運行的位於JRE的lib目錄下的核心類庫,比如rt.jar、charsets.jar等
- 擴展類加載器:負責加載支撐JVM運行的位於JRE的lib目錄下的ext擴展目錄中的JAR類包
- 應用程序類加載器:負責加載ClassPath路徑下的類包,主要就是加載我們應用中自己寫的那些類
- 自定義加載器:負責加載用戶自定義路徑下的類包
我們來看看 幾種不同的類加載器
public class ClassLoadTest {
public static void main(String[] args) {
// 核心rt.jar中的類加載器 是C++加載的,因此這裏爲null
System.out.println(String.class.getClassLoader());
// 擴展包的加載器 ExtClassLoader
System.out.println(com.sun.crypto.provider.DESKeyFactory.class.getClassLoader());
// 應用加載器 AppClassLoader
System.out.println(ClassLoadTest.class.getClassLoader());
System.out.println("");
// 獲取系統ClassLoader
ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
// appClassLoader的父加載器
ClassLoader extClassLoader = appClassLoader.getParent();
// extClassLoader的父加載器
ClassLoader boostrapClassLoader = extClassLoader.getParent();
System.out.println("the bootstrapLoader : " + boostrapClassLoader);
System.out.println("the extClassloader : " + extClassLoader);
System.out.println("the appClassLoader : "+ appClassLoader);
System.out.println("");
System.out.println("==============bootstrapLoader加載的文件====================");
URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
for (int i = 0; i < urLs.length; i++) {
System.out.println(urLs[i]);
}
System.out.println("");
System.out.println("==============extClassloader加載的文件====================");
System.out.println(System.getProperty("java.ext.dirs"));
System.out.println("");
System.out.println("==============appClassLoader 加載的文件====================");
System.out.println(System.getProperty("java.class.path"));
}
}
輸出
null
sun.misc.Launcher$ExtClassLoader@29453f44
sun.misc.Launcher$AppClassLoader@18b4aac2
the bootstrapLoader : null
the extClassloader : sun.misc.Launcher$ExtClassLoader@29453f44
the appClassLoader : sun.misc.Launcher$AppClassLoader@18b4aac2
==============bootstrapLoader加載的文件====================
file:/E:/Program%20Files/Java/jdk1.8.0_161/jre/lib/resources.jar
file:/E:/Program%20Files/Java/jdk1.8.0_161/jre/lib/rt.jar
file:/E:/Program%20Files/Java/jdk1.8.0_161/jre/lib/sunrsasign.jar
file:/E:/Program%20Files/Java/jdk1.8.0_161/jre/lib/jsse.jar
file:/E:/Program%20Files/Java/jdk1.8.0_161/jre/lib/jce.jar
file:/E:/Program%20Files/Java/jdk1.8.0_161/jre/lib/charsets.jar
file:/E:/Program%20Files/Java/jdk1.8.0_161/jre/lib/jfr.jar
file:/E:/Program%20Files/Java/jdk1.8.0_161/jre/classes
==============extClassloader加載的文件====================
E:\Program Files\Java\jdk1.8.0_161\jre\lib\ext;C:\WINDOWS\Sun\Java\lib\ext
==============appClassLoader 加載的文件====================
E:\Program Files\Java\jdk1.8.0_161\jre\lib\charsets.jar;E:\Program Files\Java\jdk1.8.0_161\jre\lib\deploy.jar;E:\Program Files\Java\jdk1.8.0_161\jre\lib\ext\access-bridge-64.jar;E:\Program Files\Java\jdk1.8.0_161\jre\lib\ext\cldrdata.jar;E:\Program Files\Java\jdk1.8.0_161\jre\lib\ext\dnsns.jar;E:\Program Files\Java\jdk1.8.0_161\jre\lib\ext\jaccess.jar;E:\Program Files\Java\jdk1.8.0_161\jre\lib\ext\jfxrt.jar;E:\Program Files\Java\jdk1.8.0_161\jre\lib\ext\localedata.jar;E:\Program Files\Java\jdk1.8.0_161\jre\lib\ext\nashorn.jar;E:\Program Files\Java\jdk1.8.0_161\jre\lib\ext\sunec.jar;E:\Program Files\Java\jdk1.8.0_161\jre\lib\ext\sunjce_provider.jar;E:\Program Files\Java\jdk1.8.0_161\jre\lib\ext\sunmscapi.jar;E:\Program Files\Java\jdk1.8.0_161\jre\lib\ext\sunpkcs11.jar;E:\Program Files\Java\jdk1.8.0_161\jre\lib\ext\zipfs.jar;E:\Program Files\Java\jdk1.8.0_161\jre\lib\javaws.jar;E:\Program Files\Java\jdk1.8.0_161\jre\lib\jce.jar;E:\Program Files\Java\jdk1.8.0_161\jre\lib\jfr.jar;E:\Program Files\Java\jdk1.8.0_161\jre\lib\jfxswt.jar;E:\Program Files\Java\jdk1.8.0_161\jre\lib\jsse.jar;E:\Program Files\Java\jdk1.8.0_161\jre\lib\management-agent.jar;E:\Program Files\Java\jdk1.8.0_161\jre\lib\plugin.jar;E:\Program Files\Java\jdk1.8.0_161\jre\lib\resources.jar;E:\Program Files\Java\jdk1.8.0_161\jre\lib\rt.jar;D:\IdeaProjects\GOF23\target\classes;C:\Program Files\JetBrains\IntelliJ IDEA 2019.3.1\lib\idea_rt.jar
看看appClassLoader 咋加載這麼多? 其實它並沒有加載這麼多,除了 D:\IdeaProjects\GOF23\target\classes; 是它加載的,剩下的都是他的父加載器給他乾的。
sun.misc.Launcher源碼解析
JVM啓動時,C++會實例化JVM啓動器實例sun.misc.Launcher ,所以很有必要研究一下Launcher的源碼 。
Launcher實例化
private static Launcher launcher = new Launcher();
採用了 餓漢模式 靜態域的方式 實現了單例模式 ,保證一個JVM虛擬機內只有一個sun.misc.Launcher實例。
Launcher 構造函數
實例化,調用構造函數,我們看下它的構造函數幹了啥?
public Launcher() {
Launcher.ExtClassLoader var1;
try {
//構造擴展類加載器,在構造的過程中將其父加載器設置爲null
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
//構造應用類加載器,在構造的過程中將其父加載器設置爲ExtClassLoader,
//Launcher的loader屬性值是AppClassLoader,我們一般都是用這個類加載器來加載我們自己寫的應用程序
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
Thread.currentThread().setContextClassLoader(this.loader);
.....
.....
.....
}
}
Launcher構造方法內部, 創建了兩個類加載器,分別是sun.misc.Launcher.ExtClassLoader(擴展類加載器)和sun.misc.Launcher.AppClassLoader(應用類加載器)。
JVM默認使用Launcher的getClassLoader()方法返回的類加載器AppClassLoader的實例加載我們的應用程序。
雙親委派機制 源碼解析
雙親委派過程
通俗的說: 當我們需要加載某個類時會先委託父加載器尋找目標類,找不到再委託上層父加載器加載,如果所有父加載器在自己的加載類路徑下都找不到目標類,則在自己的類加載路徑中查找並載入目標類。
舉個例子,我們有個類A.class ,最先會找應用程序類加載器AppClassLoader 加載,AppClassLoader 會先委託擴展類加載器ExtClassLoader加載,擴展類加載器再委託引導類加載器BootClassLoader,頂層引導類加載器BootClassLoader在自己的類加載路徑裏 沒找到A類,則向下退回加載A類的請求,擴展類加載器ExtClassLoader收到回覆就自己加載,在自己的類加載路徑裏找了半天也沒找到A類,又向下退回A類的加載請求給應用程序類加載器AppClassLoader ,應用程序類加載器 在自己的類加載路徑裏找A類,結果找到了就自己加載了。。
源碼解析 ClassLoader#loadClass
loadClass實現了雙親委派的功能,我們有必要好好的研究一下
既然都是委託向上查找,那我們來看下應用程序類加載器AppClassLoader加載類的雙親委派機制源碼,AppClassLoader的loadClass方法最終會調用其父類ClassLoader的loadClass方法
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
// 檢查當前類加載器是否已經加載了該類 ,加載直接返回
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
//如果當前加載器父加載器不爲空則委託父加載器加載該類
if (parent != null) {
c = parent.loadClass(name, false);
} else {
//如果當前加載器父加載器爲空則委託引導類加載器加載該類
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
//調用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;
}
}
看註釋~
總結一下幾個步驟
- 首先,檢查一下指定名稱的類是否已經加載過,如果加載過了,就不需要再加載,直接返回。
- 如果此類沒有加載過,那麼,再判斷一下是否有父加載器;如果有父加載器,則由父加載器加載(即調用parent.loadClass(name, false);).或者是調用bootstrap類加載器來加載。
- 如果父加載器及bootstrap類加載器都沒有找到指定的類,那麼調用當前類加載器的findClass方法 【調用URLClassLoader的findClass方法在加載器的類路徑裏查找並加載該類】來完成類加載。
雙親委派機制的優點
- 沙箱安全機制:比如我們自己寫的java.lang.String.class類不會被加載,這樣便可以防止核心API庫被隨意篡改
- 避免類的重複加載:當父親已經加載了該類時,就沒有必要子ClassLoader再加載一次,保證被加載類的唯一性
全盤負責委託機制
這個比較好理解
“全盤負責”是指當一個ClassLoder裝載一個類時,除非顯示的使用另外一個ClassLoder,該類所依賴及引用的類也由這個ClassLoder載入。
比如我們的類 A中引用了 類B,由於全盤負責委託機制 ,類B也將有加載類A的加載器來加載,除非你顯示的使用另外一個ClassLoder。