一篇帶給你JVM 類加載過程解析
類加載過程
類加載的時機(jī)
一個類型被加載到虛擬機(jī)內(nèi)存中開始,到卸載出內(nèi)存為止、它的整個生命周期將會經(jīng)歷加載、驗證、準(zhǔn)備、解析、初始化、使用、卸載七個階段。其中驗證、準(zhǔn)備、解析為連接
類被主動加載的 7 種情況
- 創(chuàng)建類的實(shí)例, 比如:new Object();
- 訪問某個類或接口的靜態(tài)變量,或者對該靜態(tài)變量賦值;
- 調(diào)用類的靜態(tài)方法;
- 反射(如 Class.forName("com.test.Test");
- 初始化一個類的子類;
- Java虛擬機(jī)啟動時被標(biāo)記為啟動類的類, 就是包含 main 方法的類(Java Test);
- JDK1.7開始提供的動態(tài)語言支持,java.lang.invoke.MethodHandle實(shí)例的解析結(jié)果REF_getStatic, REF_putStatic,;
REF_invokeStatic句柄對應(yīng)的類沒有被初始化則初始化。
其它加載情況
當(dāng) Java 虛擬機(jī)初始化一個類時,要求它所有的父類都被初始化,單這一條規(guī)則并不適用于接口。
- 在初始化一個類時,并不會先初始化它所實(shí)現(xiàn)的接口
- 在初始化一個接口時,并不會先初始化它的父類接口
- 因此,一個父接口并不會因為他的子接口或者實(shí)現(xiàn)了類的初始化而初始化,只有當(dāng)程序首次被使用特定接口的靜態(tài)變量時,才會導(dǎo)致該接口的初始化。
只有當(dāng)前程序訪問的靜態(tài)變量或靜態(tài)方法確實(shí)在當(dāng)前類或當(dāng)前接口定義時,才可認(rèn)為是對接口或類的主動使用。
調(diào)用 ClassLoader 類的 loadClass 方法加載一類,并不是對類的主動使用,不會導(dǎo)致類的初始化。
測試?yán)?1:
- public class Test_2 extends Test_2_A {
- static {
- System.out.println("子類靜態(tài)代碼塊");
- }
- {
- System.out.println("子類代碼塊");
- }
- public Test_2() {
- System.out.println("子類構(gòu)造方法");
- }
- public static void main(String[] args) {
- new Test_2();
- }
- }
- class Test_2_A {
- static {
- System.out.println("父類靜態(tài)代碼塊");
- }
- {
- System.out.println("父類代碼塊");
- }
- public Test_2_A() {
- System.out.println("父類構(gòu)造方法");
- }
- public static void find() {
- System.out.println("靜態(tài)方法");
- }
- }
- //代碼塊和構(gòu)造方法執(zhí)行順序
- //1).父類靜態(tài)代碼塊
- //2).子類靜態(tài)代碼塊
- //3).父類代碼塊
- //4).父類構(gòu)造方法
- //5).子類代碼塊
- //6).子類構(gòu)造方法
測試?yán)?2:
- public class Test_1 {
- public static void main(String[] args) {
- System.out.println(Test_1_B.str);
- }
- }
- class Test_1_A {
- public static String str = "A str";
- static {
- System.out.println("A Static Block");
- }
- }
- class Test_1_B extends Test_1_A {
- static {
- System.out.println("B Static Block");
- }
- }
- //輸出結(jié)果
- //A Static Block
- //A str
類加載流程
加載
在硬盤上查找并且通過 IO 讀入字節(jié)碼文件,使用到該類的時候才會被加載,例如調(diào)用 main 方法, new 關(guān)鍵字調(diào)用對象等,在加載階段會在內(nèi)存中生成這個類的 java.lang.Class 對象, 作為方法區(qū)這個類的各種數(shù)據(jù)的訪問入口。
驗證
校驗字節(jié)碼文件的正確性
準(zhǔn)備
給類的靜態(tài)變量分配內(nèi)存,并且賦予默認(rèn)值
解析
將符號引用替換為直接引用,該節(jié)點(diǎn)會把一些靜態(tài)方法(符號引用,比如 main() 方法)替換為指向數(shù)據(jù)所存內(nèi)存的指針或句柄等(直接引用),這就是所謂的靜態(tài)鏈接過程(類加載期間完成),動態(tài)鏈接是在程序運(yùn)行期間完成的將符號引用替換為直接引用。
初始化
對類的靜態(tài)變量初始化為指定的值,執(zhí)行靜態(tài)代碼塊。
類加載器
- **_引導(dǎo)類加載器(Bootstrap Class Loader) _**負(fù)責(zé)加載
\lib\ 目錄或者被 -Dbootclaspath 參數(shù)指定的類, 比如: rt.jar, tool.jar 等 。 - 拓展類加載器(Extension Class Loader) 負(fù)責(zé)加載
\lib\ext\ 或 -Djava.ext.dirs 選項所指定目錄下的類和 jar包。 - 應(yīng)用程序類加載器(System Class Loader) 負(fù)責(zé)加載 CLASSPATH 或 -Djava.class.path所指定的目錄下的類和 jar 包。
- 自定義類加載器:負(fù)責(zé)加載用戶自定義包路徑下的類包,通過 ClassLoader 的子類實(shí)現(xiàn) Class 的加載。
測試文件:
- public class TestJVMClassLoader {
- public static void main(String[] args) {
- System.out.println(String.class.getClassLoader());
- System.out.println(DESKeyFactory.class.getClassLoader());
- System.out.println(TestJVMClassLoader.class.getClassLoader());
- System.out.println();
- ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
- ClassLoader extClassLoader = appClassLoader.getParent();
- ClassLoader bootstrapClassLoader = extClassLoader.getParent();
- System.out.println("bootstrapClassLoader: " + bootstrapClassLoader);
- System.out.println("extClassLoader: " + extClassLoader);
- System.out.println("appClassLoader: " + appClassLoader);
- System.out.println();
- System.out.println("bootstrapLoader 加載以下文件:");
- URL[] urls = Launcher.getBootstrapClassPath().getURLs();
- for (URL url : urls) {
- System.out.println(url);
- }
- 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"));
- }
- }
雙親委派機(jī)制
什么是雙親委派機(jī)制?
一個類加載器收到了類加載的請求, 它首先不會自己去嘗試自己去加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此,因此所有的請求最終都應(yīng)該傳送到最頂層的啟動類加載器中,只有當(dāng)父加載器反饋?zhàn)约簾o法完成這個加載請求(即搜索范圍中沒有找到所需的類)時,子加載器才會嘗試自己完成加載。
類加載和雙親委派模型如下圖所示
我們再來看看 ClassLoader 類的 loadClass 方法
- // loadClass
- protected Class<?> loadClass(String name, boolean resolve)
- throws ClassNotFoundException
- {
- synchronized (getClassLoadingLock(name)) {
- // 首先檢查當(dā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();
- // 嘗試自己加載
- 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;
- }
- }
- // 類加載器的包含關(guān)系
- public abstract class ClassLoader {
- private static native void registerNatives();
- static {
- registerNatives();
- }
- // 當(dāng)前 ClassLoader 和 parent ClassLoader 的包含關(guān)系
- private final ClassLoader parent;
- }
總結(jié):
- 不是樹形結(jié)構(gòu)(只是邏輯樹形結(jié)構(gòu)),而是包含/包裝關(guān)系。
- 加載順序,應(yīng)用類加載器,拓展加載器,系統(tǒng)加載器。
- 如果有一個類加載器能夠成功加載 Test 類,那么這個類加載器被稱為定義類加載器,所有可能返回 Class 對象引用的類加載器(包括定義類加載器)都被稱為初始類加載器。
設(shè)計雙親委派機(jī)制的目的?
- 保證 Java 核心庫的類型安全:所有的java 應(yīng)用都會至少引用 java.lang.Object 類, 也就是說在運(yùn)行期, java.lang.Object 的這個類會被加載到 Java 虛擬機(jī)中,如果這個加載過程是由 Java 應(yīng)用自己的類加載器所完成的,那么很有可能會在 JVM 中存在多個版本的 java.lang.Object 類,而且這些類之間還是不兼容的?;ゲ豢梢姷?正是命名空間發(fā)揮著作用)借助于雙親委托機(jī)制,Java 核心庫中的類加載工作都是由啟動類加載器統(tǒng)一來完成的。從而確保了Java 應(yīng)用所使用的都是同一個版本的 Java 核心類庫,他們之間是相互兼容的。
- 可以確保 Java 核心庫所提供的類不會被自定義的類所替代。
- 不同的類加載器可以為相同類(binary name)的類創(chuàng)建額外的命名空間。相同名稱的類可以并存在Java虛擬機(jī)中,只需要不同的類加載器來加載他們即可,不同的類加載器的類之間是不兼容的,這相當(dāng)于在JAVA虛擬機(jī)內(nèi)部創(chuàng)建了一個又一個相互隔離的Java類空間,這類技術(shù)在很多框架中得到了實(shí)際運(yùn)用。
自定義類加載器
自定義類加載器加載類,下面是一個簡單的 Demo
- import java.io.ByteArrayOutputStream;
- import java.io.File;
- import java.io.FileInputStream;
- import java.io.InputStream;
- public class ClassLoaderTest extends ClassLoader {
- private static String rxRootPath;
- static {
- rxRootPath = "/temp/class/";
- }
- @Override
- public Class findClass(String name) {
- byte[] b = loadClassData(name);
- return defineClass(name, b, 0, b.length);
- }
- /**
- * 讀取 .class 文件為字節(jié)數(shù)組
- *
- * @param name 全路徑類名
- * @return
- */
- private byte[] loadClassData(String name) {
- try {
- String filePath = fullClassName2FilePath(name);
- InputStream is = new FileInputStream(new File(filePath));
- ByteArrayOutputStream bos = new ByteArrayOutputStream();
- byte[] buf = new byte[2048];
- int r;
- while ((r = is.read(buf)) != -1) {
- bos.write(buf, 0, r);
- }
- return bos.toByteArray();
- } catch (Throwable e) {
- e.printStackTrace();
- }
- return null;
- }
- /**
- * 全限定名轉(zhuǎn)換為文件路徑
- *
- * @param name
- * @return
- */
- private String fullClassName2FilePath(String name) {
- return rxRootPath + name.replace(".", "//") + ".class";
- }
- public static void main(String[] args) throws ClassNotFoundException {
- ClassLoaderTest classLoader = new ClassLoaderTest();
- String className = "com.test.TestAA";
- Class clazz = classLoader.loadClass(className);
- System.out.println(clazz.getClassLoader());
- // 輸出結(jié)果
- //cn.xxx.xxx.loader.ClassLoaderTest@3764951d
- }
- }
Tomcat 類加載器
Tomcat 中的類加載器模型
Tomcat 類加載器說明
tomcat 的幾個主要類加載器:
- commonLoader:Tomcat 最基本的類加載器, 加載路徑中的 class 可以被 Tomcat 容器本身以及各個 WebApp 訪問。
- catalinaLoader:Tomcat 容器私有的類加載器 加載路徑中的 class 對于 Webapp 不可見;
- sharaLoader: 各個Webapp 共享的類加載器, 加載路徑中的 class 對于所有 webapp 可見, 但是對于 Tomcat 容器不可見。
- webappLoader: 各個 Webapp 私有的類加載, 加載路徑中的 class 只對當(dāng)前 webapp 可見, 比如加載 war 包里面相關(guān)的類,每個 war 包應(yīng)用都有自己的 webappClassLoader 對象,對應(yīng)不同的命名空間,實(shí)現(xiàn)相互隔離,比如 war 包中可以引入不同的 spring 版本,實(shí)現(xiàn)多個 spring 版本 應(yīng)用的同時運(yùn)行。
總結(jié):
從圖中的委派關(guān)系中可以看出:
Commonclassloader 能加載的類都可以被 Catalinaclassloader和 Sharedclassloadert 使用, 從而實(shí)現(xiàn)了公有類庫的共用,而Catalinaclassloader 和 Sharedclassloader自己能加載的類則與對方相互隔離 Webappclassloader 可以使用 Shared Loader 加載到的類,但各個 Webappclassloader 實(shí)例之間相互隔離而 Jasper Loader 的加載范圍僅僅是這個 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)隔離性, 沒有遵守這個約定, 每個 webapp Loader加載自己的目錄下的 class'文件,不會傳遞給父類加載器,打破了雙親委派機(jī)制
參考資料
《深入理解 Java 虛擬機(jī)》 第三版 周志明
Apache Tomcat Documentation