JVM類加載器之ClassLoader的使用詳解
類加載器
概述
類加載器負責讀取Java字節(jié)代碼,并轉換成java.lang.Class類的一個實例的代碼模塊。
類加載器除了用于加載類外,還可用于確定類在Java虛擬機中的唯一性。
任意一個類,都由加載它的類加載器和這個類本身一同確定其在 Java 虛擬機中的唯一性,每一個類加載器,都有一個獨立的類名稱空間,而不同類加載器中是允許同名(指全限定名相同)類存在的。
比較兩個類是否“相等”,前提是這兩個類由同一個類加載器加載,否則,即使這兩個類來源于同一個Class 文件,被同一個虛擬機加載,只要加載它們的類加載器不同,那么這兩個類就必定不相等。
這里“相等”是指:類的Class對象的equals()方法、isInstance()方法的返回結果,使用instanceof關鍵字做對象所屬關系判定等情況。
加載器的種類
1.啟動類加載器:Bootstrap ClassLoader
最頂層的加載類,由 C++實現,負責加載%JAVA_HOME%/lib
目錄下的jar包和類或者被 -Xbootclasspath
參數指定的路徑中的所有類。
2.拓展類加載器:Extension ClassLoader
負責加載java平臺中擴展功能的一些jar包,如加載%JRE_HOME%/lib/ext
目錄下的jar包和類,或-Djava.ext.dirs
所指定的路徑下的jar包。
3.系統類加載器/應用程序加載器:App ClassLoader
負責加載當前應用classpath中指定的jar包及-Djava.class.path
所指定目錄下的類和jar包。開發(fā)者可以直接使用這個類加載器,如果應用程序中沒有自定義過自己的類加載器,一般情況下這個就是程序中默認的類加載器。
4.自定義類加載器:Custom ClassLoader
通過java.lang.ClassLoader
的子類自定義加載class,屬于應用程序根據自身需要自定義的ClassLoader,如tomcat、jboss都會根據j2ee規(guī)范自行實現ClassLoader
驗證不同加載器
每個類加載都有一個父類加載器,可以通過程序來驗證
public static void main(String[] args) { // App ClassLoader System.out.println(new User().getClass().getClassLoader()); // Ext ClassLoader System.out.println(new User().getClass().getClassLoader().getParent()); // Bootstrap ClassLoader System.out.println(new User().getClass().getClassLoader().getParent().getParent()); // Bootstrap ClassLoader System.out.println(new String().getClass().getClassLoader()); }
AppClassLoader的父類加載器為ExtClassLoader, ExtClassLoader的父類加載器為 null,null 并不代表ExtClassLoader沒有父類加載器,而是 BootstrapClassLoader 。
sun.misc.Launcher$AppClassLoader@18b4aac2 sun.misc.Launcher$ExtClassLoader@5fdef03a null null
核心方法
查看類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 { // 父加載器不為空,調用父加載器loadClass()方法處理 if (parent != null) { // 讓上一層加載器進行加載 c = parent.loadClass(name, false); } else { // 父加載器為空,使用啟動類加載器 BootstrapClassLoader 加載 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(); // 調用此類加載器所實現的findClass方法進行加載 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方法是當字節(jié)碼加載到內存后進行鏈接操作,對文件格式和字節(jié)碼驗證,并為 static 字段分配空間并初始化,符號引用轉為直接引用,訪問控制,方法覆蓋等 resolveClass(c); } return c; } }
JVM類加載機制的三種方式
全盤負責
當一個類加載器負責加載某個Class時,該Class所依賴的和引用的其他Class也將由該類加載器負責載入,除非顯示使用另外一個類加載器來載入
注意:
系統類加載器AppClassLoader加載入口類(含有main方法的類)時,會把main方法所依賴的類及引用的類也載入。只是調用了ClassLoader.loadClass(name)
方法,并沒有真正定義類。真正加載class字節(jié)碼文件生成Class對象由雙親委派機制完成。
父類委托、雙親委派
父類委托即雙親委派,雙親委派模型是描述類加載器之間的層次關系。它要求除了頂層的啟動類加載器外,其余的類加載器都應當有自己的父類加載器。父子關系一般不會以繼承的關系實現,而是以組合關系來復用父加載器的代碼。
雙親委派模型是指:子類加載器如果沒有加載過該目標類,就先委托父類加載器加載該目標類,只有在父類加載器找不到字節(jié)碼文件的情況下才從自己的類路徑中查找并裝載目標類。
雙親委派模型的好處
保證Java程序的穩(wěn)定運行,避免類的重復加載:JVM區(qū)分不同類的方式不僅僅根據類名,相同的類文件被不同的類加載器加載產生的是兩個不同的類
保證Java核心API不被篡改:如果沒有使用雙親委派模型,而是每個類加載器加載自己的話就會出現一些問題,如編寫一個稱為java.lang.Object 類,程序運行時,系統就會出現多個不同的Object類。反之使用雙親委派模型:無論使用哪個類加載器加載,最終都會委派給最頂端的啟動類加載器加載,從而使得不同加載器加載的Object類都是同一個。
雙親委派機制加載Class的具體過程:
1. ClassLoader先判斷該Class是否已加載,如果已加載,則返回Class對象,如果沒有則委托給父類加載器
2. 父類加載器判斷是否加載過該Class,如果已加載,則返回Class對象,如果沒有則委托給祖父類加載器
3. 依此類推,直到始祖類加載器(引用類加載器)
4. 始祖類加載器判斷是否加載過該Class,如果已加載,則返回Class對象
如果沒有則嘗試從其對應的類路徑下尋找class字節(jié)碼文件并載入
如果載入成功,則返回Class對象;如果載入失敗,則委托給始祖類加載器的子類加載器
5. 始祖類加載器的子類加載器嘗試從其對應的類路徑下尋找class字節(jié)碼文件并載入
如果載入成功,則返回Class對象;如果載入失敗,則委托給始祖類加載器的孫類加載器
6. 依此類推,直到源ClassLoader
7. 源ClassLoader嘗試從其對應的類路徑下尋找class字節(jié)碼文件并載入
如果載入成功,則返回Class對象;如果載入失敗,源ClassLoader不會再委托其子類加載器,而是拋出異常
注意:
雙親委派機制是Java推薦的機制,并不是強制的機制??梢岳^承java.lang.ClassLoader類,實現自己的類加載器。如果想保持雙親委派模型,應該重寫findClass(name)
方法;如果想破壞雙親委派模型,可以重寫loadClass(name)
方法。
緩存機制
緩存機制將會保證所有加載過的Class都將在內存中緩存,當程序中需要使用某個Class時,類加載器先從內存的緩存區(qū)尋找該Class,只有緩存區(qū)不存在,系統才會讀取該類對應的二進制數據,并將其轉換成Class對象,存入緩存區(qū)。
對于一個類加載器實例來說,相同全名的類只加載一次,即loadClass方法不會被重復調用。因此,這就是為什么修改Class后,必須重啟JVM,程序的修改才會生效的原因。
JDK8使用的是直接內存,所以會用到直接內存進行緩存。因此,類變量為什么只會被初始化一次的原因。
打破雙親委派
在加載類的時候,會一級一級向上委托,判斷是否已經加載,從自定義類加載器 --> 應用類加載器 --> 擴展類加載器 --> 啟動類加載器,如果到最后都沒有加載這個類,則回去加載自己的類。
雙親委派模型并不是強制模型,而且會帶來一些些的問題。例如:java.sql.Driver
類,JDK只能提供一個規(guī)范接口,而不能提供實現。提供實現的是實際的數據庫提供商,提供商的庫不可能放JDK目錄里。
重寫loadclass方法
自定義類加載,重寫loadclass方法,即可破壞雙親委派機制
因為雙親委派的機制都是通過這個方法實現的,這個方法可以指定類通過什么類加載器來進行加載,所有如果改寫加載規(guī)則,相當于打破雙親委派機制
import cn.ybzy.demo.Test; import java.io.*; public class MyClassLoader extends ClassLoader { @Override protected Class<?> findClass(String name) throws ClassNotFoundException { byte[] classData; try { classData = loadClassData(name); } catch (IOException e) { throw new RuntimeException(e); } if (classData == null) { throw new ClassNotFoundException(); } else { return defineClass(name, classData, 0, classData.length); } } private byte[] loadClassData(String className) throws IOException { String replace = className.replace('.', File.separatorChar); String path = ClassLoader.getSystemResource("").getPath() + replace + ".class"; InputStream inputStream = null; ByteArrayOutputStream byteArrayOutputStream = null; try { inputStream = new FileInputStream(path); byteArrayOutputStream = new ByteArrayOutputStream(); int bufferSize = 1024; byte[] buffer = new byte[bufferSize]; int length = 0; while ((length = inputStream.read(buffer)) != -1) { byteArrayOutputStream.write(buffer, 0, length); } return byteArrayOutputStream.toByteArray(); } catch (IOException e) { e.printStackTrace(); } finally { if (byteArrayOutputStream != null) { byteArrayOutputStream.close(); } if (inputStream != null) { inputStream.close(); } } return null; } @Override 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 { // 修改classloader的原雙親委派邏輯,從而打破雙親委派 if (name.startsWith("cn.ybzy.demo")) { c = findClass(name); } else { c = this.getParent().loadClass(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; } } }
public static void main(String[] args) throws ClassNotFoundException { MyClassLoader classLoader = new MyClassLoader(); Class<?> aClass = classLoader.loadClass(Test.class.getName()); System.out.println(aClass.getClassLoader()); }
cn.ybzy.demo.MyClassLoader@2f410acf
自定義類加載器
自定義類加載器的核心在于對字節(jié)碼文件的獲取,如果是加密的字節(jié)碼則需要在類中對文件進行解密。
準備字節(jié)碼文件
創(chuàng)建Test類,同時進行javac Test.class
編譯成字節(jié)碼文件,放到目錄下:D:\Temp\cn\ybzy\demo
package cn.ybzy.demo; public class Test { public static void main(String[] args) { System.out.println("Test..."); } }
創(chuàng)建自定義類加載器
import java.io.*; public class MyClassLoader extends ClassLoader { private String root; protected Class<?> findClass(String name) throws ClassNotFoundException { byte[] classData; try { classData = loadClassData(name); } catch (IOException e) { throw new RuntimeException(e); } if (classData == null) { throw new ClassNotFoundException(); } else { return defineClass(name, classData, 0, classData.length); } } private byte[] loadClassData(String className) throws IOException { String fileName = root + File.separatorChar + className.replace('.', File.separatorChar) + ".class"; InputStream inputStream = null; ByteArrayOutputStream byteArrayOutputStream = null; try { inputStream = new FileInputStream(fileName); byteArrayOutputStream = new ByteArrayOutputStream(); int bufferSize = 1024; byte[] buffer = new byte[bufferSize]; int length = 0; while ((length = inputStream.read(buffer)) != -1) { byteArrayOutputStream.write(buffer, 0, length); } return byteArrayOutputStream.toByteArray(); } catch (IOException e) { e.printStackTrace(); } finally { if (byteArrayOutputStream != null) { byteArrayOutputStream.close(); } if (inputStream != null) { inputStream.close(); } } return null; } public String getRoot() { return root; } public void setRoot(String root) { this.root = root; } }
執(zhí)行測試
啟動main方法,執(zhí)行測試
public static void main(String[] args) { MyClassLoader classLoader = new MyClassLoader(); classLoader.setRoot("D:\\Temp"); Class<?> testClass = null; try { testClass = classLoader.loadClass("cn.ybzy.demo.Test"); Object object = testClass.newInstance(); System.out.println(object.getClass().getClassLoader()); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } }
cn.ybzy.demo.MyClassLoader@5679c6c6
將Test類放到項目類路徑下,由于雙親委托機制的存在,會直接導致該類由 AppClassLoader 加載,而不會通過自定義類加載器來加載
sun.misc.Launcher$AppClassLoader@18b4aac2
注意事項
1、這里傳遞文件名需要是類的全限定性名稱,因為defineClass方法是按這種方式/格式進行處理
因此,若沒有全限定名,需要將類的全路徑加載進去
2、不要重寫loadClass方法,因為這樣容易破壞雙親委托模式
3、Test類本身可以被AppClassLoader類加載,因此不能把Test.class放在類路徑下
否則,由于雙親委托機制的存在,會直接導致該類由AppClassLoader加載,而不會通過自定義類加載器來加載
以上就是JVM類加載器之ClassLoader的使用詳解的詳細內容,更多關于JVM類加載器ClassLoader的資料請關注腳本之家其它相關文章!
相關文章
Spring?Boot?ORM?框架JPA使用與連接池?Hikari詳解
這篇文章主要介紹了SpringBoot?ORM框架JPA與連接池Hikari,主要就是介紹JPA?的使用姿勢,本文結合實例代碼給大家介紹的非常詳細,需要的朋友可以參考下2023-08-08