首頁 > 軟體

Java ClassLoader虛擬類實現程式碼熱替換的範例程式碼

2022-06-29 14:02:39

總結

  1. 類載入器是負責載入類的物件。類ClassLoader是一個抽象類。給定類的全限定類名,類載入器應嘗試查詢或生成構成該類定義的資料Class檔案。典型的策略是將名稱轉換為檔名,然後從檔案系統中讀取該名稱的類檔案
  2. 每個Class物件都包含一個Class.getClassLoader()方法可以獲取到定義它的ClassLoader
  3. 陣列類的Class物件不是由類載入器建立的,而是根據Java執行時的要求自動建立的。getClassLoader()返回的陣列類的類裝入器與其元素型別的類裝入器相同,如果元素型別是基礎型別,則陣列類沒有類裝入器。
  4. 除了載入類之外,類載入器還負責定位資源。資源是一些資料(例如 .class檔案、設定資料或影象)。資源通常與應用程式或庫一起打包,以便可以通過應用程式或庫中的程式碼找到它們。
  5. ClassLoader類使用委託模型來搜尋類和資源。ClassLoader的每個範例都有一個關聯的父類別載入器。當請求查詢類或資源時,ClassLoader範例通常會在嘗試查詢類或資源本身之前,將對該類或資源的搜尋委託給其父類別裝入器。
  6. Java內建類載入器
載入器名方法名作用
Bootstrap class loader虛擬機器器的內建類載入器,底層是用C++實現的,沒有父載入器。主要載入系統環境的一些jar包和.class檔案。C/C++語言編寫,是虛擬機器器的一部分,無法在Java程式碼中直接獲取它的參照。可以通過 System.getProperty(“sun.boot.class.path”)獲取其載入路徑下的檔案
Platform class loader (也稱為ExtClassLoader)getPlatformClassLoader()平臺類載入器,負責載入JDK中一些特殊的模組。主要載入java.ext.dirs下的.class檔案
System class loader (也稱為AppClassLoader)getSystemClassLoader()系統類載入器,負責載入使用者類路徑上所指定的類庫。主要載入java.class.path下的.class檔案,是面向使用者編寫類的類載入器,即自己寫的類或者引入的第三方庫通常由此載入器載入
  1. 通常Java虛擬機器器以依賴於平臺的方式從本地檔案系統載入類。但是,有些類可能不是源於檔案;它們可能來自其他來源,如網路,也可能由應用程式構建。 方法defineClass(String name, byte[] b, int off, int len),將位元組陣列轉換為類class的範例。這個新定義的類的範例可以使用Class.newInstance()方法建立
  2. 類載入器建立的物件的方法和建構函式可以參照其他類。為了確定參照的類,Java虛擬機器器呼叫最初建立該類的類載入器的loadClass方法

ClassLoader 虛擬類方法

方法名作用
protected ClassLoader(String name, ClassLoader parent)建立指定名稱name的新類載入器,並使用指定的父類別載入器parent進行委派
public String getName()返回此類載入器的名稱,如果此類載入器未命名,則返回null
public Class loadClass(String name, boolean resolve)載入具有指定類名稱的類,resolve為true表示解析類參照。loadClass方法會先呼叫getClassLoadingLock方法獲取鎖,再呼叫findLoadedClass方法檢查類是否已載入,如果未載入,則往父載入器一直遞迴呼叫loadClass載入該類,如果父載入器也載入不了該類,才呼叫findClass方法獲取Class物件,而findClass是虛擬方法由子類實現。其實現使用defineClass(String name, byte[] b, int off, int len)方法可以將class檔案的位元組陣列轉為Class物件,最後使用resolveClass方法進行類的連結。而由於獲取該位元組陣列的方法是很多樣的,所以類載入的方式也非常多樣,如本地載入、網路載入、壓縮包中載入、自己構建Class檔案
protected Object getClassLoadingLock(String className)返回類載入操作的鎖物件。 如果此ClassLoader物件註冊為支援並行,則該方法返回與指定類名 className關聯的專用物件。否則該方法將返回此ClassLoader物件,即同一時間一個ClassLoader只能載入一個類
protected final Class findLoadedClass(String name)如果Java虛擬機器器已將此載入程式記錄為具有給定全限定類名稱的類的初始載入程式,則返回具有給定全限定類名稱的類。否則返回 null
protected Class findClass(String name)查詢具有指定全限定類名的類。這個方法是空方法應該被子類重寫,並且被呼叫在檢查請求類的父類別載入器之後,loadClass方法將呼叫這個方法
protected final Class<?> defineClass(String name, byte[] b, int off, int len, ProtectionDomain protectionDomain)將位元組陣列轉換為具有給定保護域ProtectionDomain的類Class的範例。 如果指定的name以“java.”開頭,它只能由getPlatformClassLoader()獲取到的平臺類載入器或其祖先定義(define),否則將丟擲SecurityException。如果name不是null,則它必須等於位元組陣列b指定的類的全限定類名稱,否則將丟擲NoClassDefFoundError
protected final Class<?> defineClass(String name, java.nio.ByteBuffer b, ProtectionDomain protectionDomain)將位元組緩衝區ByteBuffer轉換為具有給定保護域ProtectionDomain的類Class的範例。其餘和上面一樣
protected final void resolveClass(Class c)連結指定的類。類載入器可能會使用此方法連結類。如果類c已經被連結,那麼這個方法直接返回。 否則,將按照Java語言規範執行一章中的描述連結該類

