# jvm-第五節類加載器
本篇知識點概況
- 字節碼相關知識
- 字節碼的基礎知識,瞭解字節碼的概念,用途,特點
- 字節碼的分析工具使用
- class文件格式格式與詳解
- 字節碼的指令:基礎存儲運算指令,異常處理,裝箱拆箱,數組
- 熱點探測之jit與字節碼
- 類加載與類加載器相關知識
- 一個類的生命週期
- jdk提供的三種類加載器
- 自定義類加載器
- 問題:
字節碼相關知識
1.字節碼的基礎知識,瞭解字節碼的概念,用途,特點
- 字節碼是一堆指令的集合,它具有平臺無關性,可以運行在任何支持jvm的平臺上;是Java程序在運行時的執行單元;它是一種中間形式的代碼,是源代碼和機器碼之間的橋樑
2.字節碼的分析工具使用
- 以這個工具舉例,可以查看具體的指令,可以修改裏面的值,github地址https://github.com/ingokegel/jclasslib
3.class文件格式格式與詳解
- class文件中各個部分的含義
- class文件裏是16進制的(一個字節佔8位,一個十六進制佔4位,所以cafebabe佔32位),前四個字節0xCAFEBABE;作用是標識這是一個有效的class文件;
- 後面的 0000 0034表示版本;其中第五六個字節表示次版本號,第七八字節是主版本號(0034(16)=52(10));Java 版本從45開始jdk1.1每發行一個大版本就加一
- 常量池 0010 表示常量的數量,常量池中常見的倆類常量,字面量以及符號引用,字面量一般是靜態常量,符號引用是類的全限定名字段名,方法名;
- 訪問標誌(Access Flags):(瞭解)
- 訪問標誌指示了類或者接口的訪問權限和屬性,如是否是public、final、abstract等。
- 訪問標誌由兩個字節表示,使用特定的標誌位來表示不同的訪問屬性。
- 類索引、父類索引和接口索引(This Class, Super Class, Interfaces):(瞭解)
- 類索引指向當前類在常量池中的類描述符的常量項。
- 父類索引指向當前類的直接父類在常量池中的類描述符的常量項。
- 接口索引表存儲了當前類所實現的接口的索引,每個接口索引指向常量池中的接口描述符的常量項。
- 字段表(Fields):(瞭解)
- 字段表用於描述類或接口中定義的字段信息,包括字段的訪問標誌、名稱、描述符等。
- 字段表中的每一項都包含了字段的相關信息,可以是類變量、實例變量或常量。
- 方法表(Methods):(瞭解)
- 方法表用於描述類或接口中定義的方法信息,包括方法的訪問標誌、名稱、描述符等。
- 方法表中的每一項都包含了方法的相關信息,包括方法的參數列表、返回值類型、異常表等。
- 屬性表(Attributes):(瞭解)
- 屬性表用於存儲與類、字段或方法相關的附加信息,如註解、源文件信息、行號表等。
- 屬性表包含了屬性的名稱、長度以及具體的屬性值。
4.字節碼的指令:基礎存儲運算指令,異常處理,裝箱拆箱,數組(瞭解)
- 基礎存儲運算指令:包括將值加載到操作數棧上、從操作數棧上存儲值到變量中等操作。例如:
iload
:將int類型的變量加載到操作數棧上istore
:將int類型的值存儲到變量中
- 異常處理指令:用於處理異常情況,包括拋出異常和捕獲異常。例如:
athrow
:拋出異常try-catch
:捕獲和處理異常
- 裝箱拆箱指令:用於基本類型和對應的包裝類之間的轉換。例如:
invokestatic
:調用靜態方法進行裝箱或拆箱invokevirtual
:調用包裝類的實例方法進行裝箱或拆箱
- 數組指令:用於創建和操作數組。例如:
newarray
:創建一個基本類型的數組anewarray
:創建一個引用類型的數組arraylength
:獲取數組的長度iaload
:將int類型的值加載到操作數棧上
5.熱點探測之jit與字節碼
-
java程序運行時主要就是執行字節碼指令,解釋執行時需要翻譯成機器碼,這個效率比較低,爲了提高效率就有了jit(just in time compiler);
-
熱點代碼,比如for裏的代碼就會緩存起來,爲了下次用
-
熱點探測:在 HotSpot 虛擬機中的熱點探測是 JIT 優化的條件,熱點探測是基於計數器的熱點探測,採用這種方法的虛擬機會爲每個方法建立計數器統計方法的執
行次數,如果執行次數超過一定的閾值就認爲它是“熱點方法”
虛擬機爲每個方法準備了兩類計數器:方法調用計數器(
Invocation Counter)和回邊計數器(Back Edge Counter)。在確定虛擬機運行參數的前提下,這
兩個計數器都有一個確定的閾值,當計數器超過閾值溢出了,就會觸發 JIT 編譯。
-
方法調用計數器:用於統計方法被調用的次數,方法調用計數器的默認閾值在 C1 模式下是 1500 次,在 C2 模式在是 10000 次,可通過 -XX: CompileThreshold 來設定;
而在分層編譯的情況下,-XX: CompileThreshold 指定的閾值將失效,此時將會根據當前待編譯的方法數以及編譯線程數來動態調整。當方法計數器和回邊
計數器之和超過方法計數器閾值時,就會觸發 JIT 編譯器。
-
回邊計數器:用於統計一個方法中循環體代碼執行的次數,在字節碼中遇到控制流向後跳轉的指令稱爲“回邊”(Back Edge),該值用於計算是否觸發 C1 編譯的閾值,
在不開啓分層編譯的情況下,C1 默認爲 13995,C2 默認爲 10700,可通過 -XX: OnStackReplacePercentage=N 來設置;而在分層編譯的情況下,-XX:
OnStackReplacePercentage 指定的閾值同樣會失效,此時將根據當前待編譯的方法數以及編譯線程數來動態調整。
建立回邊計數器的主要目的是爲了觸發 OSR(On StackReplacement)編譯,即棧上編譯。在一些循環週期比較長的代碼段中,當循環達到回邊計數器閾值時,JVM 會認爲這段是熱點代碼,JIT 編譯器就會將這段代碼編譯成機器語言並緩存,在該循環時間段內,會直接將執行代碼替換,執行緩存的機器語
言。
6.字節碼總結:都是一些需要了解的東西,在以後jvm調優,會用到
類加載與類加載器相關知識
1.一個類的生命週期
- 一個類的生命週期:加載,驗證,準備,解析,初始化,使用,卸載
- 這裏有個順序問題,解析的順序可能在初始化之後纔開始,爲了支持Java的動態綁定;
- 加載的時機:
- 初次使用時
- 引用了類中的某個靜態屬性
- 反射調用
- 子類繼承時調用
- 驗證:包括四種驗證,文件格式,元數據,字節碼,符號引用驗證,驗證重要但不是必要步驟,如果覺得沒必要可以通過參數關閉-Xverify:none
- 準備:爲類中的static修飾的屬性賦初始值,這是賦值表
- 解析:將符號引用替換成直接引用的過程
- 初始化:
- 當遇到四個關鍵字 new getstatic putstatic 和invokestatic時就會觸發初始化
- 觸發反射時
- 觸發子類時,父類要初始化
- 當虛擬機啓動時,用戶需要指定一個要執行的主類(包含 main()方法的那個類),虛擬機會先初始化這個主類
- 下面的案例子類調用父類的屬性時,子類和父類的加載情況和初始化情況,這塊建議看一下字節碼,看他是否加載了子類
- 線程安全:由於一個類的初始化時一個一個線程來的,其他線程都阻塞,所以是線程安全的
2.jdk提供的三層類加載器
- bootstrap classloader:加載核心類庫,任何加載行爲都要經過他,c++編寫,隨着jvm啓動;
- extention classloader:主要加載lib/ext 下的jar和class文件,這是一個java類繼承了 urlClassLoader
- application classloader:是默認的Java類加載器,加載classPath下的jar class文件,我們寫的代碼首先用這個加載器
- custom classloader:自定義加載器,支持一些擴展,下面詳細說
- 類加載器問題;對於任意一個類,同一個類加載器,同一個類,確定jvm中該類的唯一性,注意每一個類加載器有自己獨立的命名空間
- 雙親委派機制:向上委託,向下加載,可以避免重複加載一個類
3.自定義類加載器tomcat
- 先說結論, 如何解決tomcat通過war發佈服務違背雙親委派機制的問題?
- 將第三方依賴放入tomcat的公共目錄,由公共加載器加載就實現了共享,使用webappclassloader加載器加在web就先實現了每個web有自己獨立的類加載器,實現了隔離;
- 爲什麼說tomcat通過war發佈服務是違背了雙親委派機制,因爲tomcat有一個webappclassloader,擁有加載web的優先權,這意味着當他需要加載類時會首先搜索自己的路徑( 即
WEB-INF/classes
和WEB-INF/lib
目錄 ); - 如果一個jvm運行着倆個不同版本的web,是如何解決的呢?看下面的代碼
自定義類加載器-擴展
-
spi:service provider interface,是一套被第三方實現,擴展的api,他不是在編譯時檢查,而是在運行時加載,表現爲當我們寫一行代碼 class.forName("com.mysql.jdbc.Driver"),不會報錯 ,詳細的解釋如下
-
在Java的SPI(Service Provider Interface)機制中,
Class.forName("com.mysql.jdbc.Driver")
並不需要引入對應的JAR文件來讓代碼編譯通過。這是因爲在SPI機制中,服務提供者的具體實現類是通過類路徑(Classpath)動態加載的,而不是在編譯時就確定的。當你調用
Class.forName("com.mysql.jdbc.Driver")
時,JVM會嘗試在類路徑上查找並加載com.mysql.jdbc.Driver
類。如果找到了該類,JVM就會加載它並執行相應的靜態代碼塊。這時,com.mysql.jdbc.Driver
類會向JVM註冊自己作爲MySQL數據庫的驅動程序。這樣,在後續的代碼中,你就可以使用java.sql.DriverManager
類來獲取MySQL數據庫的連接。如果你在運行時沒有將MySQL驅動程序的JAR文件放在類路徑上,
Class.forName("com.mysql.jdbc.Driver")
會拋出ClassNotFoundException
異常。但是,如果你確保MySQL驅動程序的JAR文件已經由其他方式加載(如通過Tomcat的公共庫目錄),那麼Class.forName("com.mysql.jdbc.Driver")
就不會拋出異常,因爲類加載器已經能夠找到並加載了com.mysql.jdbc.Driver
類。需要注意的是,最新的MySQL驅動已經遷移到了
com.mysql.cj.jdbc.Driver
類,而不再是com.mysql.jdbc.Driver
。因此,如果你使用的是較新的MySQL驅動版本,應該使用Class.forName("com.mysql.cj.jdbc.Driver")
來加載驅動類。總結來說,
Class.forName("com.mysql.jdbc.Driver")
不會在編譯時檢查類的存在與否,而是在運行時動態加載類。如果類路徑上存在對應的類,加載就會成功,否則會拋出ClassNotFoundException
異常。
-
問題
- 爲什麼要拆箱裝箱
- integerCache
- 雙親委派機制的原名: Parent Delegation Mode , 父級委託模型 這個名稱更貼切一些