1.背景
一個.java
檔案如何執行起來的。
在此之前,我們先了解下一些基本知識。
1.1 lib和dll
參考:LIB和DLL的區別與使用
大致就是說,lib是編譯時用到的,dll是執行時用到的。
在咱們的jdk資料夾中搜尋,是能找到一個jvm.dll
檔案的。
1.2 java.exe
jre檔案下一般有個java可執行檔案。
java.exe
是 Java 開發工具包(JDK)中的一個重要可執行檔案,用於啟動和執行 Java 應用程式。
它的主要作用是啟動 Java 虛擬機器(JVM),載入指定的類,並執行該類的 main
方法。
像linux下,就是一個名為java的可執行檔案。
2.類載入器
類載入器,無非就是載入我們的class檔案到jvm中,方便後續使用,先來記住幾個特點。
- 類載入的目的是將類class檔案讀入記憶體,併為之建立一個java.lang.Class物件。
- class檔案被載入到了記憶體之後,才能被其它class所引用。
- jvm啟動的時候,並不會一次性載入所有的class檔案,而是根據需要去動態載入。
- 類的唯一性由類載入器和類共同決定
下面列舉了Java中的幾個類載入器:
序號 | 類載入器 | 英文名 | 描述 | 典型載入位置 |
---|---|---|---|---|
1 | 引導類載入器 | Bootstrap ClassLoader | JVM 內建的最頂層類載入器,由原生代碼實現,不是 java.lang.ClassLoader 的子類。負責載入核心類庫。 |
通常是 <JAVA_HOME>/jre/lib 目錄中的 rt.jar 、resources.jar 等核心類庫 |
2 | 擴充套件類載入器 | Extension ClassLoader | 引導類載入器的子類載入器,負責載入 JRE 擴充套件目錄中的類庫。實現為 sun.misc.Launcher$ExtClassLoader 。 |
通常是 <JAVA_HOME>/jre/lib/ext 目錄中的 JAR 檔案 |
3 | 應用程式類載入器 | Application ClassLoader 或 System ClassLoader | 擴充套件類載入器的子類載入器,負責載入應用程式類路徑中的類。實現為 sun.misc.Launcher$AppClassLoader 。 |
由 -cp 或 -classpath 引數指定的類路徑中的類和 JAR 檔案 |
4 | 自定義類載入器 | Custom ClassLoaders | 使用者根據需要建立的類載入器,繼承自 java.lang.ClassLoader ,可以定義自己的類載入邏輯。 |
取決於使用者定義,通常用於載入特殊格式或特殊位置的類 |
好,什麼類載入器這種鳥語啊,抽象的很,理解成讀取class檔案的工具類就行了。
然後我們要注意一下,畢竟Java啟動了肯定要有個玩意初始化最開始的class啊,先得把雞抓過來才能生蛋。
-
最上面的引導類載入器(根載入器)是由咱們的虛擬機器啟動的,它先把重要的類都載入好,比如咱們的
Object
類(位於rt.jar
)。 -
根載入器同時載入了一個很重要的類
sun.misc.Launcher
(也位於rt.jar
),這個玩意的用處之一就是啟動咱們的擴充套件類載入器和應用程式類載入器。
2.1 引導類載入器
- JVM建立引導類載入器(Bootstrap ClassLoader),用於載入核心 Java 類庫。
- 引導類載入器載入並初始化 JVM 需要的基礎類(如
java.lang
、java.util
、java.io
、java.net
等)。
額,這個引導類載入器要載入啥呢,大概就是jre\lib
下面的那堆。
public class HelloWorld {
public static void main(String[] args) {
// 獲取並列印引導類載入器的載入路徑
String bootstrapClassPath = System.getProperty("sun.boot.class.path");
System.out.println("Bootstrap Class Loader Path:");
for (String path : bootstrapClassPath.split(";")) {
System.out.println(path);
}
}
}
輸出結果:
Bootstrap Class Loader Path:
D:\Toolkit\jdk\jdk-1.8\jre\lib\resources.jar
D:\Toolkit\jdk\jdk-1.8\jre\lib\rt.jar
D:\Toolkit\jdk\jdk-1.8\jre\lib\sunrsasign.jar
D:\Toolkit\jdk\jdk-1.8\jre\lib\jsse.jar
D:\Toolkit\jdk\jdk-1.8\jre\lib\jce.jar
D:\Toolkit\jdk\jdk-1.8\jre\lib\charsets.jar
D:\Toolkit\jdk\jdk-1.8\jre\lib\jfr.jar
D:\Toolkit\jdk\jdk-1.8\jre\classes
C:\Users\Administrator\AppData\Local\JetBrains\IntelliJIdea2022.3\captureAgent\debugger-agent.jar
像我們的Object
類,就在這個rt.jar
中。
2.2 擴充套件類載入器
這個玩意呢,就是咱們jre\lib\ext
資料夾下的一些東西。
public static void main(String[] args) {
String extClassPath = System.getProperty("java.ext.dirs");
System.out.println("ExtClassPath Class Loader Path:");
for (String path : extClassPath.split(";")) {
System.out.println(path);
}
}
輸出結果:
ExtClassPath Class Loader Path:
D:\Toolkit\jdk\jdk-1.8\jre\lib\ext
C:\WINDOWS\Sun\Java\lib\ext
2.3 應用程式類載入器
應用程式類載入器,就是要載入咱們自己寫的程式碼。平時咱們習慣在idea中編碼,實際上是idea幫我們傳遞進去的。
public static void main(String[] args) {
String appClassPath = System.getProperty("java.class.path");
System.out.println("AppClassPath Class Loader Path:");
for (String path : appClassPath.split(";")) {
System.out.println(path);
}
}
3.擴充套件
3.1 Launcher類
上文提到了,Launcher
類中初始化了擴充套件類載入器和應用程式類載入器。
可以在Launcher類中看到這兩個內部類。
這個try語句真的很影響閱讀啊,哎,我給你簡化下。
public Launcher() {
ExtClassLoader var1;
// 獲取擴充套件類載入器
var1= Launcher.ExtClassLoader.getExtClassLoader();
// 設定AppClassLoader的父載入器為ExtClassLoader
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
Thread.currentThread().setContextClassLoader(this.loader);
String var2 = System.getProperty("java.security.manager");
// ...
哈哈,這幾行程式碼看懵了吧?尤其是!
// 設定AppClassLoader的父載入器為ExtClassLoader
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
你這個註釋騙人的吧,getAppClassLoader()
這方法咋還把extClassLoader傳進去了?我們檢視下這個方法。
static class AppClassLoader extends URLClassLoader {
final URLClassPath ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);
// 傳入一個類載入器,返回一個新的類載入器
// 傳入的這個類載入器會被設定為返回值的這個載入器的父類載入器
public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
final String var1 = System.getProperty("java.class.path");
final File[] var2 = var1 == null ? new File[0] : Launcher.getClassPath(var1);
return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction<AppClassLoader>() {
public AppClassLoader run() {
URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2);
return new AppClassLoader(var1x, var0);
}
});
}
// ...
其他的你都不用關心,你就看這行返回的。
return new AppClassLoader(var1x, var0);
然後就是一頓點點點,我幫你點完了。
AppClassLoader
URLClassLoader
SecureClassLoader
ClassLoader
最後就到了這個頂層的ClassLoader類,然後賦值成員變數parent為這個傳入的ClassLoader。
所以,我們現在知道。
public static ClassLoader getAppClassLoader(final ClassLoader var0)
返回的這個新的ClassLoader中,其中有個parent欄位,值就是我們傳入的這個形參ClassLoader var0。
哎,怎麼驗證,不妨試試。
public static void main(String[] args) {
ClassLoader classLoader = Launcher.getLauncher().getClassLoader();
System.out.println();
}
debug可以看到,Launch類中的這個loader。
3.1 Launch類設定自己的loader為AppClassLoader
前面提到,Launch類是由根載入器載入的,然後,我們根據上面的程式碼可以看到,它又將自己的loader屬性設定為了AppClassLoader
。
注意這裡的getAppClassLoader實際上返回的是個AppClassLoader
,只不過它把自己的父類載入器設定成了extClassLoader,前文剛講了為啥哦。
所以,哇靠,明明是根載入器把你載入出來的,你反手給自己的loader設定成AppClassLoader幹嘛?
3.2 擴充套件類載入器的parent又在哪設定的
翻看Launch類的程式碼,似乎沒有找到設定擴充套件類父類載入器的這種操作,根據前面的知識我們可以知道,擴充套件類的父類載入器是根載入器。
額,誰搞的?
JVM設定的,在建立擴充套件類載入器時,將引導類載入器(null
表示引導類載入器)作為其父類載入器。
3.2 雙親委派
雙親委派機制(Parent-Delegate Model)是Java類載入器中採用的一種類載入策略。
如果一個類載入器收到了類載入請求,預設先將該請求委託給其父類載入器處理。只有當父級載入器無法載入該類時,才會嘗試自行載入。
主要目的是以下兩點:
- 避免核心類被修改
- 避免類被重複載入
這兩個比較好理解,這樣我們自己寫的java.lang.String
類,別人是不認的,因為根載入器那裡就載入出來String類了。
下面從程式碼層面來看下,載入類的時候,咋就從父類載入器先載入了。
2.2.1 ExtClassLoader
從extClassLoader類宣告中,我們可以看到它繼承了URLClassLoader。
它的loadClass
方法,是繼承的直接父類URLClassLoader的。
最後實際上用的還是ClassLoader
類的loadClass方法。
這裡還要注意一個點啊,最開始的那幾行程式碼。
Class<?> c = findLoadedClass(name);
if (c == null) {
// ...
}
這裡,也說明了,一個類咱們只會載入一次,如果載入過,直接就返回了。
2.2.2 AppClassLoader
AppClassLoader同樣繼承自URLClassLoader,不過它重寫了loadClass
方法。但是if嘛,就是加了些判斷而已,最後還是會回到super.loadClass()
。
4.例項分析
以HelloWorld.java為例。
4.1 編寫程式碼
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
4.2 編譯程式碼
使用 javac
編譯 Java 程式,生成位元組碼檔案 HelloWorld.class
:
javac HelloWorld.java
4.3 執行程式碼
java HelloWorld
4.4 解析命令列引數
java.exe
解析命令列引數,確定要執行的主類 HelloWorld
。
4.5 啟動 JVM
-
java.exe
呼叫底層的 JVM 動態連結庫(如jvm.dll
),建立並初始化 JVM 例項。 -
具體步驟包括設定 JVM 啟動引數(如類路徑)並呼叫
JNI_CreateJavaVM
函式等。
4.6 引導類載入器
引導類載入器,載入核心類。
4.7 應用程式類載入器
sun.misc.Launcher
中初始化的應用程式類載入器(AppClassLoader
),載入使用者編寫(我們編寫)的HelloWorld
類。
4.8 載入主類 HelloWorld
- 應用程式類載入器根據指定的主類名
HelloWorld
,在類路徑中查詢並載入HelloWorld.class
檔案。 - 類載入器將
HelloWorld
類的位元組碼載入到記憶體中,並進行解析和初始化。
4.9 查詢並呼叫 main 方法
- JVM 查詢
HelloWorld
類中的public static void main(String[] args)
方法。 - JVM 呼叫
main
方法,開始執行HelloWorld
程式。
4.10 程式結束
main
方法執行完畢,Java 程式執行結束。- JVM 進行資源清理,釋放記憶體,終止程序。
5.擴充套件
經典章節,背背面試題。
5.1 為啥Tomcat重新類載入器啊?
首先,咱們的tomcat裡面可能部署了好幾個服務,那裡面可能有相同的類。
- 應用1:
cn.yang37.Hello
- 應用2:
cn.yang37.Hello
這兩個Hello類不一樣很正常吧?欄位、方法等等都可能不一樣。
現在,按照預設的載入邏輯,應用1載入好了Hello類後,應用2中的Hello類就不能載入了。這個時候呢很簡單,應用2等死就好了吧。
所以,tomcat裡重寫了類載入器,針對不同的應用使用不同的類載入器。
核心點在於:實現類載入的隔離
還記得之前說過一句話嘛,類的唯一性由類載入器和類共同決定,你看咱們的Class類,裡面是有這個屬性的。