實現程式碼熱替換

通過上面我們可以知道類載入流程是

  1. loadClass方法會先呼叫getClassLoadingLock方法獲取鎖
  2. 再呼叫findLoadedClass方法檢查類是否已載入,如果已經載入則直接獲取到該Class類物件,再判斷該Class類物件是否需要連結(resolve),如果要連結進入resolveClass,連結完後直接返回。連結就是執行類載入過程中的驗證、準備、解析這些過程
  3. 如果未載入,則往父載入器一直遞迴呼叫loadClass載入該類
  4. 如果父載入器也載入不了該類,才呼叫findClass方法獲取Class物件而findClass是空方法由子類重寫
  5. 其實現中會使用defineClass(String name, byte[] b, int off, int len)方法,該可以將class檔案的位元組陣列轉為Class物件
  6. 如果需要連結,最後使用resolveClass方法進行類的連結

實現

  1. 如果我們要實現程式碼熱替換,那麼就要使用defineClass方法載入新的類,所以最簡單的實現就是直接使用defineClass方法將新的Class檔案位元組陣列轉為Class物件,再使用反射建立新物件並執行新方法
  2. 但是defineClass是protected方法,所以我們只能繼承ClassLoader虛擬類才能呼叫該方法
  3. 最好的規範就是繼承ClassLoader虛擬類,並實現其loadClass方法和findClass方法,並在loadClass方法中呼叫findClass方法,在findClass方法中再呼叫defineClass方法
  4. 為了便於理解,直接拋棄規範,直接自己寫一個方法直接呼叫defineClass方法實現程式碼熱替換

專案結構,out是編譯出的class檔案目錄,由於Test就在src目錄下,沒有包名,則其全限定類名為Test

public class Test extends ClassLoader {
    public static void main(String[] args) throws Exception {
        while(true) {
            try {
                Test test = new Test();
                // 編譯後的class檔案位置 ./表示程式碼根目錄
                String classFile = "./out/production/Java_hot_replace/Test.class";
                FileInputStream fis = new FileInputStream(classFile);
                byte[] bytes = new byte[1024*10];
                int len = fis.read(bytes);
                //將位元組陣列轉為Class類物件 Test為全限定類名
                Class clazz = test.defineClass("Test", bytes, 0 ,len);
                //使用反射根據新的Class物件建立新物件,並執行其printStr方法
                Object object = clazz.newInstance();
                Method m = object.getClass().getMethod("printStr", new Class[] {});
                m.invoke(object, new Object[] {});
                Thread.sleep(2000);
            } catch(Exception e) {
                e.printStackTrace();
                try {
                    Thread.sleep(2000);
                } catch(InterruptedException ex) {

                }
            }
        }
    }

    public void printStr() {
        System.out.println("A");
    }
}

啟動後修改程式碼,然後點重新編譯

可以看到程式碼被熱替換了

改進思考

  • 正常實現流程應該為繼承ClassLoader虛擬類,並重寫其loadClass方法和findClass方法,並在loadClass方法中呼叫findClass方法,在findClass方法中再呼叫defineClass方法
  • 實現熱替換應該是替換修改過的程式碼,則應當維護一個Map<String, Long> 儲存從全限定類名到上次檔案修改時間的對映,每次定時掃描Class檔案目錄或檢測到儲存快捷鍵Ctrl+s時觸發掃描,檔案的屬性也有上次修改時間,拿我們儲存的和檔案的屬性比較即可知道檔案是否修改,即是否需要重新載入Class類
  • 熱替換產生了大量類資訊都儲存在jdk1.7的永久代,jdk1.8的元空間,如果無用的類資訊過多則會造成OOM,我們自定義類載入器和其產生的Class類物件,都可以通過置空(= null)使其不可達,然後呼叫System.gc()就可以解除安裝,即類似如下程式碼
public class Test extends ClassLoader {
   public static void main(String[] args) throws Exception {
     	MyClassLoader classLoader = new MyClassLoader();
 	 	Class classLoaded = classLoader.loadClass("MyClass");
  	 	classLoaded = null; 
  		classLoader = null; 
  	 	System.gc();  
  	}
}

到此這篇關於Java ClassLoader虛擬類實現程式碼熱替換的範例程式碼的文章就介紹到這了,更多相關Java ClassLoader程式碼熱替換內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com