深入淺出類加載器及其用法

1 類產生的原因

在Linux上創建hello.c,使用gcc編譯器進行編譯gcc hello.c -o hello,得到綠色的可執行程序hello,執行./hello便會在控制檯輸出Hello World!

在這裏插入圖片描述

學過編譯原理的同學都清楚,整個編譯過程經歷了以下階段:
在這裏插入圖片描述

這種編譯方式的優點很明顯,就是可執行文件運行速度快。缺點也很明顯,不能跨平臺。1)硬件環境原因:如X86和ARM指令集不一樣,輸出的的彙編代碼不同,最後生成的機器碼也不一樣;2)操作系統原因:Windows編譯生產的*.exe可執行文件,放在Linux系統上就執行不了。

這時候,一種跨平臺的技術產生了,將源程序編譯爲平臺無關的字節碼,在程序運行時再轉換成具體的二進制本地機器碼(Native Code),從而達到跨平臺的目的。舉個簡單例子,你在Windows上編譯web項目生成的war包,放到Linux的tomcat上照常能跑起來。

在這裏插入圖片描述

2 類加載過程

整個類的生命週期主要包含以下7個階段:
在這裏插入圖片描述
1) 加載
查找並加載類的二進制數據。
2) 驗證
確保被加載類的正確性。
3) 準備
把類種的符號引用轉換爲直接引用。
4) 解析
把類中的符號引用轉換爲直接引用。
5) 初始化
爲類的靜態變量賦予正確的初始值。
6) 使用
根據類創建實例對象,如new一個實例。
7) 卸載
類的Class對象結束生命週期。這裏要特別注意的是由JVM自帶的三種類加載加載的類在虛擬機的整個生命週期中是不會被卸載的,由用戶自定義的類加載器所加載的類纔可以被卸載。

3 JVM自帶類加載器

JVM自帶的類加載器有3種,如表格所示:

加載器種類 實現語言 作用域
根(Bootstrap)類加載器 C++ $(JAVA_HOME)/lib目錄中或者被-Xbootclasspath參數指定的路徑
拓展(Extension)類加載器 Java $(JAVA_HOME)/lib/ext目錄中或者被java.ext.dirs系統變量指定的路徑
系統(System)類加載器 Java 一般是用戶自己編譯的類

類加載器就是用來加載類的,問題來了,這麼多種類加載器有什麼區別?答案是有區別的。類的加載遵循雙親委派模型,一個類加載器收到類加載請求,首先是委派給自己的父類加載器,只有父類加載器無法加載的條件下才自身才會嘗試去加載。因此,越是頂層的加載器,加載的越是基礎的包,這樣才能保證系統的可靠性和安全性, 如所有類的父類Object所在的java.lang包就是由根類加載器所加載。
在這裏插入圖片描述

3.1 查看類加載器

public class ClassloaderApp {
    public static void main(String[] args) throws Exception {
        System.out.println( String.class.getClassLoader());
        System.out.println(ClassloaderApp.class.getClassLoader());

    }
}

運行結果:
在這裏插入圖片描述

第一個打印結果爲null說明根類加載器可能使用null來表示,因此這個方法返回null。
AppClassLoader是系統類加載器,使用 null 來表示引導類加載器。

3.2 巧用類加載的作用域

有兩個類Student和Subject,依賴關係如圖所示:
在這裏插入圖片描述

/**
 * 學生類
 */
public class Student {
    public static void main(String[] args) {
        Subject.mostLikeSubejct();
        System.out.println("Student is loaded by" + Student.class.getClassLoader());
    }
}


/**
 *科目類
 */
public class Subject {
    public static void mostLikeSubejct(){
        System.out.println("I like English");
        System.out.println("Subject is loaded by" + Subject.class.getClassLoader());
    }

}

編譯:

root@ubuntu18:~/workspace/jvmstudy/target2/classes# javac Student.java 
root@ubuntu18:~/workspace/jvmstudy/target2/classes# javac Subject.java

打成jar包:

root@ubuntu18:~/workspace/jvmstudy/target2/classes#jar cfv Student.jar Student.class

將Subject.class添加到Student.jar中
在MANIFEST.MF末尾添加:
Main-Class: Student

運行:

root@ubuntu18:~/workspace/jvmstudy/target2/classes# java -jar Student.jar

控制檯打印結果如下,說明此時兩個類都是由系統類加載器加載。
在這裏插入圖片描述再新增一個同名的Subject類,愛好爲計算機了。此時,如何不改動Student.jar包,而替換舊的Subjct類呢?我們可以利用雙親委派模型和加載器的作用域進行控制。

/**
 *科目類
 */
public class Subject {
    public static void mostLikeSubejct(){
        System.out.println("I like CS");
        System.out.println("Subject is loaded by" + Subject.class.getClassLoader());
    }
}

