目錄
1、JVM加載運行全過程梳理
2、JVM Hotspot底層
3、war包、jar包如何加載
4、類加載器
我們來查看一下getLauncher:
1.我們先查看getExtClassLoader()
2、再來看看getAppClassLoader(extcl)
5、雙親委派機制
1.職責明確,路徑隔離?:?
2.那為什么要這么設計呢?我們再次來看看源碼:
3.那為什么非得從應用程序加載器開始呢?
4.雙親委派機制源碼剖析:
實現雙親委派機制:
5.為什么要實現雙親委派機制
6、全盤負責委托機制
7、自定義類加載器
1.核心流程
2.關鍵點
3.實際效果
1、JVM加載運行全過程梳理
當我們用java命令運行某個類的main函數啟動程序時,首先需要通過類加載器把主類加載到 JVM
代碼執行流程圖:
C++的啟動程序通過 JNI 啟動了一個Java虛擬機,并且JVM 內部用 C++ 實現的引導類加載器先加載核心類 ,然后 Java 層的?sun.misc.Launcher
?被初始化,創建擴展類加載器(ExtClassLoader)和應用類加載器(AppClassLoader),最終通過?loadClass()
?按雙親委派機制加載磁盤上的字節碼文件。?最后再調用Main方法
2、JVM Hotspot底層
HotSpot 主要集中在 JVM 初始化、類加載機制和字節碼執行
其中loadClass的類加載過程有如下幾步:
加載 >> 驗證 >> 準備 >> 解析 >> 初始化 >> 使用 >> 卸載
加載:在硬盤上查找并通過IO讀入字節碼文件,使用到類時才會加載,例如調用類的 main()方法,new對象等等,在加載階段會在內存中生成一個代表這個類的 java.lang.Class對象,作為方法區這個類的各種數據的訪問入口驗證:校驗字節碼文件的正確性準備:給類的靜態變量分配內存,并賦予默認值
驗證:驗證格式是否正確,比如開頭的cafe babe
準備:靜態變量做一個初始化賦值(final關鍵字變成常量不再是變量)
靜態變量類型?? | ??準備階段賦的默認值?? | ??示例?? |
---|---|---|
int ?/?long | 0 ?/?0L | static int x; ?→?x = 0 |
float ?/?double | 0.0f ?/?0.0d | static double y; ?→?y = 0.0 |
boolean | false | static boolean flag; ?→?flag = false |
引用類型 (如String ) | null | static String s; ?→?s = null |
final static常量 | ??直接賦代碼中的值?? | final static int z = 100; ?→?z = 100 |
解析:將符號引用替換為直接引用,該階段會把一些靜態方法(符號引用,比如 main()方法)替換為指向數據所存內存的指針或句柄等(直接引用),這是所謂的靜態鏈接過程(符號到內存地址的轉換)(類加載期間完成),動態鏈接是在程序運行期間完成的將符號引用替換為直接引用
初始化:對類的靜態變量初始化為指定的值,執行靜態代碼塊
jar包的Terminal打開可以輸入指令查看代碼信息(類、常量池...)?
javap -v xxx.class
3、war包、jar包如何加載
類被加載到方法區中后主要包含 運行時常量池、類型信息、字段信息、方法信息、類加載器的引用、對應class實例的引用等信息。
類加載器的引用:這個類到類加載器實例的引用對應class實例的引用:類加載器在加載類信息放到方法區中后,會創建一個對應的Class 類型的對象實例放到堆(Heap)中, 作為開發人員訪問方法區中類定義的入口和切入點。
注意,主類在運行過程中如果使用到其它類,會逐步加載這些類。 jar包或war包里的類不是一次性全部加載的,是使用到時才加載(懶加載)。
4、類加載器
上面的類加載過程主要是通過類加載器來實現的,Java里有如下幾種類加載器
- 引導類加載器??Bootstrap:負責加載支撐JVM運行的位于JRE的lib目錄下的核心類庫,比如 rt.jar、charsets.jar等
- 擴展類加載器??ExtClassLoader???:負責加載支撐JVM運行的位于JRE的lib目錄下的ext擴展目錄中的JAR 類包
- 應用程序類加載器AppClassLoader:負責加載ClassPath路徑下的類包,主要就是加載你自己寫的那些類
- 自定義加載器:負責加載用戶自定義路徑下的類包
public class TestJDKClassLoader {public static void main(String[] args) {// 1. 打印核心類、擴展類、應用類的加載器System.out.println(String.class.getClassLoader()); // null (Bootstrap)System.out.println(com.sun.crypto.provider.DESKeyFactory.class.getClassLoader().getClass().getName()); // ExtClassLoaderSystem.out.println(TestJDKClassLoader.class.getClassLoader().getClass().getName()); // AppClassLoader// 2. 獲取并打印類加載器層次ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();ClassLoader extClassloader = appClassLoader.getParent();ClassLoader bootstrapLoader = extClassloader.getParent(); // nullSystem.out.println("the bootstrapLoader : " + bootstrapLoader);System.out.println("the extClassloader : " + extClassloader);System.out.println("the appClassLoader : " + appClassLoader);// 3. 打印各加載器加載的路徑System.out.println("\nbootstrapLoader加載以下文件:");URL[] urls = Launcher.getBootstrapClassPath().getURLs();for (URL url : urls) {System.out.println(url);}System.out.println("\nextClassloader加載以下文件:");System.out.println(System.getProperty("java.ext.dirs"));System.out.println("\nappClassLoader加載以下文件:");System.out.println(System.getProperty("java.class.path"));}
}
運行結果:
35 null36 sun.misc.Launcher$ExtClassLoader37 sun.misc.Launcher$AppClassLoader3839 the bootstrapLoader : null40 the extClassloader : sun.misc.Launcher$ExtClassLoader@3764951d41 the appClassLoader : sun.misc.Launcher$AppClassLoader@14dad5dc4243 bootstrapLoader加載以下文件:
44 file:/D:/dev/Java/jdk1.8.0_45/jre/lib/resources.jar45 file:/D:/dev/Java/jdk1.8.0_45/jre/lib/rt.jar46 file:/D:/dev/Java/jdk1.8.0_45/jre/lib/sunrsasign.jar47 file:/D:/dev/Java/jdk1.8.0_45/jre/lib/jsse.jar48 file:/D:/dev/Java/jdk1.8.0_45/jre/lib/jce.jar49 file:/D:/dev/Java/jdk1.8.0_45/jre/lib/charsets.jar50 file:/D:/dev/Java/jdk1.8.0_45/jre/lib/jfr.jar51 file:/D:/dev/Java/jdk1.8.0_45/jre/classes
5253 extClassloader加載以下文件:
54 D:\dev\Java\jdk1.8.0_45\jre\lib\ext;C:\Windows\Sun\Java\lib\ext5556 appClassLoader加載以下文件:
57 D:\dev\Java\jdk1.8.0_45\jre\lib\charsets.jar;D:\dev\Java\jdk1.8.0_45\jre\lib\deploy.jar;D:\dev\Java\jdk1.8.0_45\jre\lib\ext\access‐bridge‐64.jar;D:\dev\Java\jdk1.8.0_45\jre\lib\ext\cldrdata.jar;D:\dev\Java\jdk1.8.0_45\jre\lib\ext\dnsns.jar;D:\dev\Java\jdk1.8.0_45\jre\lib\ext\jaccess.jar;D:\dev\Java\jdk1.8.0_45\jre\lib\ext\jfxrt.jar;D:\dev\Java\jdk1.8.0_45\jre\lib\ext\localedata.jar;D:\dev\Java\jdk1.8.0_45\jre\lib\ext\nashorn.jar;D:\dev\Java\jdk1.8.0_45\jre\lib\ext\sunec.jar;D:\dev\Java\jdk1.8.0_45\jre\lib\ext\sunjce_provider.jar;D:\dev\Java\jdk1.8.0_45\jre\lib\ext\sunmscapi.jar;D:\dev\Java\jdk1.8.0_45\jre\lib\ext\sunpkcs11.jar;D:ev\Java\jdk1.8.0_45\jre\lib\ext\zipfs.jar;D:\dev\Java\jdk1.8.0_45\jre\lib\javaws.ar;D:\dev\Java\jdk1.8.0_45\jre\lib\jce.jar;D:\dev\Java\jdk1.8.0_45\jre\lib\jfr.jar;D:\dev\Java\jdk1.8.0_45\jre\lib\jfxswt.jar;D:\dev\Java\jdk1.8.0_45\jre\lib\jsse.jar;D:\dev\Java\jdk1.8.0_45\jre\lib\management
agent.jar;D:\dev\Java\jdk1.8.0_45\jre\lib\plugin.jar;D:\dev\Java\jdk1.8.0_45\jre\lib\resources.jar;D:\dev\Java\jdk1.8.0_45\jre\lib\rt.jar;D:\ideaProjects\project‐all\target\classes;C:\Users\zhuge\.m2\repository\org\apache\zookeeper\zookeeper\3.4.12\zookeeper‐3.4.12.jar;C:\Users\zhuge\.m2\repository\org\slf4j\slf4j
api\1.7.25\slf4j‐api‐1.7.25.jar;C:\Users\zhuge\.m2\repository\org\slf4j\slf4j‐log4j12\1.7.25\slf4j‐log4j12
1.7.25.jar;C:\Users\zhuge\.m2\repository\log4j\log4j\1.2.17\log4j
1.2.17.jar;C:\Users\zhuge\.m2\repository\jline\jline\0.9.94\jline
0.9.94.jar;C:\Users\zhuge\.m2\repository\org\apache\yetus\audience
annotations\0.5.0\audience‐annotations‐0.5.0.jar;C:\Users\zhuge\.m2\repository\io\netty\netty\3.10.6.Final\netty‐3.10.6.Final.jar;C:\Users\zhuge\.m2\repository\com\google\guava\guava\22.0\guava‐22.0.jar;C:\Users\zhuge\.m2\repository\com\google\code\findbugs\jsr305\1.3.9\jsr305‐1.3.9.jar;C:\Users\zhuge\.m2\repository\com\google\errorprone\error_prone_annotations\2.0.18\error_prone_annotations‐2.0.18.jar;C:\Users\zhuge\.m2\repository\com\google\j2objc\j2objc‐annotations\1.1\j2objc‐annotations‐1.1.jar;C:\Users\zhuge\.m2\repository\org\codehaus\mojo\animal
sniffer‐annotations\1.14\animal‐sniffer‐annotations‐1.14.jar;D:\dev\IntelliJ IDEA 2018.3.2\lib\idea_rt.jar
我們來查看一下getLauncher:
我們先進入到Launcher.class,直接再idea搜索就行了
這時候你會發現他返回了一個launcher,我們追進去查看launcher怎么定義的
你會發現他早就初始化好了在加載階段的時候,是一個單例。接下來我們查看一下launcher的構造方法:
1.我們先查看getExtClassLoader()
你會發現extcl = ExtClassLoader.getExtClassLoader();獲取到了擴展類加載器,接下來我們去查看擴展類加載器是怎么初始化的:
他這里創建了一個實例,返回了一個實例,我們繼續追源碼
然后我們發現他在這返回了一個初始化的類加載器,他在初始化的時候還會調用他的父類URLClassLoader.java,這個類可以通過傳過來的磁盤文件路徑通過一些文件的讀寫加載到內存里面去
2、再來看看getAppClassLoader(extcl)
loader = AppClassLoader.getAppClassLoader(extcl);
這里的extcl是extcl = ExtClassLoader.getExtClassLoader();
?我們追入getAppClassLoader(extcl)查看:
其中final String s = System.getProperty("java.class.path");拿到我們的環境變量
最后他又返回了一個應用程序加載器return new AppClassLoader(urls, extcl);
同樣他也會調用URLClassLoader?
那extcl = ExtClassLoader.getExtClassLoader()到底去哪了呢?當我們不斷的追傳入的第二個參數
最終我們追到了ClassLoader:
找到了這個定義:private final ClassLoader parent;
所以AppClassLoader的parent是ExtClassLoader,這里不是父類加載器的關系,父類加載器是URLClassLoader(static class AppClassLoader extends URLClassLoader),而ExtClassLoader呢
他是空的,因為ExtClassLoader算是引導類加載器,引導類加載器是C++寫的
5、雙親委派機制
JVM類加載器是有親子層級結構的,如下圖
這里類加載其實就有一個雙親委派機制,加載某個類時會先委托父加載器尋找目標類,找不到再 委托上層父加載器加載,如果所有父加載器在自己的加載類路徑下都找不到目標類,則在自己的 類加載路徑中查找并載入目標類。
比如我們的Math類,最先會找應用程序類加載器加載,應用程序類加載器會先委托擴展類加載器加載,擴展類加載器再委托引導類加載器,頂層引導類加載器在自己的類(lib里面)加載路徑里找了半天 沒找到Math類,則向下退回加載Math類的請求,擴展類加載器收到回復就自己加載,在自己的類加載路徑里找了半天也沒找到Math類,又向下退回Math類的加載請求給應用程序類加載器, 應用程序類加載器于是在自己的類加載路徑(在?java.class.path
(用戶類路徑)中查找?.class
?文件或 JAR 包)里找Math類,結果找到了就自己加載了。。 雙親委派機制說簡單點就是,先找父親加載,不行再由兒子自己加載
1.職責明確,路徑隔離?:?
- ??Bootstrap?? 只加載?
JRE/lib
?下的核心類(如?java.lang.*
)。 - ??ExtClassLoader?? 只加載?
JRE/lib/ext
?下的擴展類。 - ??AppClassLoader?? 負責所有??用戶類路徑(
java.class.path
)??的類,包括:- 項目代碼(如?
com.example.MyClass
)。 - 第三方依賴(如 Maven/Gradle 引入的 JAR 包)。
- 項目代碼(如?
??只要類在用戶類路徑中存在,AppClassLoader
?一定能加載??,因為父加載器不會越權加載這些類。
2.那為什么要這么設計呢?我們再次來看看源碼:
當我去得到類加載器的時候:
C++語言在最終加載類的時候就會調用這個方法,獲得這個loader,從而加載應用程序的類(比如Math),這個laoder在初始化Launcher的時候就
?最終你可以發現還是先加載的AppClassLoader??應用類加載器
3.那為什么非得從應用程序加載器開始呢?
實際上,對于一個web程序來說,95%以上都是這個應用程序類加載器去加載,只有第一次加載的時候需要過這個流程:應用程序類加載器==>拓展類加載器==>引導類加載器==>拓展類加載器==>應用程序類加載器,后續再次去運行的時候,類已經加載到應用程序類加載器了,直接拿來用就行了,如果是從引導類加載器開始,那每次都要走到應用程序類加載器才行。?
4.雙親委派機制源碼剖析:
我們來看下應用程序類加載器AppClassLoader加載類的雙親委派機制源碼,AppClassLoader 的loadClass方法最終會調用其父類ClassLoader的loadClass方法,該方法的大體邏輯如下:
1.?首先,檢查一下指定名稱的類是否已經加載過,如果加載過了,就不需要再加載,直接 返回。
2.?如果此類沒有加載過,那么,再判斷一下是否有父加載器;如果有父加載器,則由父加 載器加載(即調用parent.loadClass(name,?false);).或者是調用bootstrap類加載器來加 載。
3.?如果父加載器及bootstrap類加載器都沒有找到指定的類,那么調用當前類加載器的 findClass方法來完成類加載?
實現雙親委派機制:
launcher下的loadClass類:
追到父類?:classLoader下的loadClass類:
重點來了,建議背下來!
1.會調用Class<?> c = findLoadedClass(name)方法來檢查是不是已經加載過了,加載過了就肯定不是0,就直接return c,追入findLoadedClass()方法你會發現調用的本地方法findLoadedClass0(),就是c++代碼:private native final Class<?> findLoadedClass0(String name);
2.當c是0也就是第一次加載的時候,會判斷還有沒有父類然后繼續判斷有沒有加載過,但此時在c = parent.loadClass(name, false)之后,已經是ExtClassLoader?了,同樣c是0,進入第一個if語句,然后ExtClassLoader?的parent是null!!,所以調findBootstrapClassOrNull這個方法,就是引導類加載器,底層也是C++,第一次加載肯定是null,之后就會進入c = findClass(name)這個方法,這個extClassLoader沒有findClass方法但是他的父類URLClassLoader有findClass方法,后續大部分都是本地方法,查看不了,但是第一次加載,返回的c肯定還是null,重點來了!!此時return c之后的出口是c = parent.loadClass(name, false),也就是第一個if之后,之后就回到了AppClassLoader,又會調用findclass()方法,也要調用父類URLClassLoader的findClass方法最終拿到目標類
5.為什么要實現雙親委派機制
- 沙箱安全機制:自己寫的java.lang.String.class類不會被加載,這樣便可以防止核心 API庫被隨意篡改
- 避免類的重復加載:當父親已經加載了該類時,就沒有必要子ClassLoader再加載一 次,保證被加載類的唯一性
?實例代碼:
package java.lang;23 public class String {4 public static void main(String[] args) {5 System.out.println("**************My String Class**************");6 }7 }89運行結果:
10錯誤: 在類 java.lang.String 中找不到 main 方法, 請將 main 方法定義為:11 public static void main(String[] args)12否則 JavaFX 應用程序類必須擴展javafx.application.Application
解釋:
由于雙親委派機制,??Bootstrap 永遠優先加載 JDK 核心類??,用戶自定義的同名類會被忽略:當這個String類從應用程序類加載器到拓展類加載器都沒找到,就回去引導類加載器找,結果找到了在JDK的在rt.jar包下,然后加載到jvm里面運行直接加載 JDK 的原生類,??不會加載用戶自定義的?String
?類?,沒有main()方法。
6、全盤負責委托機制
“全盤負責”是指當一個ClassLoder裝載一個類時,除非顯示的使用另外一個ClassLoder,該類所依賴及引用的類也由這個ClassLoder載入
7、自定義類加載器
自定義類加載器只需要繼承?java.lang.ClassLoader?類,該類有兩個核心方法,一個是 loadClass(String,?boolean),實現了雙親委派機制,還有一個方法是findClass,默認實現是空 方法,所以我們自定義類加載器主要是重寫findClass 方法。
public class MyClassLoaderTest {static class MyClassLoader extends ClassLoader {private String classPath;public MyClassLoader(String classPath) {this.classPath = classPath;}private byte[] loadByte(String name) throws Exception {name = name.replaceAll("\\.", "/");FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class");int len = fis.available();byte[] data = new byte[len];fis.read(data);fis.close();return data;}@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {try {byte[] data = loadByte(name);// defineClass將一個字節數組轉為Class對象// 這個字節數組是class文件讀取后最終的字節數組return defineClass(name, data, 0, data.length);} catch (Exception e) {e.printStackTrace();throw new ClassNotFoundException();}}}public static void main(String args[]) throws Exception {// 初始化自定義類加載器// 會先初始化父類ClassLoader,其中會把自定義類加載器的父加載器設置為應用程序類加載器AppClassLoaderMyClassLoader classLoader = new MyClassLoader("D:/test");// D盤創建 test/com/tuling/jvm 幾級目錄// 將User類的復制類User1.class丟入該目錄Class clazz = classLoader.loadClass("com.tuling.jvm.User1");// 1. 通過反射創建實例Object obj = clazz.newInstance(); // 相當于 new User1()// 2. 通過反射獲取方法Method method = clazz.getDeclaredMethod("sout", null); // 獲取無參的sout方法// 3. 通過反射調用方法method.invoke(obj, null); // 相當于 obj.sout()System.out.println(clazz.getClassLoader().getClass().getName());}
}/*
運行結果:
=======自己的加載器加載類調用方法=======
com.tuling.jvm.MyClassLoaderTest$MyClassLoader
*/
第一步:繼承ClassLoader
第二步:重寫findClass方法
1.核心流程
-
??創建自定義加載器??:
MyClassLoader classLoader = new MyClassLoader("D:/test");
這個加載器會從D盤的test文件夾找類文件
-
??加載類??:
Class clazz = classLoader.loadClass("com.tuling.jvm.User1");
加載User1類,實際查找路徑是:D:/test/com/tuling/jvm/User1.class
-
??運行類方法??:
Object obj = clazz.newInstance(); // 創建對象 Method method = clazz.getDeclaredMethod("sout", null); // 獲取sout方法 method.invoke(obj, null); // 調用方法
2.關鍵點
loadByte()類
:從磁盤上把類文件讀到一個字節數組里面findClass()
:最終把這個字節數字讀入到defineClass(name, data, 0, data.length)方法里面去,name是類名,data???磁盤上 .class 文件的二進制原始數據- 最終輸出證明類確實是由我們的自定義加載器加載的
3.實際效果
程序會:
- 自定義類加載器,類加載器的路徑就是
("D:/test")
- com.tuling.jvm.User1:在
D:/test的路徑下創建
com/tuling/jvm,然后把User1.class丟進去 - 自定義加載器就會從d盤加載這個類
- 打印出加載這個類的加載器名稱
輸出結果示例:
=======自己的加載器加載類調用方法=======
com.tuling.jvm.MyClassLoaderTest$MyClassLoader
自定義類加載器的默認父類類加載器是應用程序類加載器?
后續理由反射機制調用方法輸出:自己的加載器加載類的調用方法
理解:
- ??自定義加載器加載類??:
MyClassLoader
從指定路徑(D:/test
)加載User1.class
文件 - ??反射調用方法??:通過反射API調用加載類中的
sout()
方法
// 1. 通過反射創建實例
Object obj = clazz.newInstance(); // 相當于 new User1()// 2. 通過反射獲取方法
Method method = clazz.getDeclaredMethod("sout", null); // 獲取無參的sout方法// 3. 通過反射調用方法
method.invoke(obj, null); // 相當于 obj.sout()
這時候同學們可能忘記反射機制了,沒關系我帶大家用一個例子來復習一遍
1.準備一個簡單的類
public class User {private String name;private int age;public User() {this.name = "默認用戶";this.age = 18;}public User(String name, int age) {this.name = name;this.age = age;}public void printInfo() {System.out.println("用戶信息: " + name + ", " + age + "歲");}// getter和setter省略...
}
2.普通方法調用
public class NormalExample {public static void main(String[] args) {// 1. 直接使用new創建對象User user1 = new User();User user2 = new User("張三", 25);// 2. 直接調用方法user1.printInfo(); // 輸出: 用戶信息: 默認用戶, 18歲user2.printInfo(); // 輸出: 用戶信息: 張三, 25歲// 3. 直接訪問public字段(如果有的話)// user1.name = "李四"; // 如果name是public的// 4. 編譯時就能發現錯誤// User user3 = new User("參數錯誤"); // 編譯報錯,沒有匹配的構造方法}
}
?3.反射調用
import java.lang.reflect.*;public class ReflectionExample {public static void main(String[] args) throws Exception {// 1. 獲取Class對象Class<?> userClass = Class.forName("User");// 2. 創建對象(無參構造)Object user1 = userClass.newInstance();// 3. 創建對象(帶參構造)Constructor<?> constructor = userClass.getConstructor(String.class, int.class);Object user2 = constructor.newInstance("張三", 25);// 4. 調用方法Method printMethod = userClass.getMethod("printInfo");printMethod.invoke(user1); // 輸出: 用戶信息: 默認用戶, 18歲printMethod.invoke(user2); // 輸出: 用戶信息: 張三, 25歲// 5. 訪問私有字段Field nameField = userClass.getDeclaredField("name");nameField.setAccessible(true); // 突破private限制nameField.set(user1, "反射修改的名字");printMethod.invoke(user1); // 輸出: 用戶信息: 反射修改的名字, 18歲// 6. 運行時才會發現錯誤try {Constructor<?> wrongConstructor = userClass.getConstructor(String.class);Object user3 = wrongConstructor.newInstance("參數錯誤");} catch (NoSuchMethodException e) {System.out.println("運行時才發現構造方法不存在");}}
}
言歸正傳,接下來我們來探討最后輸出的那句話 :System.out.println(clazz.getClassLoader().getClass().getName());
為什么這句話輸出的加載器是AppClassLoader?
答案是因為AppClassLoader中也有這個類,當我們刪除AppClassLoader下的User1類就會輸出我們自己的加載器,這就是雙親委派機制!