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的這種SPI場景用圖來更具體的描述一下:

很明顯JDBC會去參考JDBCImpl的具體廠商的實作,而JDBC標準是由根類加載器所加載,那對于具體實作廠商的類也會用根類加載器去加載,而由于它們是處于工程中的classPath當中,由系統類加載器去加載,很顯然是沒辦法由根類加載器去加載的,為了解決這個問題,執行緒的背景關系類加載器就發揮作用了,
分析:
由上面的理論可知:Java應用運行時初始執行緒的背景關系類加載器是系統類加載器
那思考一下:為什么默認的執行緒背景關系類加載器就是系統類加載器呢?肯定是在某個地方給設定了,其實它是在Launcher中進行設定的,如下:

1、執行緒背景關系類加載器的一般使用模式(獲取 - 使用 - 還原)
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();//獲取
try{
ClassLoader targetTccl = xxx;//要設定的背景關系類記載器
Thread.currentThread().setContextClassLoader(targetTccl);//設定
myMethod();//使用
} finally {
Thread.currentThread().setContextClassLoader(classLoader);//還原
}
2、如果一個類由類加載器A加載,那么這個類的依賴類也是由相同的類加載器加載的(如果該依賴類之前沒有被加載過的話),ContextClassLoader的作用就是為破壞Java的類加載委托機制,
3、當高層提供了統一的介面讓低層來實作,同時又要在高層加載(或實體化)低層的類時,就必須要通過執行緒背景關系類加載器來幫助高層的ClassLoader找到并加載該類,
Thread.currentThread().getContextClassLoader();//獲取
Thread.currentThread().setContextClassLoader(targetTccl);//設定
至此執行緒背景關系類加載器就介紹到這里,
類加載的程序
其實一個類從加載到使用是要經歷很多個程序的,下面我們來詳細的說說,一個類從加載到初始化的這個程序,然而還有哪些坑不為人知,
下面給出一張圖:

固定的類加載執行順序: 加載 驗證 準備 初始化 卸載 的執行順序是一定的 為什么決議程序沒有在這個執行順序中?(接下來分析)
什么時候觸發類加載不一定,但是類的初始化如下四種情況就要求一定初始化, 但是初始化之前 就一定會執行 加載 驗證 準備 三個階段,
觸發類加載的程序(由初始化程序引起的類加載)
1):使用new 關鍵字 獲取一個靜態屬性 設定一個靜態屬性 呼叫一個靜態方法,
? int myValue = https://www.cnblogs.com/tomakemyself/p/SuperClass.value;會導致父類初始化,但是不會導致子類初始化
? SuperClass.Value = https://www.cnblogs.com/tomakemyself/p/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 = https://www.cnblogs.com/tomakemyself/p/123;
? 在準備的程序中 value=https://www.cnblogs.com/tomakemyself/p/0 而不是123 ,當執行類的初始化的方法的時候,value=123
? 若是一個靜態常量
? public static final int value = https://www.cnblogs.com/tomakemyself/p/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
從上面這個案例可知,一個類呼叫另一個類的常量不會導致一個類的初始化,
結論二:
- 此處宣告的靜態常量,按照之前的理解是靜態常量被呼叫不會初始化該靜態常量所在的類
- 但是此處當靜態常量的值是一個參考型別的時候,這個時候該靜態常量所在的類就會被初始化
- 故此會先列印我被初始化了,然后在列印a的隨機值
代碼:
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);
}
小結:
1,假如這個類還沒有被加載和鏈接就先進行加載和鏈接,
2,假如類存在直接參考父類,并且這個父類還沒有被初始化,就先初始化父類,
3,如果類中存在初始化陳述句,就依次執行這些初始化陳述句,
命名空間相關結論總結:
1:同一個命名空間下的Class物件相同(hasCode相同),不同命名空間下不同,
2:同一個類加載器加載的類處于一個命名空間,
3:不同的類加載器實體加載的類命名空間不同,
4:每一個類加載器都有自己的命名空間,
5:子類加載器加載的類能見父類加載器加載的類,
6:父類加載器不可見子類類加載加載的類,
至此:jvm藝術類加載器篇就說這么多,如果jvm的藝術三篇文章,各位小伙兒伴都看懂了,并且掌握了,那么恭喜你,至少在面試的時候,考類加載器應該不會丟分,后面的文章還是針對jvm的,將會開啟一個新的篇章,主要針對,jvm的記憶體模型、物件模型、以及jvm的堆疊、調優、垃圾回收等領域進行細致的講解,歡迎各位小伙伴兒持續關注更新,也感謝大家一直以來的支持和關注,筆者會繼續努力,深度學習并且拿出高質量的文章來回饋廣大的讀者,謝謝!!!
更多內容請關注我的公眾號:奇客時間
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/233356.html
標籤:Java
