JAVA學習之ClassLoader 前言 最近被 一句話所觸動—— 種一棵樹最好的時間是十年前,其次是現在。 所以決定要開始記錄自己的學習之路。 什麼是類載入? 我們都知道,每個.java文件可以經過javac指令編譯成.class文件,裡面包含著java虛擬機的機器指令。當我們需要使用一個jav ...
JAVA學習之ClassLoader
前言
最近被 一句話所觸動——種一棵樹最好的時間是十年前,其次是現在。所以決定要開始記錄自己的學習之路。
什麼是類載入?
我們都知道,每個.java文件可以經過javac指令編譯成.class文件,裡面包含著java虛擬機的機器指令。當我們需要使用一個java類時,虛擬機會載入它的.class文件,創建對應的java對象。將.class調入虛擬機的過程,稱之為載入。
loading :載入。通過類的完全限定名找到.class位元組碼文件,同時創建一個對象。
verification:驗證。確保class位元組碼文件符合當前虛擬機的要求。
preparation:準備。這時候將static修飾的變數進行記憶體分配,同時設置初始值。
resolution:解析。虛擬機將常量池中的符號引用變為直接引用。
- 符號引用:在編譯之後完成的,一個常量並沒有進行記憶體分配,也就只能用符號引用。
- 直接引用:常量會在preparation階段將常量進行記憶體分配,於是就可以建立直接的虛擬機記憶體聯繫,就可以直接引用。
initialization:初始化。類載入的最後階段。如果這個類有超類,進行超類的初始化,執行類的靜態代碼塊,同時給類的靜態變數賦予初值。前面的preparation階段是分配記憶體,都只是預設的值,並沒有被賦予初值。
類載入在java中有兩種方式
- 顯示載入。通過class.fornNme(classname)或者this.getClass().getClassLoader.loadClass(classname)載入。即通過調用類載入器classLoader來完成類的載入
- 隱式載入。類在需要被使用時,不直接調用ClassLoader,而是虛擬機自動調用ClassLoader載入到記憶體中。
什麼是ClassLoader?
類載入器的任務是將類的二進位位元組流讀入到JVM中,然後變成一個JVM能識別的class對象,同時實例化。於是我們就可以分解ClassLoader的任務。
- 找到類的二進位位元組流,並載入進來
- 規則化為JVM能識別的class對象
我們查看源碼,找到對應解決方案:
在ClassLoader中,定義了兩個介面:
- findClass(String name).
- defineClass(byte[] b , int off , int len)
findClass用於找到二進位文件並讀入,調用defineClass用位元組流變成JVM能識別的class對象,同時實例化class對象。
我們來看個例子:
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 獲取類的位元組數組,通過name找到類,如果你對位元組碼加密,需要自己解密出來
byte[] classData = getClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
//使用defineClass生成class對象
return defineClass(name, classData, 0, classData.length);
}
}
代理模式
提到類載入器,一定得涉及的是委派模式。在JAVA中,ClassLoader存在一下幾類,他們的關係如下:
-
Bootstrap ClassLoader:引導類載入器。採用原生c++實現,用於載入java的核心類(
%JAVA_HOME%/lib
路徑下的核心類庫或者-Xbootclasspath
指定下的jar包)到記憶體中。沒有父類。 -
Extension ClassLoader:擴展類載入器。java實現,載入
/lib/ext
目錄下或者由系統變數-Djava.ext.dir指定位路徑中的類庫。父類載入器為null。 -
System ClassLoader:它會根據java應用的類路徑(CLASSPATH)來載入類,即
java -classpath
或-D java.class.path
指定路徑下的類庫,也就是我們經常用到的classpath路徑。一般來說,java應用的類都是由它完成。可以由ClassLoader.getSystemLoader()方法獲得。父類載入器是Extension ClassLoader。 -
Customize ClassLoader:用戶自定義載入器。用於完成用戶自身的特有需求。父類載入器為System ClassLoader。
- 載入不在CLASSPATH路徑的class文件
- 加密後的class文件需要用戶自定義ClassLoader來解密後才能被JVM識別。
- 熱部署,同一個class文件通過不同的類載入器產生不同的class對象。兩個類完全相同不僅僅要是同一個class文件,還需要同一個類載入器、同一個JVM。
而代理模式 ,就是像上面圖片所展示那樣,當要載入一個類時,載入器會尋求其父類的幫助,讓父類嘗試去載入這個類。只有當父類失敗後,才會由自己載入。ClassLoader的loadClass()方法中體現。
示例代碼:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { 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 // 如果都沒有找到,則通過自定義實現的findClass去查找並載入 c = findClass(name); } } if (resolve) {//是否需要在載入時進行解析 resolveClass(c); } return c; } }
findClass(String name)的類似實現上面有示例,resolveClass()就是完成解析功能。
URLClassLoader
URLClassLoader是常用的ClassLoader類,其實現了findclass()介面,所以如果自定義時繼承URLClassLoader可以不用重寫findclass()。ExtClassLoader在代理模式中屬於Extension ClassLoader,而AppClassLoader屬於System ClassLoader。
線程上下文載入器
前面我們提到過BootStrap ClassLoader載入的是%JAVA_HOME%/lib下的核心庫文件,而CLASSPATH路徑下的庫由System ClassLoader載入。但在java語言中,存在這種現象,Java 提供了很多服務提供者介面(Service Provider Interface,SPI),允許第三方為這些介面提供實現,如JDBC、JCE、JNDI等。而SPI是Java的核心庫文件,由 BootStrap ClassLoader載入,第三方實現是放在CLASSPATH路徑下,由System ClassLoader載入。當BootStrap ClassLoader啟用時,需要載入其實現,但自己找不到,又因為代理模式的存在,無法委托System ClassLoader來載入,所以無法實現。
Contex ClassLoader(線程上下文載入器)剛好可以解決這個問題。
Contex ClassLoader(線程上下文載入器)是從 JDK 1.2 開始引入的。類
java.lang.Thread
中的方法getContextClassLoader()
和setContextClassLoader(ClassLoader cl)
用來獲取和設置線程的上下文類載入器。如果沒有通過setContextClassLoader(ClassLoader cl)
方法進行設置的話,線程將繼承其父線程的上下文類載入器。Java 應用運行的初始線程的上下文類載入器是系統類載入器。線上程中運行的代碼可以通過此類載入器來載入類和資源。例子JDBC:
public class DriverManager { //省略...... static { loadInitialDrivers(); println("JDBC DriverManager initialized"); } private static void loadInitialDrivers() { sun.misc.Providers() AccessController.doPrivileged(new PrivilegedAction<Void>() { public Void run() { ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class); //省略不必要的代碼...... } }); } public class Driver extends com.mysql.cj.jdbc.Driver { public Driver() throws SQLException { super(); } static { //省略 } } public static <S> ServiceLoader<S> load(Class<S> service) { //通過線程上下文類載入器載入 ClassLoader cl = Thread.currentThread().getContextClassLoader(); return ServiceLoader.load(service, cl); } //調用 String url = "jdbc:mysql://localhost:3342/cm-storylocker?characterEncoding=UTF-8"; // 通過java庫獲取資料庫連接 Connection conn = java.sql.DriverManager.getConnection(url, "root", "password");
我們可以看到,當我們在使用JDBC的時候,會使用有DriverManager類,它的static代碼區引用的ServiceLoader類會完成JDBC實現類的載入。
如何設計自己的ClassLoader
給出例子,重寫文件系統載入器
public class FileSystemClassLoader extends ClassLoader { private String rootDir; public FileSystemClassLoader(String rootDir) { this.rootDir = rootDir; } protected Class<?> findClass(String name) throws ClassNotFoundException { //根據規則獲取位元組流數組 byte[] classData = getClassData(name); if (classData == null) { throw new ClassNotFoundException(); } else { return defineClass(name, classData, 0, classData.length); } } //設定自己的讀取規則 private byte[] getClassData(String className) { String path = classNameToPath(className); try { InputStream ins = new FileInputStream(path); ByteArrayOutputStream baos = new ByteArrayOutputStream(); int bufferSize = 4096; byte[] buffer = new byte[bufferSize]; int bytesNumRead = 0; while ((bytesNumRead = ins.read(buffer)) != -1) { baos.write(buffer, 0, bytesNumRead); } return baos.toByteArray(); } catch (IOException e) { e.printStackTrace(); } return null; } private String classNameToPath(String className) { return rootDir + File.separatorChar + className.replace('.', File.separatorChar) + ".class"; } }
本文學習參考自
[1] https://www.ibm.com/developerworks/cn/java/j-lo-classloader/index.html#minor1.1
[2] https://blog.csdn.net/javazejian/article/details/73413292
[3] https://juejin.im/post/5e1aaf626fb9a0301d11ac8e#heading-8