JVM自定義類載入器載入指定classPath下的所有class及jar

来源:https://www.cnblogs.com/chenerzhu/archive/2018/10/04/9741883.html
-Advertisement-
Play Games

一、JVM中的類載入器類型 從Java虛擬機的角度講,只有兩種不同的類載入器:啟動類載入器和其他類載入器。 1.啟動類載入器(Boostrap ClassLoader):這個是由c++實現的,主要負責JAVA_HOME/lib目錄下的核心 api 或 -Xbootclasspath 選項指定的jar ...


一、JVM中的類載入器類型

  從Java虛擬機的角度講,只有兩種不同的類載入器:啟動類載入器和其他類載入器。
  1.啟動類載入器(Boostrap ClassLoader):這個是由c++實現的,主要負責JAVA_HOME/lib目錄下的核心 api 或 -Xbootclasspath 選項指定的jar包裝入工作。
  2.其他類載入器:由java實現,可以在方法區找到其Class對象。這裡又細分為幾個載入器
    a).擴展類載入器(Extension ClassLoader):負責用於載入JAVA_HOME/lib/ext目錄中的,或者被-Djava.ext.dirs系統變數指定所指定的路徑中所有類庫(jar),開發者可以直接使用擴展類載入器。java.ext.dirs系統變數所指定的路徑的可以通過System.getProperty("java.ext.dirs")來查看。
    b).應用程式類載入器(Application ClassLoader):負責java -classpath或-Djava.class.path所指的目錄下的類與jar包裝入工作。開發者可以直接使用這個類載入器。在沒有指定自定義類載入器的情況下,這就是程式的預設載入器。
    c).自定義類載入器(User ClassLoader):在程式運行期間, 通過java.lang.ClassLoader的子類動態載入class文件, 體現java動態實時類裝入特性。


  這四個類載入器的層級關係,如下圖所示。

      

 

二、為什麼要自定義類載入器

  1. 區分同名的類:假定在tomcat 應用伺服器,上面部署著許多獨立的應用,同時他們擁有許多同名卻不同版本的類。要區分不同版本的類當然是需要每個應用都擁有自己獨立的類載入器了,否則無法區分使用的具體是哪一個。
  2. 類庫共用:每個web應用在tomcat中都可以使用自己版本的jar。但存在如Servlet-api.jar,java原生的包和自定義添加的Java類庫可以相互共用。
  3. 加強類:類載入器可以在 loadClass 時對 class 進行重寫和覆蓋,在此期間就可以對類進行功能性的增強。比如使用javassist對class進行功能添加和修改,或者添加面向切麵編程時用到的動態代理,以及 debug 等原理。
  4. 熱替換:在應用正在運行的時候升級軟體,不需要重新啟動應用。比如toccat伺服器中JSP更新替換。

 

三、自定義類載入器

  3.1 ClassLoader實現自定義類載入器相關方法說明

    要實現自定義類載入器需要先繼承ClassLoader,ClassLoader類是一個抽象類,負責載入classes的對象。自定義ClassLoader中至少需要瞭解其中的三個的方法: loadClass,findClass,defineClass。
   

public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
throws ClassFormatError
{
return defineClass(name, b, off, len, null);
}

    loadClass:JVM在載入類的時候,都是通過ClassLoader的loadClass()方法來載入class的,loadClass使用雙親委派模式。如果要改變雙親委派模式,可以修改loadClass來改變class的載入方式。雙親委派模式這裡就不贅述了。
    findClass:ClassLoader通過findClass()方法來載入類。自定義類載入器實現這個方法來載入需要的類,比如指定路徑下的文件,位元組流等。
    definedClass:definedClass在findClass中使用,通過調用傳進去一個Class文件的位元組數組,就可以方法區生成一個Class對象,也就是findClass實現了類載入的功能了。

    貼上一段ClassLoader中loadClass源碼,見見真面目...
      

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    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;
    }
}

    源碼說明...

