概敘
科普文:一文搞懂jvm(一)jvm概敘-CSDN博客
????????前面我們介紹了jvm,jvm主要包括兩個子系統和兩個組件: Class loader(類裝載器) 子系統,Execution engine(執行引擎) 子系統;Runtime data area (運行時數據區域)組件, Native interface(本地接口)組件。
? ? ? ? 在這里,我們詳細描述第一個子系統:Class loader(類裝載器) 子系統
jvm作用:
? ? ? ? Java虛擬機就是二進制字節碼的運行環境,負責裝載字節碼到其內部,解釋/編譯為對應平臺上的機器指令執行。每一條Java指令,Java虛擬機規范中都有詳細定義,如怎么取操作數,怎么處理操作數,處理結果放在哪里
jvm特點:
? ? ? ? 跨平臺:一次編譯,到處運行;這是JVM 的主要特征之一,Java 程序在編譯為字節碼后可以在任何支持 JVM 的平臺上運行,擺脫了硬件平臺的束縛,實現了"一次編譯,到處運行"的理想。
? ? ? ? 自動內存管理(內存分配和回收):JVM 提供了自動的內存管理機制,包括內存分配、垃圾回收和內存優化。開發者無需手動分配和釋放內存,JVM 會自動管理對象的生命周期和內存回收,通過垃圾回收器(Garbage Collector)自動回收不再使用的對象,避免了內存泄漏和懸掛指針等問題。
? ? ? ? 即時編譯:JVM 通過即時編譯器將熱點代碼動態編譯成本地機器碼,提高程序的執行性能。編譯器可以根據程序的運行情況進行優化,使得Java應用能隨著運行事件的增長而獲得更高的性能。
java代碼編譯和運行流程
????????主要是兩個階段:編譯和執行。
- 編譯:javac編譯器將源代碼*.java編譯成class文件,即java字節碼文件。
- 執行:jvm虛擬機,即java命令執行class文件時,通過加載器加載class文件,并通過執行引擎中的解釋器翻譯成匯編語言(機器指令+符號表+輔助信息)執行。
JVM的組織架構
????????第一步:我們要將Class文件加載到內存當中,而類加載需要用到類加載子系統Class Loader來進行加載同時對應到我們的內存當中,生成一個大的Class對象并且將必要的靜態屬性進行初始化等等(方法區提現)
????????第二步:當我們真正去執行字節碼指令的時候,就需要執行引擎去發揮作用,按照我們程序的字節碼指令去依次執行(涉及到虛擬機棧里去局部變量表取數據,以及操作入棧),若需要創建對象的話還需要用到堆空間
????????第三步:當程序繼續往下走的時候,還會用到程序計數器,若用到本地的C類庫,還需要用到本地方法棧
? ? ? ? 下面將按照這個詳細圖來講解整個過程。
類加載器
????????類加載器(ClassLoader):JVM會使用類加載器將字節碼文件加載到內存中,并在運行時動態地鏈接和加載類的定義。
類的生命周期
????????類從被加載到虛擬機內存開始,到卸載出內存為止,它的整個生命周期包括以下 7 個階段,其中驗證、準備、解析三個部分統稱為連接。
1.類的加載階段
????????類加載器所做的工作實質是把類文件從硬盤讀取到jvm內存(方法區/元數據區)中,加載完成后,即可生成一個class對象。
- 通過一個類的全限定名獲取定義此類的二進制字節流
- 將這個字節流所代表的靜態存儲結構轉化為方法區(元數據)的運行時數據結構
- 在內存中生成一個代表這個類的java.lang.Class對象,作為方法區(元數據)這個類的各種數據的訪問入口
????????那么對于一些加載.class文件的方式我們可以進行一些舉例說明
- 從本地系統中直接加載
- 通過網絡獲取,典型場景:Web Applet
- 從zip壓縮包中讀取,成為日后jar、war格式的基礎
- 運行時計算生成,使用最多的是:動態代理技術
- 由其他文件生成,典型場景:JSP應用從專有數據庫中提取.class文件,比較少見
- 從加密文件中獲取,典型的防Class文件被反編譯的保護措施
?
2.類的連接階段
2.類的連接階段--驗證
????????這一階段設計的目的是檢測Java字節碼文件是否遵守了《Java虛擬機規范》約束要求。這個階段一般不需要程序員參與。
- 目的在于確保Class文件的字節流中包含信息符合當前虛擬機要求,保證被加載類的正確性,不會危害虛擬機自身安全
- 主要包括四種驗證:文件格式驗證,元數據驗證,字節碼驗證,符號引用驗證。
驗證階段會完成以下校驗:
? ? ? ? 1.文件格式驗證
????????驗證字節流是否符合Class文件格式的規范。例如:是否以0xCAFEBABE開頭、主次版本號是否在當前虛擬機的處理范圍之內、常量池中的常量是否有不被支持的類型 ...... 等等
? ? ? ? 2.元數據驗證
????????對字節碼描述的元數據信息進行語義分析,要符合Java語言規范。例如:是否繼承了不允許被繼承的類(例如final修飾過的)、類中的字段、方法是否和父類產生矛盾 ...... 等等
? ? ? ? 3.字節碼驗證
????????對類的方法體進行校驗分析,確保這些方法在運行時是合法的、符合邏輯的。
? ? ? ? 4.符號引用驗證
????????發生在解析階段,符號引用轉為直接引用的時候,例如:確保符號引用的全限定名能找到對應的類、符號引用中的類、字段、方法允許被當前類所訪問 ...... 等等
驗證階段不是必須的,雖然這個階段非常重要,但是它對程序運行期沒有影響,只影響類加載的時間,也就是說程序的啟動耗時。Java虛擬機允許程序員主動取消這個階段,用來縮短類加載的時間,可以根據自身需求,使用 -Xverify:none參數來關閉大部分的類驗證措施。
3.類的連接階段--準備
????????準備階段為靜態變量(static)分配內存并設置默認值。
為類的靜態變量分配內存,并設為jvm默認的初值;對于非靜態的變量(對象實例化時才分配內存),則不會為它們分配內存。簡單說就是分內存、賦初值。注意:設置初始值為jvm默認初值,而不是程序設定。規則如下
- 基本類型(int、long、short、char、byte、float、double)的默認值為0,boolean默認值false
- 引用類型的默認值為null
- 常量的默認值為我們程序中設定的值,對于final修飾的靜態變量,final static int a = 100,則準備階段中a的初值就是100,而不是0。非靜態的final常量在初始化階段賦值,比如:static int a = 5,則在準備階段初始值就是0而非5。
在JDK8取消永久代后,方法區變成了一個邏輯上的區域,這些類變量的內存實際上是分配在Java堆中的。
- 為類變量(static變量)分配內存并且設置該類變量的默認初始值,即零值
- 這里不包含用final修飾的static,因為final在編譯的時候就會分配好了默認值,準備階段會顯式初始化
????????注意:這里不會為實例變量分配初始化,類變量會分配在方法區中,而實例變量是會隨著對象一起分配到Java堆中。(注意圖片中的堆)jdk1.7之前方法區在堆中分配;jdk1.8之后,方法區移到堆外,用直接內存,即元數據區。
? ? ? ? 模擬元數據區的oom,可以定義final修飾static的大對象字節數組。
4.類的連接階段--解析
????????這一階段的任務就是把Class文件中、常量池中的符號引用轉換為直接引用。主要解析的是 類或接口、字段、類方法、接口方法、方法類型、方法句柄等符號引用。我們可以把解析階段中,符號引用轉換為直接引用的過程,理解為當前加載的這個類,和它所引用的類,正式進行“連接“的過程。解析環節介紹主要以下事情
- 將常量池內的符號引用轉換為直接引用的過程
- 符號引用就是一組符號來描述所引用的目標。符號引用的字面量形式明確定義在《java虛擬機規范》的class文件格式中。直接引用就是直接指向目標的指針、相對偏移量或一個間接定位到目標的句柄
- 解析動作主要針對類或接口、字段、類方法、接口方法、方法類型等。對應常量池中的CONSTANT Class info、CONSTANT Fieldref info、CONSTANT Methodref info等
?
????????解析階段主要是將常量池中的符號引用替換為直接引用。
- 符號引用就是在字節碼文件中使用編號來訪問常量池中的內容。
- 直接引用不在使用編號,而是使用內存中地址進行訪問具體的數據。
5.類的連接階段--初始化
????????當執行完加載階段、鏈接階段到達初始化階段時,就會執行類構造器方法()的過程。
????????初始化階段會執行靜態代碼塊中的代碼,并為靜態變量賦值。
????????此方法不需定義,是javac編譯器自動收集類中的所有類變量的賦值動作和靜態代碼塊中的語句合并而來。
類初始化階段是類加載過程的最后一步。而也是到了該階段,才真正開始執行類中定義的java程序代碼(字節碼),之前的動作都由虛擬機主導。
注意這里一定要注意這是類的初始化,不是對象的初始化哦,對象的初始化也就是創建類實例的時候執行。
jvm對類的加載時機沒有明確規范,但對類的初始化時機有:只有當類被直接引用的時候,才會觸發類的初始化。類被直接引用的情況有以下幾種:
- 通過以下幾種方式:
- new關鍵字創建對象
- 讀取或設置類的靜態變量(注意:在準備階段就已經賦值的變量,讀取時不會觸發初始化)
- 調用類的靜態方法
- 通過反射方式執行1里面的三種方式;
- 初始化子類的時候,會觸發父類的初始化;
- 作為程序入口直接運行時(調用main方法);
- 接口實現類初始化的時候,會觸發直接或間接實現的所有接口的初始化。
關于類的初始化,記住兩句話
1、類的初始化,會自上而下運行靜態代碼塊或靜態賦值語句,非靜態與非賦值的靜態語句均不執行(是在類實例化對象的時候執行)。
2、如果存在父類,則父類先進行初始化,是一個典型的遞歸模型。
區別于對象的初始化(實例化),類的初始化所做的一切都是基于類變量或類語句的(static修飾的),也就是說執行的都是共性的抽象信息。而我們知道,類就是對象實例的抽象。
例題鞏固:
- 例題A
- 例題B
- 例題C
- 例題D
????????類只加載一次:我們可以使用示例代碼來體會一下這個說法
class DeadThread{static{if(true){System.out.println(Thread.currentThread().getName() + "初始化當前類");while(true){}}}
}
public class DeadThreadTest {public static void main(String[] args) {Runnable r = () -> {System.out.println(Thread.currentThread().getName() + "開始");DeadThread dead = new DeadThread();System.out.println(Thread.currentThread().getName() + "結束");};Thread t1 = new Thread(r,"線程1");Thread t2 = new Thread(r,"線程2");t1.start();t2.start();}
}
//運行結果如下:
線程2開始
線程1開始
線程2初始化當前類//程序卡死了...
6.類的連接階段--使用
????????類的使用分為直接引用和間接引用。
????????直接引用與間接引用等判別條件,是看對該類的引用是否會引起類的初始化
????????直接引用已經在類的初始化中的有過闡述,不再贅述。而類的間接引用,主要有下面幾種情況:
- 當引用了一個類的靜態變量,而該靜態變量繼承自父類的話,不引起初始化
- 定義一個類的數組,不會引起該類的初始化;
- 當引用一個類的的常量時,不會引起該類的初始化
7.類的連接階段--卸載
????????當類使用完了之后,類就要進入卸載階段了。可卸載需要具備以下條件:
- 該類所有的實例都已經被回收,也就是java堆中不存在該類的任何實例。
- 加載該類的ClassLoader已經被回收。
- 該類對應的java.lang.Class對象沒有任何地方被引用,無法在任何地方通過反射訪問該類的方法。
????????如果以上三個條件全部滿足,jvm就會在方法區垃圾回收的時候對類進行卸載,類的卸載過程其實就是在方法區中清空類信息,java類的整個生命周期就結束了。
static關鍵字
????????static關鍵字修飾的數據存儲在我們的方法區中的靜態常量池中,static可以修飾方法、變量和代碼塊
????????static修飾方法:指定不需要實例化就可以激活的一個方法。this關鍵字不能在static方法中使用,靜態方法中不能調用非靜態方法,非靜態方法可以調用靜態方法。
????????static修飾變量:指定變量被所有對象共享,即所有實例都可以使用該變量。變量屬于這個類。
????????static修飾代碼塊:通常用于初始化靜態變量,靜態代碼塊屬于類。沒加static的代碼塊認為是構造代碼塊
執行順序
- 實例化對象前,先加載類(對象載入之前,一定要是類先被載入)
- 類(或者可以說靜態變量和靜態代碼塊)在生命周期結束前,只執行一次
- 靜態變量(屬性)和靜態代碼塊誰先聲明誰先執行(同一個類中)
- 非靜態變量(屬性)和非靜態代碼塊誰先聲明誰先執行(同一個類中)
- 靜態構造代碼塊是和類同時加載的,靜態構造代碼塊是在實例化之后執行構造方法之前執行的,構造方法是在構造代碼塊執行完之后才執行的。
- 靜態方法屬于類的,加載完類就可以調用靜態方法(可以執行多次,注意區別于靜態代碼塊,靜態代碼塊只會執行一次);非靜態方法是屬于對象的,加載完對象就可以調用非靜態方法。
- 每創建一個對象,即每載入一個對象,非靜態代碼塊都執行一次。執行類對象的載入之前就會調用
案例一
我們來通過一個例子來驗證以下上面的觀點
public class InitializeDemo {private static int k = 1;private static InitializeDemo t1 = new InitializeDemo("t1");private static InitializeDemo t2 = new InitializeDemo("t2");private static int i = print("i");private static int n = 99;{print("初始化塊");j = 100;}public InitializeDemo(String str) {System.out.println((k++) + ":" + str + " i=" + i + " n=" + n);++i;++n;}static {print("靜態塊");n = 100;}private int j = print("j");public static int print(String str) {System.out.println((k++) + ":" + str + " i=" + i + " n=" + n);++n;return ++i;}public static void main(String[] args) {InitializeDemo test = new InitializeDemo("test");}
}
輸出結果:
1:初始化塊 i=0 n=0 | |
2:j i=1 n=1 | |
3:t1 i=2 n=2 | |
4:初始化塊 i=3 n=3 | |
5:j i=4 n=4 | |
6:t2 i=5 n=5 | |
7:i i=6 n=6 | |
8:靜態塊 i=7 n=99 | |
9:初始化塊 i=8 n=100 | |
10:j i=9 n=101 | |
11:test i=10 n=102 |
我們來逐個分析,
????????一開始調用main方法,main方法內實例化InitializeDemo的對象,在對象載入之前,一定要是類先被載入
????????所以我們先加載InitializeDemo類,加載類的同時,會加載靜態變量和靜態代碼塊,但是是按順序執行,且只執行一次
????????先加載如下靜態變量
private static int k = 1; |
????????加載如下靜態變量的時候,發現要去加載類,由于類已經被加載了,所以會實例化這個對象,這個對象實例化前,會執行非靜態代碼塊和為非靜態屬性賦值,然后再執行構造方法,按在代碼中順序執行。
private static InitializeDemo t1 = new InitializeDemo("t1"); |
????????所以先執行非靜態代碼塊的內容:
{ | |
print("初始化塊"); | |
j = 100; | |
} |
輸出:1:初始化塊 i=0 n=0
初始的時候i的值和n的值默認值是0,執行完這個方法后會變成i=1,n=1
接著為非靜態屬性賦值:
private int j = print("j"); |
輸出:2:j i=1 n=1
輸出時i=1,n=1,執行完這個方法后會變成i=2,n=2
然后執行構造方法
public InitializeDemo(String str) {System.out.println((k++) + ":" + str + " i=" + i + " n=" + n);++i;++n;}
輸出:3:t1 i=2 n=2
輸出時i=2,n=2,執行完這個方法后會變成i=3,n=3
t1的實例化執行結束,接著執行t2的實例化
private static InitializeDemo t2 = new InitializeDemo("t2"); |
結果和上述一致,按非靜態代碼塊和非靜態屬性然后構造方法方法的順序執行
輸出:
4:初始化塊 i=3 n=3
5:j i=4 n=4
6:t2 i=5 n=5
兩個靜態屬性(實例化)執行完,執行如下代碼
private static int i = print("i"); |
輸出:7:i i=6 n=6
這里執行完成后,i=7,n=7
接著執行下面的代碼,此時n變成了99
private static int n = 99; |
注意:執行完這行代碼后,n的值就被賦成99了,i的值還是7。
接著執行靜態代碼塊
static { | |
print("靜態塊"); | |
n = 100; | |
} |
輸出:8:靜態塊 i=7 n=99
輸出時i=7,n=99,執行完這個方法后會變成i=8,n=100
到此類加載完畢,可以看到static變量和靜態代碼塊都按順序執行了,然后開始實例化test對象,參考t1,t2的實例化,按非靜態代碼塊和非靜態屬性然后構造方法方法的順序執行,這里就不會再處理static相關的代碼了。
輸出:
9:初始化塊 i=8 n=100
10:j i=9 n=101
11:test i=10 n=102
案例二
繼承中的static執行順序,看以下例子
public class Test3 extends Base {static {System.out.println("test static");}public Test3() {System.out.println("test constructor");}public static void main(String[] args) {new Test3();}
}class Base {static {System.out.println("Base static");}public Base() {System.out.println("Base constructor");}
}
輸出結果:
Base static | |
test static | |
Base constructor | |
test constructor | |
執行Test3的構造方法,要先加載Test3的類加載,由于Test3繼承于Base,所以他要先加載父類Base,靜態代碼塊先執行。
則會先輸出:Base static
再輸出:test static
再執行子類的構造方法的時候,要先執行父類的構造方法(一般是找默認的構造方法即無參構造方法,除非在子類的構造方法里指定要調用父類的構造方案),如果是多級繼承,會先執行最頂級父類的構造方法,然后依次執行各級子類的構造方法。
所以再輸出:Base constructor
然后輸出:test constructor
結果就如上。
案例三
再舉一個例子
public class MyTest {MyPerson person = new MyPerson("test");//這里可以理解為成員變量輔助,,要先把MyPerson先加載到jvm中static {System.out.println("test static");//1}public MyTest() {System.out.println("test constructor");//5}public static void main(String[] args) {//main方法在MyTest類中,使用mian方法先加載MyTest的靜態方法,不調用其他,MyClass myClass = new MyClass();//對象創建的時候,會加載對應的成員變量}
}class MyPerson {static {System.out.println("person static");//3}public MyPerson(String str) {System.out.println("person " + str);//4 6}
}class MyClass extends MyTest {MyPerson person = new MyPerson("class");//這里可以理解為成員變量輔助,要先把MyPerson先加載到jvm中static {System.out.println("class static");//2}public MyClass() {//默認super()System.out.println("class constructor");//7}
}
輸出:
test static | |
class static | |
person static | |
person test | |
test constructor | |
person class | |
class constructor | |
下面分析執行步驟:
- 先看MyTest類及其靜態的變量,方法和代碼塊會隨類的加載而開辟空間,有一個靜態代碼塊,先執行,所以輸出:test static,且此時MyTest類的其他語句不執行,此時MyTest類加載完成。
- 接著看main方法中調用了MyClass myClass =new MyClass(),實例化了一個MyClass類的對象,這時會先加載MyClass類,而MyClass類繼承于MyTest類,在加載MyClass類前,會先加載MyTest類,但是MyTest類以及其靜態的變量和靜態代碼塊已經加載(在類的生命周期只執行一次),所以返回到子類(MyClass類)的加載,這時候會調用MyClass類的靜態的變量和靜態代碼塊。所以輸出:class static。
- MyClass類加載完后,在執行MyClass類的構造方法前,先初始化對象的成員變量(先初始化父類MyTest的成員變量),所以執行父類MyTest(類已加載過,這里就直接執行成員變量的初始化)的成員變量:MyPerson person = new MyPerson(“test”),于是會加載MyPerson類和其靜態的變量和靜態代碼塊。則先輸出:person static
- 加載完MyPerson類和其靜態的變量和靜態代碼塊后,回到MyClass類開始執行非靜態代碼塊和屬性,由于MyClass繼承了MyTest,所以會先初始化MyTest,初始化MyTest類的屬性:MyPerson person = new MyPerson("test");會調用MyPerson類的有參構造方法,即輸出:person test
- MyTest類的非靜態屬性和非靜態的代碼塊執行完成后,然后接著執行父類構造方法,即輸出:test constructor
- 父類MyTest構造方法執行結束,回到子類MyClass,子類再調用構造方法前,先初始化對象的成員變量MyPerson person = new MyPerson(“class”);,這時候會先先加載MyPerson 和其靜態的變量和靜態代碼塊,由于上述類已經加載,而且MyPerson沒有非靜態屬性及非靜態代碼塊,所以直接執行其有參構造方法,即輸出:person class
- MyClass再無其他非靜態屬性及非靜態代碼塊,執行無參構造方法,即輸出:class constructor
- MyClass實例化完成,回到MyTest類,無后續代碼,至此程序執行完成。
總結
????????使用一個類創建對象的時候,一般都是先加載類,完成類的初始化(靜態變量的賦值和靜態代碼塊的執行,按代碼中的先后順序,整個生命周期中只會執行一次),然后實例化對象,實例化對象時不再處理靜態變量和靜態代碼塊,會處理非靜態屬性和非靜態代碼塊,也是按照代碼中的先后順序執行,最后才調用構造方法。如有繼承關系,則按照此規則先執行父類后執行子類。
類加載順序的三個原則是
- 1、父類優先于子類
- 2、屬性和代碼塊(看先后順序)優先于構造方法
- 3、靜態優先于非靜態
整個程序執行順序:
????????父類靜態變量、父類靜態語句塊(按代碼中的先后順序執行)--> 子類靜態變量、子類靜態語句塊(按代碼中的先后順序執行)--> 父類非靜態變量、父類非靜態語句塊(按代碼中的先后順序執行)--> 父類構造器 --> 子類非靜態變量、子類非靜態語句塊(按代碼中的先后順序執行)--> 子類構造器 --> 完成
????????來一張圖更直觀些。
加載器分類
????????類加載器(ClassLoader)是Java虛擬機提供給應用程序去實現獲取類和接口字節碼數據的技術。
- 啟動類加載器(Bootstrap ClassLoader):用于加載Java核心類庫,通常位于jre包下的類。
- 擴展類加載器:負責加載jre/lib/ext目錄下的JAR包。
- 應用程序類加載器:負責加載應用程序classpath下的類文件。
- 自定義類加載器(User-Defined ClassLoader):用戶可以根據需要創建自己的類加載器,以加載特定位置或方式的類文件。
????????所以將擴展類加載器、系統類加載器也認為是自定義類加載器
啟動類加載器(引導類加載器,Bootstrap ClassLoader)
- 這個類加載使用C/C++語言實現的,嵌套在JVM內部
- 它用來加載Java的核心庫(JAVA_HOME/jre/lib/rt.jar、resources.jar或sun.boot.class.path路徑下的內容),用于提供JVM自身需要的類
- 并不繼承自java.lang.ClassLoader,沒有父加載器
- 加載擴展類和應用程序類加載器,并作為他們的父類加載器
- 出于安全考慮,Bootstrap啟動類加載器只加載包名為java、javax、sun等開頭的類
擴展類加載器(Extension ClassLoader)
- Java語言編寫,由sun.misc.Launcher$ExtClassLoader實現
- 派生于ClassLoader類
- 父類加載器為啟動類加載器
- 從java.ext.dirs系統屬性所指定的目錄中加載類庫,或從JDK的安裝目錄的jre/lib/ext子目錄(擴展目錄)下加載類庫。如果用戶創建的JAR放在此目錄下,也會自動由擴展類加載器加載
應用程序類加載器(也稱為系統類加載器,AppClassLoader)
- Java語言編寫,由sun.misc.LaunchersAppClassLoader實現
- 派生于ClassLoader類
- 父類加載器為擴展類加載器
- 它負責加載環境變量classpath或系統屬性java.class.path指定路徑下的類庫
- 該類加載是程序中默認的類加載器,一般來說,Java應用的類都是由它來完成加載
- 通過classLoader.getSystemclassLoader()方法可以獲取到該類加載器
示例:
public class ClassLoaderTest {public static void main(String[] args) {//獲取系統類加載器ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2//獲取系統類加載器其上層:擴展類加載器ClassLoader extClassLoader = systemClassLoader.getParent();System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader@1540e19d//獲取擴展類加載器其上層:獲取不到引導類加載器ClassLoader bootstrapClassLoader = extClassLoader.getParent();System.out.println(bootstrapClassLoader);//null}
}
這些加載器分別能加載哪些路徑下的文件呢?
public class ClassLoaderTest1 {public static void main(String[] args) {System.out.println("**********啟動類加載器**************");//獲取BootstrapClassLoader能夠加載的api的路徑URL[] urLs = sun.misc.Launcher.getBootstrapClassPath().getURLs();for (URL element : urLs) {System.out.println(element.toExternalForm());}}
}
//運行結果如下:
**********啟動類加載器**************
file: /D:/developer_tools/Java/jdk1.8.0_131/jre/lib/resources.jar
file: /D:/developer_tools/Java/jdk1.8.0_131/jre/lib/rt.jar
file: /D:/developer_tools/Java/jdk1.8.0_131/jre/lib/sunrsasign.jar
file: /D:/developer.tools/Java/jdk1.8.0_131/jre/lib/jsse.jar
file: /D:/developer.tools/Java/jdk1.8.0_131/jre/lib/jce.jar
file: /D:/developer.tools/Java/jdk1.8.0_131/jre/lib/charsets.jar
file: /D:/developer.tools/Java/jdk1.8.0_131/jre/lib/jfr.jar
file: /D:/developer.tools/Java/jdk1.8.0_131/jre/classes
可以打開路徑下的jsee.jar包里的Class文件反查看加載器是什么?
public class ClassLoaderTest1 {public static void main(String[] args) {//file: /D:/developer.tools/Java/jdk1.8.0_131/jre/lib/jsse.jar//從路徑中隨意選擇一個類,來看看他的類加載器是什么:引導類加載器ClassLoader classLoader = Provider.class.getClassLoader();System.out.println(classLoader);//運行結果:null}
}
接下來我們接著看看擴展類的加載器有哪一些
public class ClassLoaderTest1 {public static void main(String[] args) {System.out.println("***********擴展類加載器*************");String extDirs = System.getProperty("java.ext.dirs");for (String path : extDirs.split(";")) {System.out.println(path);}}
}
//運行結果如下:
***********擴展類加載器**** *** ******
D: \developer_tools\Java\jdk1.8.0_131\jre\lib\ext
C: \Windows\Sun\Java\lib\ext
同理我們打開文件路徑通過Class文件反查一下加載器是什么
public class ClassLoaderTest1 {public static void main(String[] args) {//file: D:\developer_tools\Java\jdk1.8.0_131\jre\lib\extClassLoader classLoader1 = CurveDB.class.getClassLoader();System.out.println(classLoader1);}
}
//運行結果如下:
sun.misc.Launcher$ExtClassLoader@1540e19d
用戶自定義加載器
????????在Java的日常應用程序開發中,類的加載幾乎是由上述3種類加載器相互配合執行的,在必要時我們還可以自定義類加載器,來定制類的加載方式。
????????那為什么還需要自定義類加載器?
- 隔離加載類(比如說我假設現在Spring框架,和RocketMQ有包名路徑完全一樣的類,類名也一樣,這個時候類就沖突了。不過一般的主流框架和中間件都會自定義類加載器,實現不同的框架,中間價之間是隔離的)
- 修改類加載的方式
- 擴展加載源(還可以考慮從數據庫中加載類,路由器等等不同的地方)
- 防止源碼泄漏(對字節碼文件進行解密,自己用的時候通過自定義類加載器來對其進行解密)
????????如何自定義類加載器?
- 開發人員可以通過繼承抽象類java.lang.ClassLoader類的方式,實現自己的類加載器,以滿足一些特殊的需求
- 在JDK1.2之前,在自定義類加載器時,總會去繼承ClassLoader類并重寫loadClass()方法,從而實現自定義的類加載類,但是在JDK1.2之后已不再建議用戶去覆蓋loadClass()方法,而是建議把自定義的類加載邏輯寫在findclass()方法中
- 在編寫自定義類加載器時,如果沒有太過于復雜的需求,可以直接繼承URIClassLoader類,這樣就可以避免自己去編寫findclass()方法及其獲取字節碼流的方式,使自定義類加載器編寫更加簡潔。
public?class?CustomClassLoader?extends?ClassLoader?{@Overrideprotected?Class<?>?findClass(String?name)?throws?ClassNotFoundException?{try?{//將路徑下的文件以流的形式存入到內存中byte[]?result?=?getClassFromCustomPath(name);if?(result?==?null)?{throw?new?FileNotFoundException();}?else?{//defineClass和findClass搭配使用return?defineClass(name,?result,?0,?result.length);}}?catch?(FileNotFoundException?e)?{e.printStackTrace();}throw?new?ClassNotFoundException(name);}//自定義流的獲取方式private?byte[]?getClassFromCustomPath(String?name)?{//從自定義路徑中加載指定類:細節略//如果指定路徑的字節碼文件進行了加密,則需要在此方法中進行解密操作。return?null;}
}
關于ClassLoader
????????ClassLoader類,它是一個抽象類,其后所有的類加載器都繼承自ClassLoader(不包括啟動類加載器)
????????以下這些方法都不是抽象方法,可以具體的實現
可以跑一下 體驗一下 classloader
public class ClassLoaderTest2 {public static void main(String[] args) {try {//1.ClassLoader classLoader = Class.forName("java.lang.String").getClassLoader();System.out.println(classLoader);//2.ClassLoader classLoader1 = Thread.currentThread().getContextClassLoader();System.out.println(classLoader1);//3.ClassLoader classLoader2 = ClassLoader.getSystemClassLoader().getParent();System.out.println(classLoader2);} catch (ClassNotFoundException e) {e.printStackTrace();}}
}
//運行結果如下:
null
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@1540e19d
雙親委派
類加載器的雙親委派機制
????????當類加載器收到某個類加載請求時,它首先會將這個加載請求委派給父類加載器去嘗試加載,會一直往上遞歸委派。只有當父加載器無法完成這個類的加載時,才會給對應的子類去加載,一直往下嘗試去加載。
我們在自己的src路徑下創建自己的java.lang.String類
public class String {//static{System.out.println("我是自定義的String類的靜態代碼塊");}
}public class StringTest {public static void main(String[] args) {java.lang.String str = new java.lang.String();System.out.println("hello,atguigu.com");StringTest test = new StringTest();System.out.println(test.getClass().getClassLoader());}
}
//運行結果如下:
hello,atguigu.com
sun.misc.Launcher$AppClassLoader@18b4aac2這時我們在創建一個新的Test類來引用它,并且看看他的加載器是什么?程序并沒有輸出我們靜態代碼塊中的內容,可見仍然加載的是 JDK 自帶的 String 類。
單獨
我們將代碼進行修改一下,再來運行起來看看是怎么樣的輸出結果
package java.lang;
public class String {//static{System.out.println("我是自定義的String類的靜態代碼塊");}//錯誤: 在類 java.lang.String 中找不到 main 方法public static void main(String[] args) {System.out.println("hello,String");}
}
//運行結果如下:
錯誤:在類java.lang.String中找不到main方法,請將main方法定義為:
public static void main (String[] args)
否則JavaFX 應用程序類必須擴展javafx.application.Application由于雙親委派機制一直找父類,所以最后找到了Bootstrap ClassLoader,Bootstrap ClassLoader找到的是 JDK 自帶的 String 類,在那個String類中并沒有 main() 方法,所以就報了上面的錯誤
雙親委派機制流程圖:

雙親委派機制原理
- ????????如果一個類加載器收到了類加載請求,它并不會自己先去加載,而是把這個請求委托給父類的加載器去執行;
- ????????如果父類加載器還存在其父類加載器,則進一步向上委托,依次遞歸,請求最終將到達頂層的啟動類加載器;
- ????????如果父類加載器可以完成類加載任務,就成功返回,倘若父類加載器無法完成此加載任務,子加載器才會嘗試自己去加載,這就是雙親委派模式。
雙親委派主要解決的三個問題:
雙親委派機制優勢
????????接下來我們在創建一個示例來java.lang包下看看是否能運行起來
package java.lang;public class ShkStart {public static void main(String[] args) {System.out.println("hello!");}
}
//運行結果如下:
java.lang.SecurityException: Prohibited package name: java.langat java.lang.ClassLoader.preDefineClass(ClassLoader.java:662)at java.lang.ClassLoader.defineClass(ClassLoader.java:761)at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)at java.net.URLClassLoader.access$100(URLClassLoader.java:73)at java.net.URLClassLoader$1.run(URLClassLoader.java:368)at java.net.URLClassLoader$1.run(URLClassLoader.java:362)at java.security.AccessController.doPrivileged(Native Method)at java.net.URLClassLoader.findClass(URLClassLoader.java:361)at java.lang.ClassLoader.loadClass(ClassLoader.java:424)at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:335)at java.lang.ClassLoader.loadClass(ClassLoader.java:357)at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:495)
Error: A JNI error has occurred, please check your installation and try again
Exception in thread "main"即使類名沒有重復,也禁止使用java.lang這種包名。這是一種保護機制在比如我們使用加載jdbc.jar 用于實現數據庫連接的時候需要用到SPI接口,而SPI接口屬于rt.jar包中Java核心api這個時候我們就要使用雙清委派機制,引導類加載器把rt.jar包加載進來針對具體的第三方實現jar包時使用系統類加載器來加載
????????從這里面就可以看到SPI核心接口由引導類加載器來加載,SPI具體實現類由系統類加載器來加載
????????通過上面的例子,我們可以知道,雙親機制可以
- 避免類的重復加載
- 保護程序安全,防止核心API被隨意篡改
- ????????自定義類:自定義java.lang.String 沒有被加載。
- ????????自定義類:java.lang.ShkStart(報錯:阻止創建 java.lang開頭的類)
沙箱安全機制
????????
????????當我們運行自定義String類main方法的時候出現了報錯,這種其實就是沙箱安全機制,不允許你在程序中破壞核心的源代碼程序
如何判斷兩個class對象是否相同?
????????在JVM中表示兩個class對象是否為同一個類存在兩個必要條件:
- 類的完整類名必須一致,包括包名
- 加載這個類的ClassLoader(指ClassLoader實例對象)必須相同
????????換句話說,在JVM中,即使這兩個類對象(class對象)來源同一個Class文件,被同一個虛擬機所加載,但只要加載它們的ClassLoader實例對象不同,那么這兩個類對象也是不相等的
打破雙親委派機制
????????打破雙親委派的三種方式:
-
????????自定義類加載器
?
-
????????線程上下文類加載器
-
OSGi框架類加載器