今日分享開始啦,請大家多多指教~
1。 類載入階段
1。1 載入階段
將類的位元組碼載入方法區中,內部採用 C++ 的 instanceKlass 描述 java 類,它的重要 field 有:
_java_mirror 即 java 的類映象,例如對 String 來說,就是 String。class,作用是把 klass 暴 露給 java 使用
_super 即父類
_fields 即成員變數
_methods 即方法
_constants 即常量池
_class_loader 即類載入器
_vtable 虛方法表
_itable 介面方法表
如果這個類還有父類沒有載入,則先觸發父類的載入。
載入和連結可能是交替執行的。
注意:
instanceKlass 這樣的【元資料】是儲存在方法區(1。8 後的元空間內),但 _java_mirror 是儲存在堆中
可以透過前面介紹的 HSDB 工具檢視
1。2 連結階段
驗證
驗證類是否符合 JVM規範,安全性檢查,阻止不合法的類繼續執行。用 UE 等支援二進位制的編輯器修改 HelloWorld。class的魔數,在控制檯執行:
準備
為 static 變數分配空間,設定預設值:
static 變數在 JDK 7 之前儲存於 instanceKlass 末尾,從 JDK 7 開始,儲存於 _java_mirror 末尾
static 變數分配空間和賦值是兩個步驟,分配空間在準備階段完成,賦值在初始化階段完成
如果 static 變數是 final 的基本型別,以及字串常量,那麼編譯階段值就確定了,賦值在準備階段完成
如果 static 變數是 final 的,但屬於引用型別,那麼賦值也會在初始化階段完成
將常量池中的符號引用解析為直接引用
解析
將常量池中的符號引用解析為直接引用:
1。3 初始化階段
< init()> V 方法
初始化即呼叫 < cinit>()V ,虛擬機器會保證這個類的『構造方法』的執行緒安全。
發生的時機
概括地說,類初始化是【懶惰的】
main 方法所在的類,總會被首先初始化
首次訪問這個類的靜態變數或靜態方法時
子類初始化,如果父類還沒初始化,會引發
子類訪問父類的靜態變數,只會觸發父類的初始化
Class。forName
new 會導致初始化
不會導致類初始化的情況:
訪問類的 static final 靜態常量(基本型別和字串)不會觸發初始化
類物件。class 不會觸發初始化
建立該類的陣列不會觸發初始化
類載入器的 loadClass 方法
測試程式碼:
驗證(測試時請先全部註釋,每次只執行其中一個)
1。4 練習
從位元組碼分析,使用 a,b,c 這三個常量是否會導致 E 初始化:
典型應用 - 完成懶惰初始化單例模式:
以上的實現特點是:
懶惰例項化
初始化時的執行緒安全是有保障的
2。 類載入器
以 JDK 8 為例:
類載入器的優先順序(由高到低):
啟動類載入器 -> 擴充套件類載入器 -> 應用程式類載入器 -> 自定義類載入器
2。1 啟動類載入器
用 Bootstrap 類載入器載入類:
執行:
輸出:
-Xbootclasspath 表示設定 bootclasspath
其中 /a:。 表示將當前目錄追加至 bootclasspath 之後
可以有以下幾個方式替換啟動類路徑下的核心類:
java -Xbootclasspath: < new bootclasspath>
前追加:java -Xbootclasspath/a:<追加路徑>
後追加:java -Xbootclasspath/p:<追加路徑>
2。2 擴充套件類載入器
程式執行:
輸出結果:
寫一個同名的類:
打個 jar 包:
將 jar 包複製到JAVA_HOME/jre/lib/ext(擴充套件類載入器載入的類必須是以jar包方式存在),重新執行 Load5_2
輸出:
2。3 雙親委派模式
所謂的雙親委派,就是指呼叫類載入器的 loadClass 方法時,查詢類的規則。
注意:這裡的雙親,翻譯為上級似乎更為合適,因為它們並沒有繼承關係
例如:
執行流程為:
sun。misc。Launcher$AppClassLoader // 1 處, 開始檢視已載入的類,結果沒有
sun。misc。Launcher$AppClassLoader // 2 處,委派上級 sun。misc。Launcher$ExtClassLoader。loadClass()
sun。misc。Launcher$ExtClassLoader // 1 處,檢視已載入的類,結果沒有
sun。misc。Launcher$ExtClassLoader // 3 處,沒有上級了,則委派 BootstrapClassLoader 查詢
BootstrapClassLoader 是在 JAVA_HOME/jre/lib 下找 H 這個類,顯然沒有
sun。misc。Launcher$ExtClassLoader // 4 處,呼叫自己的 findClass 方法,是在JAVA_HOME/jre/lib/ext 下找 H 這個類,顯然沒有,回到 sun。misc。Launcher$AppClassLoader 的 // 2 處
繼續執行到 sun。misc。Launcher$AppClassLoader // 4 處,呼叫它自己的 findClass 方法,在 classpath 下查詢,找到了
2。4 執行緒上下文類載入器
我們在使用 JDBC 時,都需要載入 Driver 驅動,不知道你注意到沒有,不寫
Class。forName(“com。mysql。jdbc。Driver”)
也是可以讓 com。mysql。jdbc。Driver 正確載入的,你知道是怎麼做的嗎? 讓我們追蹤一下原始碼:
先不看別的,看看 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() 方法:
先看 2)發現它最後是使用 Class。forName 完成類的載入和初始化,關聯的是應用程式類載入器,因此 可以順利完成類載入
再看 1)它就是大名鼎鼎的 Service Provider Interface (SPI)
約定如下,在 jar 包的 META-INF/services 包下,以介面全限定命名為檔案,檔案內容是實現類名稱
這樣就可以使用:
來得到實現類,體現的是【面向介面程式設計+解耦】的思想,在下面一些框架中都運用了此思想:
JDBC
Servlet 初始化器
Spring 容器
Dubbo(對 SPI 進行了擴充套件)
接著看 ServiceLoader。load 方法:
執行緒上下文類載入器是當前執行緒使用的類載入器,預設就是應用程式類載入器,它內部又是由 Class。forName 呼叫了執行緒上下文類載入器完成類載入,具體程式碼在 ServiceLoader 的內部類 LazyIterator 中:
2。5 自定義類載入器
問問自己,什麼時候需要自定義類載入器:
1)想載入非 classpath 隨意路徑中的類檔案
2)都是透過介面來使用實現,希望解耦時,常用在框架設計
3)這些類希望予以隔離,不同應用的同名類都可以載入,不衝突,常見於 tomcat 容器
步驟:
繼承 ClassLoader 父類
要遵從雙親委派機制,重寫 findClass 方法 注意不是重寫 loadClass 方法,否則不會走雙親委派機制
讀取類檔案的位元組碼
呼叫父類的 defineClass 方法來載入類
使用者呼叫該類載入器的 loadClass 方法
今日份分享已結束,請大家多多包涵和指點!