/**
* Loads the class with the specified <a href="#name">binary name</a>. The
* default implementation of this method searches for classes in the
* following order:
*
* <ol>
*
* <li><p> Invoke {@link #findLoadedClass(String)} to check if the class
* has already been loaded. </p></li>
*
* <li><p> Invoke the {@link #loadClass(String) <tt>loadClass</tt>} method
* on the parent class loader. If the parent is <tt>null</tt> the class
* loader built-in to the virtual machine is used, instead. </p></li>
*
* <li><p> Invoke the {@link #findClass(String)} method to find the
* class. </p></li>
*
* </ol>
*
* <p> If the class was found using the above steps, and the
* <tt>resolve</tt> flag is true, this method will then invoke the {@link
* #resolveClass(Class)} method on the resulting <tt>Class</tt> object.
*
* <p> Subclasses of <tt>ClassLoader</tt> are encouraged to override {@link
* #findClass(String)}, rather than this method. </p>
*
* <p> Unless overridden, this method synchronizes on the result of
* {@link #getClassLoadingLock <tt>getClassLoadingLock</tt>} method
* during the entire class loading process.
*
* @param name
* The <a href="#name">binary name</a> of the class
*
* @param resolve
* If <tt>true</tt> then resolve the class
*
* @return The resulting <tt>Class</tt> object
*
* @throws ClassNotFoundException
* If the class could not be found
*/

   翻譯過來大概是:使用指定的二進位名稱來載入類,這個方法的預設實現按照以下順序查找類: 調用findLoadedClass(String)方法檢查這個類是否被載入過 使用父載入器調用loadClass(String)方法,如果父載入器為Null,類載入器裝載虛擬機內置的載入器調用findClass(String)方法裝載類, 如果,按照以上的步驟成功的找到對應的類,並且該方法接收的resolve參數的值為true,那麼就調用resolveClass(Class)方法來處理類。 ClassLoader的子類最好覆蓋findClass(String)而不是這個方法。 除非被重寫,這個方法預設在整個裝載過程中都是同步的(線程安全的)。

   resolveClass:Class載入必須鏈接(link),鏈接指的是把單一的Class加入到有繼承關係的類樹中。這個方法給Classloader用來鏈接一個類,如果這個類已經被鏈接過了,那麼這個方法只做一個簡單的返回。否則,這個類將被按照 Java™規範中的Execution描述進行鏈接。

 3.2 自定義類載入器實現

    按照3.1的說明,繼承ClassLoader後重寫了findClass方法載入指定路徑上的class。先貼上自定義類載入器。

package com.chenerzhu.learning.classloader;

import java.nio.file.Files;
import java.nio.file.Paths;

/**
 * @author chenerzhu
 * @create 2018-10-04 10:47
 **/
public class MyClassLoader extends ClassLoader {
    private String path;

