一文理类加载相关知识:类加载器、双亲委派、SPI
思維導圖
類加載的時機
類加載的流程
類從被加載到內存中開始,直到被從內存中卸載為止,它的整個生命周期包括:驗證、準備、解析、初始化、使用和卸載7 個階段。
其中驗證、準備、解析 3 個部分統稱為連接(Linking)
1.加載(重點)
類加載過程的第一步,主要完成下面 3 件事情:
加載階段(準確地說,是加載階段中獲取類的二進制字節流的動作)可以使用系統提供的類加載器(ClassLoader)來完成,也可以由用戶自定義的類加載器完成,開發人員可以通過定義自己的類加載器去控制字節流的獲取方式。
加載階段完成后,虛擬機外部的二進制字節流就按照虛擬機所需的格式存儲在方法區之中,方法區中的數據存儲格式由虛擬機實現自行定義,虛擬機并未規定此區域的具體數據結構。然后在java堆中實例化一個java.lang.Class類的對象,這個對象作為程序訪問方法區中的這些類型數據的外部接口。
2.驗證
驗證是鏈接階段的第一步,這一步主要的目的是確保class文件的字節流中包含的信息符合當前虛擬機的要求,并且不會危害虛擬機自身安全。
驗證階段主要包括四個檢驗過程:文件格式驗證、元數據驗證、字節碼驗證和符號引用驗證。
3.準備
準備階段是正式為類變量分配內存并設置類變量初始值的階段,這些內存都將在方法區中進行分配。這個階段中有兩個容易產生混淆的知識點,首先是這時候進行內存分配的僅包括類變量(static 修飾的變量),而不包括實例變量,實例變量將會在對象實例化時隨著對象一起分配在java堆中。
4.解析
解析階段是虛擬機常量池內的符號引用替換為直接引用的過程。
- 符號引用:符號引用是一組符號來描述所引用的目標對象,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可。符號引用與虛擬機實現的內存布局無關,引用的目標對象并不一定已經加載到內存中。
- 直接引用:直接引用可以是直接指向目標對象的指針、相對偏移量或是一個能間接定位到目標的句柄。直接引用是與虛擬機內存布局實現相關的,同一個符號引用在不同虛擬機實例上翻譯出來的直接引用一般不會相同,如果有了直接引用,那引用的目標必定已經在內存中存在。
5.初始化
類的初始化階段是類加載過程的最后一步,在準備階段,類變量已賦過一次系統要求的初始值,而在初始化階段,則是根據程序員通過程序制定的主觀計劃去初始化類變量和其他資源,或者可以從另外一個角度來表達:初始化階段是執行類構造器()方法的過程。
類加載器
類加載器主要分為四類:
BootStrap ClassLoader:啟動類加載器,C++實現的,是Java類加載層次中最頂層的類加載器(JVM啟動后初始化的),負責加載JDK中的核心類庫,如:rt.jar、resources.jar、charsets.jar等;
ExtensionClassLoader:擴展類加載器,負責加載Java的擴展類庫,默認加載JAVA_HOME/jre/lib/ext/目下的所有jar。該加載器是有java實現的,由Bootstrploader加載ExtClassLoader,并且將ExtClassLoader的父加載器設置為Bootstrp loader;
AppClassLoader:系統類加載器,負責加載應用程序classpath目錄下的所有jar和class文件。
CustomLoader:自定義類加載器,負責加載指定的目錄和文件
雙親委派
類加載器在加載類時,會先委托父類加載器去加載該類,如果父類加載器無法加載才會嘗試自己加載。
當一個ClassLoader實例需要加載某個類時,它會先檢查父類加載器(一直檢查到Bootstrap ClassLoader)是否已經加載過該類,如果父類加載器已經加載該類則直接返回該類對象。然后由上至下依次加載類,首先由最頂層的類加載器BootstrapClassLoader試圖加載,如果沒加載到,則把任務轉交給Extension ClassLoader試圖加載,如果也沒加載到,則轉交給AppClassLoader進行加載,如果它也沒有加載得到的話,則返回給委托的發起者,由它到指定的文件系統或網絡等URL中加載該類。如果它們都沒有加載到這個類時,則拋出ClassNotFoundException異常。
JVM在判定兩個Class是否相同時,不僅要判斷兩個類名是否相同,而且要判斷是否由同一個類加載器實例加載的。只有兩者同時滿足的情況下,JVM才認為這兩個class是相同的。
雙親委派的優點
打破雙親委派
在實際的應用中雙親委派解決了java 基礎類統一加載的問題,但是也存在著問題。jdk中的基礎類作為用戶api被調用,但是也存在調用用戶的代碼的情況,典型的如SPI。
Java 提供了很多服務提供者接口(Service Provider Interface,SPI),允許第三方為這些接口提供實現。常見的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。
這些 SPI 的接口由 Java 核心庫來提供,而這些 SPI 的實現代碼則是作為 Java 應用所依賴的 jar 包被包含進類路徑(CLASSPATH)里。SPI接口中的代碼經常需要加載具體的實現類。
那么問題來了,SPI的接口是Java核心庫的一部分,是由啟動類加載器(Bootstrap Classloader)加載的,而SPI的實現類是由系統類加載器(App ClassLoader)來加載的。啟動類加載器是無法找到 SPI 的實現類的,因為依照雙親委派模型,BootstrapClassloader無法委派AppClassLoader來加載類。
(1)線程上下文類加載器
為解決上述問題,引入了線程上下文類加載器(Thread Context ClassLoader),線程上下文類加載器可以通過java.lang.Thread 類的setContextClassLoader方法進行設置。默認情況下為系統類加載器(App ClassLoader)。
通過線程上下文類加載器,父類即可打破雙親委派模型,委托子類加載器實現類的加載。當父類無法加載某個類時,就可以委托線程上下文類加載器加載對應的類。
(2)自定義類加載器覆寫loadClass()
自定義加載器,需要繼承 ClassLoader 。如果不想打破雙親委派模型,就重寫 ClassLoader 類中的 findClass() 方法,無法被父類加載器加載的類最終會通過這個方法被加載。但是,如果想打破雙親委派模型則需要重寫 loadClass() 方法。
SPI
SPI(服務提供接口) ,全稱為 Service Provider Interface,可以理解為調用方來制定接口規范,提供給外部來實現,調用方在調用時則選擇自己需要的外部實現。SPI接口一般在核心庫里,由BootStrap ClassLoader加載。
SPI是一種服務發現機制。SPI約定在 Classpath 下的 META-INF/services/ 目錄里創建一個以服務接口命名的文件(如:java.sql.Driver),然后文件里面記錄的是此 jar 包提供的接口具體實現類的全限定名(如:Mysql中提供的 com.mysql.cj.jdbc.Driver)。
在加載接口的實現類時,通過在查找ClassPath路徑下的META-INF/services文件夾中存有實現類類名的文件,并實例化文件所定義的實現類,來實例化某個接口。
SPI 通過 ServiceLoader.load() 去完成上述的實例化META-INF/services中的類。ServiceLoader.load() 會通過 線程上下文類加載器(默認為App Loader)打破雙親委派,委子類類加載器去加載實現類。
SPI的主要流程:約定一個目錄,調用ServiceLoader.load()根據接口名去那個目錄找到文件,文件解析得到實現類的全限定名,然后循環加載實現類和創建其實例。
圖片來源
Java SPI的缺點
Java SPI 無法按需加載實現類:Java SPI 在查找擴展實現類的時候遍歷 SPI 的配置文件并且將實現類全部實例化,假設一個實現類初始化過程比較消耗資源且耗時,但是你的代碼里面又用不上它,這就產生了資源的浪費。
推薦閱讀:
- 推薦:三歪問我Dubbo的SPI機制是啥?(帶有ServiceLoader的源碼分析)
- Java SPI詳解
- 推薦:深入理解SPI機制
總結
類的加載過程基本如下圖:
(1)自定義類加載器覆寫了loadClass()方法
(2)父類加載器需要使用由子類加載器加載的類,此時父類加載器會使用線程上下文加載器,去委托子類加載器去加載相應的類
(1)當高層提供了統一接口讓低層去實現,同時又要是在高層加載(或實例化)低層的類時,必須通過線程上下文類加載器來幫助高層的ClassLoader找到并加載該類。
(2)當使用本類托管類加載,然而加載本類的ClassLoader未知時,為了隔離不同的調用者,可以取調用者各自的線程上下文類加載器代為托管。
參考
- 推薦:【JVM】淺談雙親委派和破壞雙親委派
- 推薦:詳細jvm-類加載機制
- 推薦:Java 類加載器
- 推薦:深入理解SPI機制
- 真正理解線程上下文類加載器(多案例分析)
- 自定義類加載器:從網上加載class到內存、實例化調用其中的方法
- jvm(1)類的加載(三)(線程上下文加載器)
- 類加載過程
總結
以上是生活随笔為你收集整理的一文理类加载相关知识:类加载器、双亲委派、SPI的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: godaddy怎么注册域名(godadd
- 下一篇: 影楼怎么制作网页(影楼怎么制作网页视频)