1)方法1,使用根類加載器加載新的Subject類
打成jar包:

root@ubuntu18:~/workspace/jvmstudy/target2/classes#javac Subject.java Subject.class
root@ubuntu18:~/workspace/jvmstudy/target2/classes#jar cfv Subject.jar Subject.class

運行:

root@ubuntu18:~/workspace/jvmstudy/target2/classes# java -Xbootclasspath/a:/root/workspace/jvmstudy/target2/classes/Subject.jar: -jar Student.jar
 

結果:
在這裏插入圖片描述
1)方法2,使用拓展類加載器加載新的Subject類
運行:

root@ubuntu18:~/workspace/jvmstudy/target2/classes#mkdir ext
root@ubuntu18:~/workspace/jvmstudy/target2/classes#mv Subject.jar ext
root@ubuntu18:~/workspace/jvmstudy/target2/classes# java -Djava.ext.dirs=./ext/ Student

結果:
在這裏插入圖片描述

雖然存在兩個同名的Subject類,但是根據雙親委派模型,根類加載器和拓展類加載器的優先級別都高於應用類加載器,當JVM加載了一個Subject.class以後,Student.jar中同名的Subject.class是不會被加載的!

4 手動實現類加載器

在Java中,兩個全類名一樣的Class文件,只要加載他們的加載器不同,比如一個使用系統自帶類加載器,另外一個使用自定義類加載器,JVM就會加載2次這個類,否則就只能加載一次。

public class ClassLoaderTest {

    public static void main(String[] args) throws  Exception {

        //使用自定義類加載器
        ClassLoader myCL = new ClassLoader(){
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException{
                try {
                    String fileName = name.substring(name.lastIndexOf(".") + 1)+".class";

                    // /開頭代表從項目的ClassPath根下獲取資源
//                    InputStream is = getClass().getResourceAsStream("/classloader/" +fileName);

                    // 不以'/'開頭時,默認是指所在類的相對路徑
                    InputStream is = getClass().getResourceAsStream(fileName);
                    if(is == null){
                        return  super.loadClass(name);
                    }

                    byte[] bytes = new byte[is.available()];
                    is.read(bytes);
                    return  defineClass(name, bytes, 0, bytes.length);
                } catch (IOException e) {
                    e.printStackTrace();
                }


                return super.loadClass(name);
            }
        };

        //獲取加載類的一個對象
        Object instance = myCL.loadClass("classloader.ClassLoaderTest").newInstance();

        //返回instance對象運行時屬於哪個類
        System.out.println(instance.getClass());

        //測試一個對象是否爲一個類的實例
        System.out.println(instance instanceof classloader.ClassLoaderTest);
    }

}

備註:本例子的代碼是來自周志明老師那本經典的JVM寶書。

5 類加載器結合反射

JVM並不是一次性把所有代碼都加載到內存,而是需要使用的時候才進行類加載,這是Java鏈接過程的動態性。根據這個特點,我們可以在JVM運行過程中從文件系統、Jar包或者遠程Http服務器加載代碼,並使用反射進行調用。下面使用ClassLoader的一個子類URLClassLoader加載Jar包中的代碼爲例。

新建一個文件Hello.java

public class Hello {
    public Hello() {
        System.out.println("Hello Contruct!");
    }

    public static void say(){
        System.out.println("Hello World!");
    }

    public static void say(String str){
        System.out.println("Hello " + str);
    }

    public static void main(String[] args) {
        System.out.println(Hello.class.getClassLoader());
    }

}

編譯和打包

root@ubuntu18:~/workspace/jvmstudy/target2/classes# javac Hello.java 
root@ubuntu18:~/workspace/jvmstudy/target2/classes# jar cvf Hello.jar Hello.class 

打開Helo.jar,
在MANIFEST.MF末尾添加:
Main-Class: Hello

public class URLClassLoaderApp {

    public static void main(String[] args) throws Exception

    {

        URL url = new URL("file:/root/workspace/jvmstudy/target2/classes/Hello.jar");
        URLClassLoader loader = new URLClassLoader (new URL[] {url});
        Class<?> cl = Class.forName ("Hello", true, loader);

		//反射
        //無參方法
        Method sayMethod = cl.getMethod("say");
        sayMethod.invoke(null);

        //有參方法
        Method sayMethod2 = cl.getMethod("say", String.class);
        sayMethod2.invoke(null,"GZ");

        //構造函數
        cl.newInstance().toString();

		//將打開的資源全部釋放掉       
 		loader.close();

    }
}
 

運行結果:
在這裏插入圖片描述

5 參考文獻

[1] 深入理解Java虛擬機:JVM高級特性與最佳實踐 第3版.2019, 機械工業出版社
[2] C Primer Plus 第6版 中文版 第1版. 2016, 人們郵電出版社.
[3] JDK8 API文檔

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章