文章目錄
- 1.類與類加載器
- 2.類加載器加載規則
- 3.JVM 中內置的三個重要類加載器
- 為什么 獲取到 ClassLoader 為null就是 BootstrapClassLoader 加載的呢?
- 4.自定義類加載器
- 什么時候需要自定義類加載器
- 代碼示例
- 5.雙親委派模式
- 類與類加載器
- 雙親委派模型
- 雙親委派模型的執行流程
- 雙親委派模型的好處
- 打破雙親委派模型方法
- 6.線程上下文類加載器
1.類與類加載器
類加載器雖然只用于實現類的加載動作,但它在Java程序中起到的作用卻遠超類加載階段。
對于任意一個類,都必須由加載它的類加載器和這個類本身一起共同確立其在Java虛擬機中的唯一性,一個類加載器,都擁有一個獨立的類名稱空間。
這句話可以表達得更通俗一些:比較兩個類是否“相等”,只有在這兩個類是由同一個類加載器加載的前提下才有意義,否則,即使這兩個類來源于同一個Class文件,被同一個Java虛擬機加載,只要加載它們的類加載器不同,那這兩個類就必定不相等。
這里所指的“相等”,包括代表類的Class對象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回結果,也包括了使用instanceof關鍵字做對象所屬關系判定等各種情況。
官方 API 文檔的介紹:
類加載器是一個負責加載類的對象。ClassLoader 是一個抽象類。給定類的二進制名稱,類加載器應嘗試定位或生成構成類定義的數據。典型的策略是將名稱轉換為文件名,然后從文件系統中讀取該名稱的“類文件”。
每個 Java 類都有一個引用指向加載它的 ClassLoader。不過,數組類不是通過 ClassLoader 創建的,而是 JVM 在需要的時候自動創建的,數組類通過getClassLoader()方法獲取 ClassLoader 的時候和該數組的元素類型的 ClassLoader 是一致的。
從上面的介紹可以看出:
- 類加載器是一個負責加載類的對象,用于實現類加載過程中的加載這一步。
- 每個 Java 類都有一個引用指向加載它的 ClassLoader。
- 數組類不是通過 ClassLoader 創建的(數組類沒有對應的二進制字節流),是由 JVM 直接生成的。
簡單來說,類加載器的主要作用就是動態加載 Java 類的字節碼( .class 文件)到 JVM 中(在內存中生成一個代表該類的 Class 對象)。 字節碼可以是 Java 源程序(.java文件)經過 javac 編譯得來,也可以是通過工具動態生成或者通過網絡下載得來。
其實除了加載類之外,類加載器還可以加載 Java 應用所需的資源如文本、圖像、配置文件、視頻等等文件資源。
2.類加載器加載規則
JVM 啟動的時候,并不會一次性加載所有的類,而是根據需要去動態加載。也就是說,大部分類在具體用到的時候才會去加載,這樣對內存更加友好。
對于已經加載的類會被放在 ClassLoader 中。在類加載的時候,系統會首先判斷當前類是否被加載過。已經被加載的類會直接返回,否則才會嘗試加載。也就是說,對于一個類加載器來說,相同二進制名稱的類只會被加載一次。
public abstract class ClassLoader {...private final ClassLoader parent;// 由這個類加載器加載的類。private final Vector<Class<?>> classes = new Vector<>();// 由VM調用,用此類加載器記錄每個已加載類。void addClass(Class<?> c) {classes.addElement(c);}...
}
3.JVM 中內置的三個重要類加載器
JVM 中內置了三個重要的 ClassLoader:
- BootstrapClassLoader(啟動類加載器):最頂層的加載類,由 C++實現,通常表示為 null,并且沒有父級,主要用來加載 JDK 內部的核心類庫( %JAVA_HOME%/lib目錄下的 rt.jar、resources.jar、charsets.jar等 jar 包和類)以及被 -Xbootclasspath參數指定的路徑下的所有類。
- ExtensionClassLoader(擴展類加載器):主要負責加載 %JRE_HOME%/lib/ext 目錄下的 jar 包和類以及被 java.ext.dirs 系統變量所指定的路徑下的所有類。
- AppClassLoader(應用程序類加載器):面向我們用戶的加載器,負責加載當前應用 classpath 下的所有 jar 包和類。
🌈 拓展一下:
rt.jar:rt 代表“RunTime”,rt.jar是 Java 基礎類庫,包含 Java doc 里面看到的所有的類的類文件。也就是說,我們常用內置庫 java.xxx.*都在里面,比如java.util.*、java.io.*、java.nio.*、java.lang.*、java.sql.*、java.math.*。
Java 9 引入了模塊系統,并且略微更改了上述的類加載器。擴展類加載器被改名為平臺類加載器(platform class loader)。Java SE 中除了少數幾個關鍵模塊,比如說 java.base 是由啟動類加載器加載之外,其他的模塊均由平臺類加載器所加載。
除了這三種類加載器之外,用戶還可以加入自定義的類加載器來進行拓展,以滿足自己的特殊需求。就比如說,我們可以對 Java 類的字節碼( .class 文件)進行加密,加載時再利用自定義的類加載器對其解密。
除了 BootstrapClassLoader 是 JVM 自身的一部分之外,其他所有的類加載器都是在 JVM 外部實現的,并且全都繼承自 ClassLoader抽象類。這樣做的好處是用戶可以自定義類加載器,以便讓應用程序自己決定如何去獲取所需的類。
每個 ClassLoader 可以通過getParent()獲取其父 ClassLoader,如果獲取到 ClassLoader 為null的話,那么該類是通過 BootstrapClassLoader 加載的。
public abstract class ClassLoader {...// 父加載器private final ClassLoader parent;@CallerSensitivepublic final ClassLoader getParent() {//...}...
}
為什么 獲取到 ClassLoader 為null就是 BootstrapClassLoader 加載的呢?
這是因為BootstrapClassLoader 由 C++ 實現,由于這個 C++ 實現的類加載器在 Java 中是沒有與之對應的類的,所以拿到的結果是 null。
4.自定義類加載器
除了 BootstrapClassLoader 其他類加載器均由 Java 實現且全部繼承自java.lang.ClassLoader。如果我們要自定義自己的類加載器,很明顯需要繼承 ClassLoader抽象類。
ClassLoader 類有兩個關鍵的方法:
- protected Class loadClass(String name, boolean resolve):加載指定二進制名稱的類,實現了雙親委派機制 。name 為類的二進制名稱,resolve 如果為 true,在加載時調用 resolveClass(Class<?> c) 方法解析該類。
- protected Class findClass(String name):根據類的二進制名稱來查找類,默認實現是空方法。
注意:如果我們不想打破雙親委派模型,就重寫 ClassLoader 類中的 findClass() 方法即可,無法被父類加載器加載的類最終會通過這個方法被加載。但是,如果想打破雙親委派模型則需要重寫 loadClass() 方法。
什么時候需要自定義類加載器
- 想加載非 classpath 隨意路徑中的類文件。
- 都是通過接口來使用實現、希望解耦時,常用在框架設計。
- 這些類希望予以隔離,不同應用的同名類都可以加載,不會發生沖突,常見于 tomcat 容器。
步驟:
- 繼承 ClassLoader 父類
- 要遵從雙親委派機制,重寫 findClass 方法
注意不是重寫 loadClass 方法,否則不會走雙親委派機制 - 讀取類文件的字節碼
- 調用父類的 defineClass 方法來加載類
- 使用者調用該類加載器的 loadClass 方法
代碼示例
public class F {//通過是否運行靜態代碼塊觀察是否被加載并初始化static {System.out.println("bootstrap F init");}
}
自定義類加載器
class MyClassLoader extends ClassLoader {@Override // name 就是類名稱protected Class<?> findClass(String name) throws ClassNotFoundException {//F.class位置String path = "D:\\java\\jvm\\out\\production\\jvm\\" + name + ".class";try {ByteArrayOutputStream os = new ByteArrayOutputStream();Files.copy(Paths.get(path), os);// 得到字節數組byte[] bytes = os.toByteArray();// byte[] -> *.classreturn defineClass(name, bytes, 0, bytes.length);} catch (IOException e) {e.printStackTrace();throw new ClassNotFoundException("類文件未找到", e);}}@Overrideprotected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {// 打印加載類的類加載器System.out.println("Loading class " + name + " with " + this);return super.loadClass(name, resolve);}
}
使用自定義類加載器加載F類
public class Load {public static void main(String[] args) throws Exception {MyClassLoader classLoader = new MyClassLoader();Class<?> c1 = classLoader.loadClass("F");Class<?> c2 = classLoader.loadClass("F");System.out.println(c1 == c2);MyClassLoader classLoader2 = new MyClassLoader();Class<?> c3 = classLoader2.loadClass("F");System.out.println(c1 == c3);System.out.println("c1: " + c1.getClassLoader());System.out.println("c2: " + c2.getClassLoader());System.out.println("c3: " + c3.getClassLoader());c1.newInstance();}
}
輸出
Loading class F with cn.itcast.jvm.t3.load.MyClassLoader@12bb4df8
Loading class F with cn.itcast.jvm.t3.load.MyClassLoader@12bb4df8
true
Loading class F with cn.itcast.jvm.t3.load.MyClassLoader@4cc77c2e
true
c1: sun.misc.Launcher$AppClassLoader@18b4aac2
c2: sun.misc.Launcher$AppClassLoader@18b4aac2
c3: sun.misc.Launcher$AppClassLoader@18b4aac2
bootstrap F init
5.雙親委派模式
類與類加載器
站在Java虛擬機的角度來看,只存在兩種不同的類加載器:
- 一種是啟動類加載器(BootstrapClassLoader),這個類加載器使用C++語言實現[1],是虛擬機自身的一部分;
- 另外一種就是其他所有的類加載器,這些類加載器都由Java語言實現,獨立存在于虛擬機外部,并且全都繼承自抽象類java.lang.ClassLoader。
站在Java開發人員的角度來看,類加載器就應當劃分得更細致一些:
JDK 9之前的Java應用都是由啟動、擴展、應用程序類加載器互相配合來完成加載的,如果用戶認為有必要,還可以加入自定義的類加載器來進行拓展,典型的如增加除了磁盤位置之外的Class文件來源,或者通過類加載器實現類的隔離、重載等功能。
類加載器有很多種,當我們想要加載一個類的時候,具體是哪個類加載器加載呢?這就需要提到雙親委派模型了。
- ClassLoader 類使用委托模型來搜索類和資源。
- 雙親委派模型要求除了頂層的啟動類加載器外,其余的類加載器都應有自己的父類加載器。
- ClassLoader 實例會在試圖親自查找類或資源之前,將搜索類或資源的任務委托給其父類加載器。
雙親委派模型
下圖展示的各種類加載器之間的層次關系被稱為類加載器的“雙親委派模型(Parents Delegation Model)”。
雙親委派模型要求除了頂層的啟動類加載器外,其余的類加載器都應有自己的父類加載器。不過這里類加載器之間的父子關系一般不是以繼承(Inheritance)的關系來實現的,而是通常使用組合(Composition)關系來復用父加載器的代碼。
注意??:類加載器的雙親委派模型在JDK 1.2時期被引入,并被廣泛應用于此后幾乎所有的Java程序中,但它并不是一個具有強制性約束力的模型,而是Java設計者們推薦給開發者的一種類加載器實現的最佳實踐。
在面向對象編程中,有一條非常經典的設計原則:組合優于繼承,多用組合少用繼承。
雙親委派模型的執行流程
java.lang.ClassLoader#loadClass(java.lang.String, boolean)
protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException
{synchronized (getClassLoadingLock(name)) {// First, check if the class has already been loaded// 1. 檢查該類是否已經加載Class<?> c = findLoadedClass(name);if (c == null) {//如果 c 為 null,則說明該類沒有被加載過long t0 = System.nanoTime();try {if (parent != null) {// 2. 有上級的話,委派上級 loadClass來加載該類c = parent.loadClass(name, false);} else {// 3. 如果沒有上級了(ExtClassLoader),則委派BootstrapClassLoaderc = 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();// 4. 每一層找不到,調用 findClass 方法(每個類加載器自己擴展)來加載c = findClass(name);// this is the defining class loader; record the stats// 5. 記錄耗時sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}}if (resolve) {resolveClass(c);}return c;}
}
結合上面的源碼,簡單總結一下雙親委派模型的執行流程:
- 如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,會首先判斷當前類是否被加載過。已經被加載的類會直接返回,否則才會嘗試加載(每個父類加載器都會走一遍這個流程)。
- 類加載器在進行類加載的時候,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成(調用父加載器 loadClass()方法來加載類)。這樣的話,所有的請求最終都會傳送到頂層的啟動類加載器 BootstrapClassLoader 中。
- 只有當父加載器反饋自己無法完成這個加載請求(它的搜索范圍中沒有找到所需的類)時,子加載器才會嘗試自己去加載(調用自己的 findClass() 方法來加載類)。
- 如果子類加載器也無法加載這個類,那么它會拋出一個 ClassNotFoundException 異常。
雙親委派模型的好處
雙親委派模型是 Java 類加載機制的重要組成部分,它通過委派父加載器優先加載類的方式,實現了兩個關鍵的安全目標:避免類的重復加載和防止核心 API 被篡改。
JVM 區分不同類的依據是類名加上加載該類的類加載器,即使類名相同,如果由不同的類加載器加載,也會被視為不同的類。 雙親委派模型確保核心類總是由 BootstrapClassLoader 加載,保證了核心類的唯一性。
例如,當應用程序嘗試加載 java.lang.Object 時,AppClassLoader 會首先將請求委派給 ExtClassLoader,ExtClassLoader 再委派給 BootstrapClassLoader。BootstrapClassLoader 會在 JRE 核心類庫中找到并加載 java.lang.Object,從而保證應用程序使用的是 JRE 提供的標準版本。
同時即使攻擊者繞過了雙親委派模型,Java 仍然具備更底層的安全機制來保護核心類庫。ClassLoader 的 preDefineClass 方法會在定義類之前進行類名校驗。任何以 “java.” 開頭的類名都會觸發 SecurityException,阻止惡意代碼定義或加載偽造的核心類。
打破雙親委派模型方法
重寫 loadClass() 方法打破雙親委派模型,
原因:類加載器在進行類加載的時候,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成(調用父加載器 loadClass()方法來加載類)。
重寫 loadClass()方法之后,我們就可以改變傳統雙親委派模型的執行流程。例如,子類加載器可以在委派給父類加載器之前,先自己嘗試加載這個類,或者在父類加載器返回之后,再嘗試從其他地方加載這個類。具體的規則由我們自己實現,根據項目需求定制化。
我們比較熟悉的 Tomcat 服務器為了能夠優先加載 Web 應用目錄下的類,然后再加載其他目錄下的類,就自定義了類加載器 WebAppClassLoader 來打破雙親委托機制。這也是 Tomcat 下 Web 應用之間的類實現隔離的具體原理。
Tomcat 的類加載器的層次結構如下:
從圖中的委派關系中可以看出:
- CommonClassLoader作為 CatalinaClassLoader 和 SharedClassLoader 的父加載器。CommonClassLoader 能加載的類都可以被 CatalinaClassLoader 和 SharedClassLoader 使用。因此,CommonClassLoader 是為了實現公共類庫(可以被所有 Web 應用和 Tomcat 內部組件使用的類庫)的共享和隔離。
- CatalinaClassLoader 和 SharedClassLoader 能加載的類則與對方相互隔離。CatalinaClassLoader 用于加載 Tomcat 自身的類,為了隔離 Tomcat 本身的類和 Web 應用的類。SharedClassLoader 作為 WebAppClassLoader 的父加載器,專門來加載 Web 應用之間共享的類比如 Spring、Mybatis。
- 每個 Web 應用都會創建一個單獨的 WebAppClassLoader,并在啟動 Web 應用的線程里設置線程上下文類加載器為 WebAppClassLoader。各個 WebAppClassLoader 實例之間相互隔離,進而實現 Web 應用之間的類隔。
單純依靠自定義類加載器沒辦法滿足某些場景的要求,例如,有些情況下,高層的類加載器需要加載低層的加載器才能加載的類。
比如,假設我們的項目中有 Spring 的 jar 包,由于其是 Web 應用之間共享的,因此會由 SharedClassLoader 加載(Web 服務器是 Tomcat)。
我們項目中有一些用到了 Spring 的業務類,比如實現了 Spring 提供的接口、用到了 Spring 提供的注解。所以,加載 Spring 的類加載器(也就是 SharedClassLoader)也會用來加載這些業務類。
但是業務類在 Web 應用目錄下,不在 SharedClassLoader 的加載路徑下,所以 SharedClassLoader 無法找到業務類,也就無法加載它們。
如何解決這個問題呢? 這個時候就需要用到 線程上下文類加載器(ThreadContextClassLoader) 了。
6.線程上下文類加載器
- 拿 Spring 這個例子來說,當 Spring 需要加載業務類的時候,它不是用自己的類加載器,而是用當前線程的上下文類加載器。
- 因為每個 Web 應用都會創建一個單獨的 WebAppClassLoader,并在啟動 Web 應用的線程里設置線程上下文類加載器為 WebAppClassLoader。
- 這樣就可以讓高層的類加載器(SharedClassLoader)借助子類加載器( WebAppClassLoader)來加載業務類,破壞了 Java 的類加載委托機制,讓應用逆向使用類加載器。
線程上下文類加載器的原理是將一個類加載器保存在線程私有數據里,跟線程綁定,然后在需要的時候取出來使用。這個類加載器通常是由應用程序或者容器(如 Tomcat)設置的。
Java.lang.Thread 中的getContextClassLoader()和 setContextClassLoader(ClassLoader cl)分別用來獲取和設置線程的上下文類加載器。如果沒有通過setContextClassLoader(ClassLoader cl)進行設置的話,線程將繼承其父線程的上下文類加載器。
Spring 獲取線程線程上下文類加載器的代碼如下:
cl = Thread.currentThread().getContextClassLoader();
我們在使用 JDBC 時,都需要加載 Driver 驅動,不知道你注意到沒有,不寫Class.forName("com.mysql.jdbc.Driver")
,也是可以讓 com.mysql.jdbc.Driver 正確加載,原因:
java.sql.DriverManager
public class DriverManager {// 注冊驅動的集合private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();// 初始化驅動static {loadInitialDrivers();println("JDBC DriverManager initialized");}
}
先看看 DriverManager 的類加載器:
System.out.println(DriverManager.class.getClassLoader());
打印 null,表示它的類加載器是 Bootstrap ClassLoader,會到 JAVA_HOME/jre/lib 下搜索類,但 JAVA_HOME/jre/lib 下顯然沒有 mysql-connector-java-5.1.47.jar 包,這樣問題來了,在DriverManager 的靜態代碼塊中,怎么能正確加載 com.mysql.jdbc.Driver 呢?
繼續看 loadInitialDrivers() 方法:
private static void loadInitialDrivers() {String drivers;try {drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {public String run() {return System.getProperty("jdbc.drivers");}});} catch (Exception ex) {drivers = null;}// 1.使用 ServiceLoader 機制加載驅動,即 SPIAccessController.doPrivileged(new PrivilegedAction<Void>() {public Void run() {ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);Iterator<Driver> driversIterator = loadedDrivers.iterator();try{while(driversIterator.hasNext()) {driversIterator.next();}} catch(Throwable t) {// Do nothing}return null;}});println("DriverManager.initialize: jdbc.drivers = " + drivers);// 2.使用 jdbc.drivers 定義的驅動名加載驅動if (drivers == null || drivers.equals("")) {return;}String[] driversList = drivers.split(":");println("number of Drivers:" + driversList.length);for (String aDriver : driversList) {try {println("DriverManager.Initialize: loading " + aDriver);// 這里的 ClassLoader.getSystemClassLoader() 就是應用程序類加載器Class.forName(aDriver, true,ClassLoader.getSystemClassLoader());} catch (Exception ex) {println("DriverManager.Initialize: load failed: " + ex);}}
}
先看 2. 發現它最后是使用 Class.forName 完成類的加載和初始化,關聯的是應用程序類加載器,因此可以順利完成類加載
再看 1. 它就是大名鼎鼎的 Service Provider Interface (SPI)
約定如下,在 jar 包的 META-INF/services 包下,以接口全限定名名為文件,文件內容是實現類名稱
這樣就可以使用以下代碼
ServiceLoader<接口類型> allImpls = ServiceLoader.load(接口類型.class);
Iterator<接口類型> iter = allImpls.iterator();
while(iter.hasNext()) {iter.next();
}
來得到實現類,體現的是【面向接口編程+解耦】的思想,在下面一些框架中都運用了此思想:
- JDBC
- Servlet 初始化器
- Spring 容器
- Dubbo(對 SPI 進行了擴展)
接著看 ServiceLoader.load 方法:
public static <S> ServiceLoader<S> load(Class<S> service) {// 獲取線程上下文類加載器(其實是應用程序類加載器)ClassLoader cl = Thread.currentThread().getContextClassLoader();return ServiceLoader.load(service, cl);
}
線程上下文類加載器是當前線程使用的類加載器,默認就是應用程序類加載器,它內部又是由Class.forName 調用了線程上下文類加載器完成類加載,具體代碼在 ServiceLoader 的內部類LazyIterator 中:
java.util.ServiceLoader.LazyIterator#nextService
private S nextService() {if (!hasNextService())throw new NoSuchElementException();String cn = nextName;nextName = null;Class<?> c = null;try {c = Class.forName(cn, false, loader);} catch (ClassNotFoundException x) {fail(service,"Provider " + cn + " not found");}if (!service.isAssignableFrom(c)) {fail(service,"Provider " + cn + " not a subtype");}try {S p = service.cast(c.newInstance());providers.put(cn, p);return p;} catch (Throwable x) {fail(service,"Provider " + cn + " could not be instantiated",x);}throw new Error(); // This cannot happen
}
相關文章:
JVM內存結構
- JVM內存結構筆記01-運行時數據區域
- JVM內存結構筆記02-堆
- JVM內存結構筆記03-方法區
- JVM內存結構筆記04-字符串常量池
- JVM內存結構筆記05-直接內存
- JVM內存結構筆記06-HotSpot虛擬機對象探秘
- JVM中常量池和運行時常量池、字符串常量池三者之間的關系
JVM垃圾回收
- JVM垃圾回收筆記01-垃圾回收算法
- JVM垃圾回收筆記02-垃圾回收器
JVM類加載與字節碼
- JVM類文件結構詳解
- JVM類加載過程詳解
- JVM類加載器詳解