    public MyClassLoader(String path) {
        this.path = path;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            byte[] result = getClass(name);
            if (result == null) {
                throw new ClassNotFoundException();
            } else {
                return defineClass(name, result, 0, result.length);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    private byte[] getClass(String name) {
        try {
            return Files.readAllBytes(Paths.get(path));
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

    

    以上就是自定義的類載入器了,實現的功能是載入指定路徑的class。再看看如何使用。 

package com.chenerzhu.learning.classloader;

import org.junit.Test;

/**
 * Created by chenerzhu on 2018/10/4.
 */
public class MyClassLoaderTest {
    @Test
    public void testClassLoader() throws Exception {
        MyClassLoader myClassLoader = new MyClassLoader("src/test/resources/bean/Hello.class");
        Class clazz = myClassLoader.loadClass("com.chenerzhu.learning.classloader.bean.Hello");
        Object obj = clazz.newInstance();
        System.out.println(obj);
        System.out.println(obj.getClass().getClassLoader());
    }
}

    首先通過構造方法創建MyClassLoader對象myClassLoader,指定載入src/test/resources/bean/Hello.class路徑的Hello.class(當然這裡只是個例子,直接指定一個class的路徑了)。然後通過myClassLoader方法loadClass載入Hello的Class對象,最後實例化對象。以下是輸出結果,看得出來實例化成功了,並且類載入器使用的是MyClassLoader。

com.chenerzhu.learning.classloader.bean.Hello@2b2948e2
com.chenerzhu.learning.classloader.MyClassLoader@335eadca

 

四、類Class卸載

  JVM中class和Meta信息存放在PermGen space區域(JDK1.8之後存放在MateSpace中)。如果載入的class文件很多,那麼可能導致元數據空間溢出。引起java.lang.OutOfMemory異常。對於有些Class我們可能只需要使用一次,就不再需要了,也可能我們修改了class文件,我們需要重新載入 newclass,那麼oldclass就不再需要了。所以需要在JVM中卸載(unload)類Class。
  JVM中的Class只有滿足以下三個條件,才能被GC回收,也就是該Class被卸載(unload):

  1. 該類所有的實例都已經被GC。
  2. 該類的java.lang.Class對象沒有在任何地方被引用。
  3. 載入該類的ClassLoader實例已經被GC。

  很容易理解,就是要被卸載的類的ClassLoader實例已經被GC並且本身不存在任何相關的引用就可以被卸載了,也就是JVM清除了類在方法區內的二進位數據。
  JVM自帶的類載入器所載入的類,在虛擬機的生命周期中,會始終引用這些類載入器,而這些類載入器則會始終引用它們所載入的類的Class對象。因此這些Class對象始終是可觸及的,不會被卸載。而用戶自定義的類載入器載入的類是可以被卸載的。雖然滿足以上三個條件Class可以被卸載,但是GC的時機我們是不可控的,那麼同樣的我們對於Class的卸載也是不可控的。

 

五、JVM自定義類載入器載入指定classPath下的所有class及jar

  經過以上幾個點的說明,現在可以實現JVM自定義類載入器載入指定classPath下的所有class及jar了。這裡沒有限制class和jar的位置,只要是classPath路徑下的都會被載入進JVM,而一些web應用伺服器載入是有限定的,比如tomcat載入的是每個應用classPath+“/classes”載入class,classPath+“/lib”載入jar。以下就是代碼啦...

  

package com.chenerzhu.learning.classloader;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Enumeration;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

/**
 * @author chenerzhu
 * @create 2018-10-04 12:24
 **/
public class ClassPathClassLoader extends ClassLoader{

    private static Map<String, byte[]> classMap = new ConcurrentHashMap<>();
    private String classPath;

    public ClassPathClassLoader() {
    }

    public ClassPathClassLoader(String classPath) {
        if (classPath.endsWith(File.separator)) {
            this.classPath = classPath;
        } else {
            this.classPath = classPath + File.separator;
        }
        preReadClassFile();
        preReadJarFile();
    }

    public static boolean addClass(String className, byte[] byteCode) {
        if (!classMap.containsKey(className)) {
            classMap.put(className, byteCode);
            return true;
        }
        return false;
    }

    /**
     * 這裡僅僅卸載了myclassLoader的classMap中的class,虛擬機中的
     * Class的卸載是不可控的
     * 自定義類的卸載需要MyClassLoader不存在引用等條件
     * @param className
     * @return
     */
    public static boolean unloadClass(String className) {
        if (classMap.containsKey(className)) {
            classMap.remove(className);
            return true;
        }
        return false;
    }

    /**
     * 遵守雙親委托規則
     */
    @Override
    protected Class<?> findClass(String name) {
        try {
            byte[] result = getClass(name);
            if (result == null) {
                throw new ClassNotFoundException();
            } else {
                return defineClass(name, result, 0, result.length);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    private byte[] getClass(String className) {
        if (classMap.containsKey(className)) {
            return classMap.get(className);
        } else {
            return null;
        }
    }

    private void preReadClassFile() {
        File[] files = new File(classPath).listFiles();
        if (files != null) {
            for (File file : files) {
                scanClassFile(file);
            }
        }
    }

    private void scanClassFile(File file) {
        if (file.exists()) {
            if (file.isFile() && file.getName().endsWith(".class")) {
                try {
                    byte[] byteCode = Files.readAllBytes(Paths.get(file.getAbsolutePath()));
                    String className = file.getAbsolutePath().replace(classPath, "")
                            .replace(File.separator, ".")
                            .replace(".class", "");
                    addClass(className, byteCode);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            } else if (file.isDirectory()) {
                for (File f : file.listFiles()) {
                    scanClassFile(f);
                }
            }
        }
    }

    private void preReadJarFile() {
        File[] files = new File(classPath).listFiles();
        if (files != null) {
            for (File file : files) {
                scanJarFile(file);
            }
        }
    }

    private void readJAR(JarFile jar) throws IOException {
        Enumeration<JarEntry> en = jar.entries();
        while (en.hasMoreElements()) {
            JarEntry je = en.nextElement();
            je.getName();
            String name = je.getName();
            if (name.endsWith(".class")) {
                //String className = name.replace(File.separator, ".").replace(".class", "");
                String className = name.replace("\\", ".")
                        .replace("/", ".")
                        .replace(".class", "");
                InputStream input = null;
                ByteArrayOutputStream baos = null;
                try {
                    input = jar.getInputStream(je);
                    baos = new ByteArrayOutputStream();
                    int bufferSize = 1024;
                    byte[] buffer = new byte[bufferSize];
                    int bytesNumRead = 0;
                    while ((bytesNumRead = input.read(buffer)) != -1) {
                        baos.write(buffer, 0, bytesNumRead);
                    }
                    addClass(className, baos.toByteArray());
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    if (baos != null) {
                        baos.close();
                    }
                    if (input != null) {
                        input.close();
                    }
                }
            }
        }
    }

    private void scanJarFile(File file) {
        if (file.exists()) {
            if (file.isFile() && file.getName().endsWith(".jar")) {
                try {
                    readJAR(new JarFile(file));
                } catch (IOException e) {
                    e.printStackTrace();
                }
            } else if (file.isDirectory()) {
                for (File f : file.listFiles()) {
                    scanJarFile(f);
                }
            }
        }
    }


    public void addJar(String jarPath) throws IOException {
        File file = new File(jarPath);
        if (file.exists()) {
            JarFile jar = new JarFile(file);
            readJAR(jar);
        }
    }
}

  

  如何使用的代碼就不貼了,和3.2節自定義類載入器的使用方式一樣。只是構造方法的參數變成classPath了,篇末有代碼。當創建MyClassLoader對象時,會自動添加指定classPath下麵的所有class和jar裡面的class到classMap中,classMap維護className和classCode位元組碼的關係,只是個緩衝作用,避免每次都從文件中讀取。自定義類載入器每次loadClass都會首先在JVM中找是否已經載入className的類,如果不存在就會到classMap中取,如果取不到就是載入錯誤了。

 

六、最後

  至此,JVM自定義類載入器載入指定classPath下的所有class及jar已經完成了。這篇博文花了兩天才寫完,在寫的過程中有意識地去瞭解了許多代碼的細節,收穫也很多。本來最近僅僅是想實現Quartz控制台頁面任務添加支持動態class,結果不知不覺跑到類載入器的坑了,在此也趁這個機會總結一遍。當然以上內容並不能保證正確,所以希望大家看到錯誤能夠指出,幫助我更正已有的認知,共同進步。。。

 

本文的代碼已經上傳github:https://github.com/chenerzhu/learning/tree/master/classloader  歡迎下載和指正。

 

參考文章:

  深度分析Java的ClassLoader機制(源碼級別)

  自定義類載入器-從.class和.jar中讀取

  Class熱替換與卸載

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • jar包名修改工具 軟體傳送門:鏈接: https://pan.baidu.com/s/12StRdEkYGmMn1DuNSquXSw 提取碼: i9w1 閑暇之餘,利用jarjar.jar寫了一個小工具,沒啥技術含量,就是為了方便平時使用,在這裡分享一下 : 工具較為簡單,基本上是傻瓜式操作,選擇 ...
  • 在面向對象程式設計中,對象可以看做是數據(特性)以及由一系列可以存取、操作這些數據的方法所組成的集合。編寫代碼時,我們可以將所有功能都寫在一個文件里,這樣也是可行的,但是這樣不利於代碼的維護,你總不希望維護代碼前,還需要從頭至尾的通讀一遍吧,就好像一間雜亂無章的房子,你想找一件想要的東西,但是需要地 ...
  • python-GUI編程-PyQt5 編寫出你開心就好的界面!! ...
  • [TOC] 前言 暑假搞數學建模接觸到了Python,不得已成為了一個Py吹,Python作為動態的高級語言,在方便的同時也伴隨了想當強的靈活性,學Python首先是為了寫爬蟲,在寫爬蟲之前先來點小前奏,用Python的Selenium包實現模擬點擊,完成啟明星工作室論壇的自動簽到。(因為本人老是沉 ...
  • 視圖函數返回HTML模板:使用“from flask import render_template”,在函數中傳入相對於文件夾“templates”HTML模板路徑名稱字元串即可,flask會自動到項目根目錄的“templates”文件夾(創建flask項目時,PyCharm會自動創建兩個空文件夾, ...
  • 將屬於一類的對象放在一起: 如果一個函數操縱一個全局變數,那麼兩者最好都在類內作為特性和方法實現。 不要讓對象過於親密: 方法應該只關心自己實例的特性,讓其他實例管理自己的狀態。 簡單就好: 讓方法小巧起來,一般來說,多數方法都應在30秒內被讀完,儘量在代碼的行數控制在一頁或者一屏之內。 小心繼承, ...
  • JPA概述 JPA(Java Persistence API)的簡稱,用於持久化的API。 JAVAEE5.0平臺標準的ORM的規範使得應用程式以統一的方式訪問持久層。 JPA和Hibernate的關係 JPA是Hibernate的一個抽象,就像JDBC和JDBC驅動的關係一樣。 PA是規範:JPA ...
  • 1. 點擊菜單欄的File >New Project 2. 打開Terminal, 進入剛剛創建的路徑執行如下命令: python manage.py startapp app01 顯示效果如下: 3. 配置靜態文件路徑 4. 在view.py文件新增方法: 5. 在urls.py文件中進行路由匹配 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...