您好,登錄后才能下訂單哦!
這篇文章主要講解了“JVM的藝術之如何使用類加載器”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“JVM的藝術之如何使用類加載器”吧!
當前類加載器(Current ClassLoader):每一個類都會使用自己的類加載器(既加載自身的類加載器)來去加載其它類(指的是所依賴的類),如果ClassX引用了ClassY,那么ClassX的類加載器就會加載ClassY(前提是ClassY尚未被加載)。
線程上下文類加載器(Context ClassLoader):線程上下文類加載器是從JDK1.2開始引入的,類Thread中的**getContextClassLoader()與setContextClassLoader(ClassLoader cl)**分別用來獲取和設置上下文類加載器。如果沒有通過setContextClassLoader(ClassLoader cl)進行設置的話,線程將繼承其父線程的上下文類加載器。
Java應用運行時初始線程的上下文類加載器是系統類加載器
為什么使用線程上下文類加載?上篇文章我也簡單的提到了。線程上下文類加載的設計初衷,原因就在于我們JAVA語言的SPI機制,我又提供了一張圖,希望下面這張圖可以全面的闡述上下文類加載器的含義。
我們在使用JDBC操作數據庫時會如下進行編寫:
Class.forName("com.mysql.driver.Driver");
Connection conn = Driver.getConnection();
Statement st = conn.getStatement();
JDBC是一個標準,這就說明使用到的Connection和Statement都是內置在JDK當中的標準,都是抽象接口,而且是位于rt.jar中,其實現肯定是由不同的數據庫廠商來實現,那么問題就來了:這些標準都是由根類加載器所加載的,但是具體的實現是由具體的廠商來做的,那肯定是需要將廠商的jar放到工程的classpath當中來進行使用,很顯然廠商的這些類是沒辦法由啟動類加載器去加載,會由應用類加載器去加載,而根據**“父類加載器所加載的類或接口是看不到子類加載器所加載的類或接口,而子類加載器所加載的類或接口是能夠看到父類加載器加載的類或接口的”這一原則,那么會導致這樣一個局面:比如說java.sql包下面的某個類會由啟動類加載器去加載,該類有可能會要訪問具體的實現類,但具體實現類是由應用類加載器所加載的,java.sql類加載器是根據看不到具體實現類加載器所加載的類的,這就是基于雙親委托模型所出現的一個非常致命的問題,這種問題不僅是在JDBC中會出現,在JNDI、xml解析等SPI(Service Provider Interface)**場景下都會出現的所以這里總結一下:父ClassLoader可以使用當前線程Thread.currentThread().getContextLoader()所指定的ClassLoader加載的類,這就改變了父ClassLoader不能使用子ClassLoader或者其它沒有直接父子關系的ClassLoader加載的類的情況,既改變了雙親委托模型。線程上下文類加載器就是當前線程的Current ClassLoader。在雙親委托模型下,類加載是由下至上的,既下層的類加載器會委托上層進行加載。但是對于SPI來說,有些接口是Java核心庫所提供的,而Java核心庫是由啟動類加載器來加載的,而這些接口的實現卻來自于不同的jar包(廠商提供)。Java的啟動類加載器是不會加載其它來源的jar包,這樣傳統的雙親委托模型就無法滿足SPI的要求。而通過給當前線程設置上下文類加載器,就可以由設置的上下文類加載器來實現對于接口實現類的加載。
很明顯JDBC會去引用JDBCImpl的具體廠商的實現,而JDBC標準是由根類加載器所加載,那對于具體實現廠商的類也會用根類加載器去加載,而由于它們是處于工程中的classPath當中,由系統類加載器去加載,很顯然是沒辦法由根類加載器去加載的,為了解決這個問題,線程的上下文類加載器就發揮作用了。
分析:由上面的理論可知:Java應用運行時初始線程的上下文類加載器是系統類加載器
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();//獲取
try{
ClassLoader targetTccl = xxx;//要設置的上下文類記載器
Thread.currentThread().setContextClassLoader(targetTccl);//設置
myMethod();//使用
} finally {
Thread.currentThread().setContextClassLoader(classLoader);//還原
}
Thread.currentThread().getContextClassLoader();//獲取
Thread.currentThread().setContextClassLoader(targetTccl);//設置
至此線程上下文類加載器就介紹到這里。
其實一個類從加載到使用是要經歷很多個過程的,下面我們來詳細的說說,一個類從加載到初始化的這個過程,然而還有哪些坑不為人知。
下面給出一張圖:
固定的類加載執行順序: 加載 驗證 準備 初始化 卸載 的執行順序是一定的 為什么解析過程沒有在這個執行順序中?(接下來分析)
什么時候觸發類加載不一定,但是類的初始化如下四種情況就要求一定初始化。 但是初始化之前 就一定會執行 加載 驗證 準備 三個階段。
觸發類加載的過程(由初始化過程引起的類加載)
1):使用new 關鍵字 獲取一個靜態屬性 設置一個靜態屬性 調用一個靜態方法。
int myValue = SuperClass.value;會導致父類初始化,但是不會導致子類初始化
SuperClass.Value = 3 ; 會導致父類初始化,不會導致子類初始化。
SubClass.staticMethod(); 先初始化父類 在初始化子類
SubClass sc = new SubClass(); 先初始化父類 再初始化子類
2):使用反射的時候,若發現類還沒有初始化,就會進行初始化
Class clazz = Class.forName("com.hnnd.classloader.SubClass");
3):在初始化一個類的時,若發現其父類沒有初始化,就會先初始化父類
SubClass.staticMethod(); 先初始化父類 在初始化子類
4):啟動虛擬機的時候,需要加載包含main方法的類.
class SuperClass{
public static int value = 5;
static {
System.out.println("Superclass ...... init........");
}
}
class SubClass extends SuperClass {
static {
System.out.println("subClass********************init");
}
public static void staticMethod(){
System.out.println("superclass value"+SubClass.value);
}
}
下面我們對類的加載、連接、初始化這幾個過程逐一的解釋:
1:加載
1.1)根據全類名獲取到對應類的字節碼流(字節流的來源 class 文件,網絡文件,還有反射的Proxygeneraotor.generaotorProxyClass)
1.2)把字節流中的靜態數據結構加載到方法區中的運行時數據結構
1.3)在內存中生成java.lang.Class對象,可以通過該對象來操作方法區中的數據結構(通過反射)
2:驗證
文件格式的驗證: 驗證class文件開頭的0XCAFFBASE 開頭
驗證主次版本號是否在當前的虛擬機的范圍之類
檢測jvm不支持的常量類型
元數據的校驗:
驗證本類是否有父類
驗證是否繼承了不允許繼承的類(final)修飾的類
驗證本類不是抽象類的時候,是否實現了所有的接口和父類的接口
**字節碼驗證:**驗證跳轉指令跳轉到 方法以外的指令.
驗證類型轉換是否為有效的, 比如子類對象賦值父類的引用是可以的,但是把父類對象賦值給子類引用是危險的
總而言之:字節碼驗證通過,并不能說明該字節碼一定沒有問題,但是字節碼驗證不通過。那么該字節碼文件一定是有問題:。
符號引用的驗證(發生在解析的過程中):
通過字符串描述的全類名是否能找到對應的類。
指定類中是否包含字段描述符,以及簡單的字段和方法名稱。
3:準備:為類變量分配內存以及設置初始值。
比如public static int value = 123;
在準備的過程中 value=0 而不是123 ,當執行類的初始化的方法的時候,value=123
若是一個靜態常量
public static final int value = 9; 那么在準備的過程中value為9.
4:解析 :把符號引用替換成直接引用
符號引用分類:
CONSTANT_Class_info 類或者接口的符號引用
CONSTANT_Fieldref_info 字段的符號引用
CONSTANT_Methodref_info 方法的符號引用
CONSTANT_intfaceMethodref_info- 接口中方法的符號引用
CONSTANT_NameAndType_info 子類或者方法的符號引用.
CONSTANT_MethodHandle_Info 方法句柄
CONSTANT_InvokeDynamic_Info 動態調用
直接引用:
指向對象的指針
相對偏移量
操作句柄
5:初始化:類的初始化時類加載的最后一步:執行類的構造器,為所有的類變量進行賦值(編譯器生成CLInit<>)
類構造器是什么?:類構造器是編譯器按照Java源文件總類變量和靜態代碼塊出現的順序來決定
靜態語句只能訪問定義在靜態語句之前的類變量,在其后的靜態變量能賦值 但是不能訪問。
父類中的靜態代碼塊優先于子類靜態代碼塊執行。
若類中沒有靜態代碼塊也沒有靜態類變量的話,那么編譯器就不會生成 Clint<>類構造器的方法。
public class TestClassInit {
public static void main(String[] args) {
System.out.println(SubClass.sub_before_v);
}
}
class SubClass extends SuperClass{
public static int sub_before_v = 5;
static {
sub_before_v = 10;
System.out.println("subclass init.......");
sub_after_v=0;
//報錯,static代碼塊中的代碼只能賦值后面的類變量 但是不能訪問。
sub_before_v = sub_after_v;
}
public static int sub_after_v = 10;
}
class SuperClass {
public static int super_before_v = 5;
static{
System.out.println("superclass init......");
}
public static int super_after_v = 10;
}
下面我們通過一系列的案例來說驗證上面所說的。先做個小的總結。
類的初始化需要對類進行主動使用,下面總結了幾點,都可以看做是對類的主動使用:
1:創建類的實例。
2:訪問某個類或者接口中的靜態變量,或者對其賦值。
3:訪問某個類的靜態方法。
4:反射。
5:初始化一個類的子類。
6:包含main方法的類。
7:jdk1.7開始提供動態語言的支持。
除了以上7種情況,都是被動使用,都不會導致類被初始化。
根據以上結論,我們來寫幾個案例,針對每種情況進行一下證明。
靜態常量初始化過程是,在jvm連接之后,靜態常量的初始化,是由調用這個靜態常量方法所在的類的常量池中被保存,此時,被調用的靜態常量所在的類的class文件就可以被刪除,即使被刪除,該常量依然有效。調用某個類的靜態常量不能初始化該類。
代碼:
package com.jdyun.jvm001;
public class TestClass03 {
public static void main(String[] args) {
System.out.println(Pet1.a);
}
}
class Pet1{
public static final int a = 10;
static {
System.out.println("我是Pet1,我被初始化了");
}
}
運行結果:
"C:\Program Files\Java\jdk-11.0.2\bin\java.exe" "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2019.2\lib\idea_rt.jar=64451:C:\Program Files\JetBrains\IntelliJ IDEA 2019.2\bin" -Dfile.encoding=UTF-8 -classpath G:\jdyun-jvm\out\production\jdyun-jvm com.jdyun.jvm001.TestClass03
10
Process finished with exit code 0
從上面這個案例可知,一個類調用另一個類的常量不會導致一個類的初始化。
代碼:
package com.jdyun.jvm001;
import java.util.UUID;
public class TestClass03 {
public static void main(String[] args) {
System.out.println(Pet1.a);
}
}
class Pet1{
public static final String a = UUID.randomUUID().toString();
static{
System.out.println("我被初始化了");
}
}
運行結果:
"C:\Program Files\Java\jdk-11.0.2\bin\java.exe" "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2019.2\lib\idea_rt.jar=50237:C:\Program Files\JetBrains\IntelliJ IDEA 2019.2\bin" -Dfile.encoding=UTF-8 -classpath G:\jdyun-jvm\out\production\jdyun-jvm com.jdyun.jvm001.TestClass03
我被初始化了
e5b56749-5a97-405f-9fe9-dfe4211bc0ce
Process finished with exit code 0
靜態變量初始化與靜態常量初始化不同,靜態變量初始化是在初始化階段被賦予真實的值比如int a = 2,那么2會被真正的賦值給a。
如果某個類調用了該類的靜態變量,那么靜態變量所在的類就會被視為被主動調用了。那么該類就會被初始化。
該類如果有靜態代碼塊兒那么靜態代碼塊兒的優先級高于靜態變量。
如果該靜態變量所在的類中有父類,那么會優先初始化父類。
package com.jdyun.jvm001;
import java.util.Random;
import java.util.UUID;
public class TestClass03 {
public static void main(String[] args) {
System.out.println(Dog3.a);
}
}
class Dog3 extends Pet1{
public static final int a = new Random().nextInt();
static {
System.out.println("我是Pet1,我是父類,我被最先加載了");
}
}
class Pet1{
static{
System.out.println("我被初始化了");
}
}
運行結果:
"C:\Program Files\Java\jdk-11.0.2\bin\java.exe" "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2019.2\lib\idea_rt.jar=64951:C:\Program Files\JetBrains\IntelliJ IDEA 2019.2\bin" -Dfile.encoding=UTF-8 -classpath G:\jdyun-jvm\out\production\jdyun-jvm com.jdyun.jvm001.TestClass03
我被初始化了
我是Pet1,我是父類,我被最先加載了
-1203457101
Process finished with exit code 0
驗證初始化次數,只會被初始化一次。
package com.jdyun.jvm001;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.UUID;
public class MyTest02 extends ClassLoader{
public static void main(String[] args) throws ClassNotFoundException {
//1,驗證初始化次數
for(int i=0;i<50;i++){
Test01 test01 = new Test01();
}
}
}
class Test01{
static{
System.out.println("我被初始化了");
}
}
運行結果:
"C:\Program Files\Java\jdk-11.0.2\bin\java.exe" "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2019.2\lib\idea_rt.jar=65340:C:\Program Files\JetBrains\IntelliJ IDEA 2019.2\bin" -Dfile.encoding=UTF-8 -classpath G:\jdyun-jvm\out\production\jdyun-jvm com.jdyun.jvm001.MyTest02
我被初始化了
Process finished with exit code 0
接口的初始化,子接口的初始化不會導致父接口的初始化,如果可以導致父接口的初始化,那么Test01類中的靜態代碼塊兒就會被打印。很顯然結果來看,Test01
中的靜態代碼塊兒沒有被打印,所以,接口的初始化中,子接口的初始化,不會導致父接口的初始化。
package com.jdyun.jvm001;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.UUID;
public class MyTest02 extends ClassLoader{
public static void main(String[] args) throws ClassNotFoundException {
//2,接口初始化,子接口的初始化不會導致父接口的初始化
System.out.println(MyChild.b);
/* System.out.println(MyParent.test01);
System.out.println(MyChild.test001);*/
//3,反射初始化類
//Class.forName("com.jdyun.jvm001.Test01");
//4,創建數組不會導致類的初始化
//Test01[] test01 = new Test01[1];
//5,靜態變量賦值
//System.out.println(MyChild.b);
//Class clesses = String.class;
}
}
class Test01{
static{
System.out.println("Test01被初始化了");
}
}
interface MyParent{
Test01 test01 = new Test01();
public static final String a="5";
}
interface MyChild extends MyParent {
public static Integer b= UUID.randomUUID().hashCode();
}
"C:\Program Files\Java\jdk-11.0. 2\bin\java.exe" "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2019.2\lib\idea_rt.jar=49632:C:\Program Files\JetBrains\IntelliJ IDEA 2019.2\bin" -Dfile.encoding=UTF-8 -classpath G:\jdyun-jvm\out\production\jdyun-jvm com.jdyun.jvm001.MyTest02
-221561202
Process finished with exit code 0
創建一個數組,不會導致類的初始化。
package com.jdyun.jvm001;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.UUID;
public class MyTest02 extends ClassLoader{
public static void main(String[] args) throws ClassNotFoundException {
//4,創建數組不會導致類的初始化
Test01[] test01 = new Test01[1];
}
}
class Test01{
static{
System.out.println("Test01被初始化了");
}
}
運行結果:
"C:\Program Files\Java\jdk-11.0.2\bin\java.exe" "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2019.2\lib\idea_rt.jar=50058:C:\Program Files\JetBrains\IntelliJ IDEA 2019.2\bin" -Dfile.encoding=UTF-8 -classpath G:\jdyun-jvm\out\production\jdyun-jvm com.jdyun.jvm001.MyTest02
Process finished with exit code 0
此處聲明的靜態常量,按照之前的理解是靜態常量被調用不會初始化該靜態常量所在的類 但是此處當靜態常量的值是一個引用類型的時候,這個時候該靜態常量所在的類就會被初始化 故此會先打印我被初始化了,然后在打印a的隨機值
package com.jdyun.jvm07;
import java.util.Random;
import java.util.UUID;
/**
* 此處聲明的靜態常量,按照之前的理解是靜態常量被調用不會初始化該靜態常量所在的類
* 但是此處當靜態常量的值是一個引用類型的時候,這個時候該靜態常量所在的類就會被初始化
* 故此會先打印我被初始化了,然后在打印a的隨機值
*/
public class Test {
public static void main(String[] args) {
System.out.println(Pet.a);
}
}
class Pet{
public static final String a = UUID.randomUUID().toString();
static{
System.out.println("我被初始化了");
}
}
運行結果:
"C:\Program Files\Java\jdk-11.0.2\bin\java.exe" "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2019.2\lib\idea_rt.jar=50995:C:\Program Files\JetBrains\IntelliJ IDEA 2019.2\bin" -Dfile.encoding=UTF-8 -classpath G:\jdyun-jvm\out\production\jdyun-jvm com.jdyun.jvm07.Test
我被初始化了
3febaad7-90fe-4d7f-be1c-62b70b9f41cc
Process finished with exit code 0
對子接口靜態常量調用時,父接口沒有被加載也并沒有被初始化。當我們有兩個接口,父子接口,然后在子接口中聲明一個靜態變量,此時對子接口中的靜態變量進行主動調用,此時父接口沒有被初始化,也沒有被加載。(刪除父接口中的class)
package com.jdyun.jvm8;
import java.util.Random;
public class Test {
public static void main(String[] args) {
System.out.println(MyChild.b);
}
}
interface MyParent{
public static final String a="5";
}
interface MyChild extends MyParent{
public static Integer b= 1;
}
運行結果:
"C:\Program Files\Java\jdk-11.0.2\bin\java.exe" "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2019.2\lib\idea_rt.jar=51297:C:\Program Files\JetBrains\IntelliJ IDEA 2019.2\bin" -Dfile.encoding=UTF-8 -classpath G:\jdyun-jvm\out\production\jdyun-jvm com.jdyun.jvm8.Test
1
Process finished with exit code 0
接口中的變量賦予引用初始值會初始化子接口。
public class Test {
public static void main(String[] args) {
System.out.println(MyChild.b);
}
}
interface MyParent{
public static String a=5;
}
interface MyChild extends MyParent{
Integer b= new Random().nextInt(2);
}
感謝各位的閱讀,以上就是“JVM的藝術之如何使用類加載器”的內容了,經過本文的學習后,相信大家對JVM的藝術之如何使用類加載器這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。