JVM专题(2)-类加载器子系统
目前博主個人博客已經搭建發布,后期相關文章也會發布在上面,大家有興趣可以去上面學習,點擊即可前往文青樂園
1.內存結構概述
假設我們想自己手寫一個Java虛擬機的話,必須考慮以下結構:
- 類加載器
- 執行引擎
2.類加載子系統
2.1 類加載器子系統的作用
- 類加載器子系統負責從文件系統或者網絡中加載Class文件,class文件在文件開頭有特定的文件標識。
- ClassLoader只負責class文件的加載,至于它是否可以運行,則由Execution Engine決定。
- 加載的類信息存放于一塊稱為方法區的內存空間。除了類的信息外,方法區中還會存放運行時常量池信息,可能還包括字符串字面量和數字常量(這部分常量信息是Class文件中常量池部分的內存映射)
2.2 類加載器ClassLoader角色
- class file存在于本地硬盤上,可以理解為設計師畫在紙上的模板,而最終這個模板在執行的時候是要加載到JVM當中來根據這個文件實例化出n個一模一樣的實例。
- class file加載到JVM中,被稱為DNA元數據模板,放在方法區。
- 在.class文件 –> JVM –> 最終成為元數據模板,此過程就要一個運輸工具(類裝載器Class Loader),扮演一個快遞員的角色。
2.3 類的加載過程
/*** @author likangmin* @version 1.0* @create 2021/2/24 10:46* @desc*/ public class HelloLoader {public static void main(String[] args) {System.out.println("謝謝ClassLoader加載我....");System.out.println("你的大恩大德,我下輩子再報!");} }以上代碼的加載過程如下:
完整的流程圖如下所示:
2.3.1 加載(Loading)階段
(1)加載
(2)加載.class文件的方式
- 從本地系統中直接加載
- 通過網絡獲取,典型場景:Web Applet
- 從zip壓縮包中讀取,成為日后jar、war格式的基礎
- 運行時計算生成,使用最多的是:動態代理技術
- 由其他文件生成,典型場景:JSP應用從專有數據庫中提取.class文件,比較少見
- 從加密文件中獲取,典型的防Class文件被反編譯的保護措施
2.3.2 鏈接(Linking)階段
鏈接分為三個子階段:驗證 --> 準備 --> 解析
(1)驗證(Verify)
- 目的在于確保Class文件的字節流中包含信息符合當前虛擬機要求,保證被加載類的正確性,不會危害虛擬機自身安全
- 主要包括四種驗證,文件格式驗證,元數據驗證,字節碼驗證,符號引用驗證。
我們可以使用 BinaryViewer(如需要可以在我的上傳的資源中進行下載,以下一些需要用到的工具也是) 查看字節碼文件,其開頭均為 CAFE BABE ,如果出現不合法的字節碼文件,那么將會驗證不通過:
(2)準備(Prepare)
- 為類變量分配內存并且設置該類變量的默認初始值,即零值
- 這里不包含用final修飾的static,因為final在編譯的時候就會分配好了默認值,準備階段會顯式初始化
- 注意:這里不會為實例變量分配初始化,類變量會分配在方法區中,而實例變量是會隨著對象一起分配到Java堆中
例如變量a在準備階段會賦初始值,但不是1,而是0,在初始化階段會被賦值為 1,代碼如下:
/*** @author likangmin* @version 1.0* @create 2021/2/24 10:46* @desc*/ public class HelloApp {private static int a = 1;//prepare:a = 0 ---> initial : a = 1public static void main(String[] args) {System.out.println(a);} }(3)解析(Resolve)
- 將常量池內的符號引用轉換為直接引用的過程
- 事實上,解析操作往往會伴隨著JVM在執行完初始化之后再執行
- 符號引用就是一組符號來描述所引用的目標。符號引用的字面量形式明確定義在《java虛擬機規范》的class文件格式中。直接引用就是直接指向目標的指針、相對偏移量或一個間接定位到目標的句柄
- 解析動作主要針對類或接口、字段、類方法、接口方法、方法類型等。對應常量池中的CONSTANT Class info、CONSTANT Fieldref info、CONSTANT Methodref info等
2.3.3 初始化(Initialization)階段
- 初始化階段就是執行類構造器方法()的過程
- 此方法不需定義,是javac編譯器自動收集類中的所有類變量的賦值動作和靜態代碼塊中的語句合并而來。也就是說,當我們代碼中包含static變量的時候,就會有( )方法;如果當前類不存在static變量,那么它的字節碼文件是不會存在( )
- ()方法中的指令按語句在源文件中出現的順序執行
- ()不同于類的構造器。(關聯:構造器是虛擬機視角下的())
- 若該類具有父類,JVM會保證子類的()執行前,父類的()已經執行完畢
- 虛擬機必須保證一個類的()方法在多線程下被同步加鎖
我們可以不同的工具查看對應的結果,如果我們需要單獨的軟件進行查看,安裝以下軟件即可
不過為了方便,我們可以直接在開發工具中安裝對應的插件,例如在Idea中安裝 JClassLib 插件:
有幾點我們需要注意一下:
a.我們代碼中包含static變量的時候,就會有clinit方法
b.如果當前類不存在static變量,那么它的字節碼文件是不會存在< clinit>( )
c.< clinit>()方法中的指令按語句在源文件中出現的順序執行
靜態變量 number 的值變化過程如下:
d.構造器是虛擬機視角下的()
public class ClinitTest {//任何一個類聲明以后,內部至少存在一個類的構造器private int a = 1;private static int c = 3;public static void main(String[] args) {int b = 2;}public ClinitTest(){a = 10;int d = 20;}}
在構造器中先將類變量 a 賦值為 10,再將局部變量賦值為 20
e.若該類具有父類,JVM會保證子類的()執行前,父類的()已經執行完畢
public class ClinitTest1 {static class Father{public static int A = 1;static{A = 2;}}static class Son extends Father{public static int B = A;}public static void main(String[] args) {//加載Father類,其次加載Son類。System.out.println(Son.B); //2} }以上代碼,加載流程如下:
f.虛擬機必須保證一個類的()方法在多線程下被同步加鎖
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();} }class DeadThread {static {if (true) {System.out.println(Thread.currentThread().getName() + "初始化當前類");while (true) {}}} }
程序卡死,分析原因:
- 兩個線程同時去加載 DeadThread 類,而 DeadThread 類中靜態代碼塊中有一處死循環
- 先加載 DeadThread 類的線程搶到了同步鎖,然后在類的靜態代碼塊中執行死循環,而另一個線程在等待同步鎖的釋放
- 所以無論哪個線程先執行 DeadThread 類的加載,另外一個類也不會繼續執行
3.類加載器的分類
- JVM支持兩種類型的類加載器 。分別為引導類加載器(Bootstrap ClassLoader)和自定義類加載器(User-Defined ClassLoader)
- 從概念上來講,自定義類加載器一般指的是程序中由開發人員自定義的一類類加載器,但是Java虛擬機規范卻沒有這么定義,而是將所有派生于抽象類ClassLoader的類加載器都劃分為自定義類加載器
無論類加載器的類型如何劃分,在程序中我們最常見的類加載器始終只有3個,如下所示
為什么 ExtClassLoader 和 AppClassLoader 都屬于自定義加載器?規范定義:所有派生于抽象類ClassLoader的類加載器都劃分為自定義類加載器。
ExtClassLoader 繼承樹:
AppClassLoader 繼承樹:
- 我們嘗試獲取引導類加載器,獲取到的值為 null ,這并不代表引導類加載器不存在,因為引導類加載器是由 C/C++ 語言構成的,所以我們是獲取不到
- 兩次獲取系統類加載器的值都相同:sun.misc.Launcher$AppClassLoader@18b4aac2 ,這說明系統類加載器是全局唯一的
3.1 虛擬機自帶的加載器
3.1.1 啟動類加載器(引導類加載器,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等開頭的類
3.1.2 擴展類加載器(Extension ClassLoader)
- Java語言編寫,由sun.misc.Launcher$ExtClassLoader實現
- 派生于ClassLoader類
- 父類加載器為啟動類加載器
- 從java.ext.dirs系統屬性所指定的目錄中加載類庫,或從JDK的安裝目錄的 jre / lib / ext子目錄(擴展目錄)下加載類庫。如果用戶創建的 JAR 放在此目錄下,也會自動由擴展類加載器加載
3.1.3 系統類加載器(應用程序類加載器,AppClassLoader)
- Java語言編寫,由sun.misc.LaunchersAppClassLoader實現
- 派生于ClassLoader類
- 父類加載器為擴展類加載器
- 它負責加載環境變量 classpath 或 系統屬性java.class.path指定路徑下的類庫
- 該類加載是程序中默認的類加載器,一般來說,Java應用的類都是由它來完成加載的
- 通過classLoader.getSystemclassLoader( )方法可以獲取到該類加載器
代碼說明:
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());}//從上面的路徑中隨意選擇一個類,來看看他的類加載器是什么:引導類加載器ClassLoader classLoader = Provider.class.getClassLoader();System.out.println(classLoader);System.out.println("***********擴展類加載器*************");String extDirs = System.getProperty("java.ext.dirs");for (String path : extDirs.split(";")) {System.out.println(path);}//從上面的路徑中隨意選擇一個類,來看看他的類加載器是什么:擴展類加載器ClassLoader classLoader1 = CurveDB.class.getClassLoader();System.out.println(classLoader1);//sun.misc.Launcher$ExtClassLoader@1540e19d} }執行結果:
**********啟動類加載器************** file:/C:/Program%20Files/Java/jdk1.8.0_121/jre/lib/resources.jar file:/C:/Program%20Files/Java/jdk1.8.0_121/jre/lib/rt.jar file:/C:/Program%20Files/Java/jdk1.8.0_121/jre/lib/sunrsasign.jar file:/C:/Program%20Files/Java/jdk1.8.0_121/jre/lib/jsse.jar file:/C:/Program%20Files/Java/jdk1.8.0_121/jre/lib/jce.jar file:/C:/Program%20Files/Java/jdk1.8.0_121/jre/lib/charsets.jar file:/C:/Program%20Files/Java/jdk1.8.0_121/jre/lib/jfr.jar file:/C:/Program%20Files/Java/jdk1.8.0_121/jre/classes null ***********擴展類加載器************* C:\Program Files\Java\jdk1.8.0_121\jre\lib\ext C:\Windows\Sun\Java\lib\ext sun.misc.Launcher$ExtClassLoader@29453f44加載器的執行流程圖如下:
3.2 用戶自定義類加載器
在Java的日常應用程序開發中,類的加載幾乎是由上述3種類加載器相互配合執行的,在必要時,我們還可以自定義類加載器,來定制類的加載方式。那為什么還需要自定義類加載器?
- 隔離加載類
- 修改類加載的方式
- 擴展加載源
- 防止源碼泄露
那么如何自定義類加載器呢?
- 開發人員可以通過繼承抽象類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{return defineClass(name,result,0,result.length);}} catch (FileNotFoundException e) {e.printStackTrace();}throw new ClassNotFoundException(name);}private byte[] getClassFromCustomPath(String name){//從自定義路徑中加載指定類:細節略//如果指定路徑的字節碼文件進行了加密,則需要在此方法中進行解密操作。return null;}public static void main(String[] args) {CustomClassLoader customClassLoader = new CustomClassLoader();try {Class<?> clazz = Class.forName("One",true,customClassLoader);Object obj = clazz.newInstance();System.out.println(obj.getClass().getClassLoader());} catch (Exception e) {e.printStackTrace();}} }3.3 關于 ClassLoader
ClassLoader類,它是一個抽象類,其后所有的類加載器都繼承自ClassLoader(不包括啟動類加載器)
sun.misc.Launcher 它是一個java虛擬機的入口應用
3.3.1 獲取 ClassLoader 的途徑
public class ClassLoaderTest2 {public static void main(String[] args) {try {//1.Class.forName().getClassLoader()ClassLoader classLoader = Class.forName("java.lang.String").getClassLoader();System.out.println(classLoader); // String 類由啟動類加載器加載,我們無法獲取//2.Thread.currentThread().getContextClassLoader()ClassLoader classLoader1 = Thread.currentThread().getContextClassLoader();System.out.println(classLoader1); //sun.misc.Launcher$AppClassLoader@18b4aac2//3.ClassLoader.getSystemClassLoader().getParent()ClassLoader classLoader2 = ClassLoader.getSystemClassLoader().getParent();System.out.println(classLoader2); //sun.misc.Launcher$ExtClassLoader@61bbe9ba} catch (ClassNotFoundException e) {e.printStackTrace();}} }4.雙親委派機制(重點,面試常問)
4.1 雙親委派機制原理
Java虛擬機對 class 文件采用的是按需加載的方式,也就是說當需要使用該類時才會將它的 class 文件加載到內存中生成 class 對象。而且加載某個類的class文件時,Java虛擬機采用的是雙親委派模式,即把請求交由父類處理,它是一種任務委派模式
- 如果一個類加載器收到了類加載請求,它并不會自己先去加載,而是把這個請求委托給父類的加載器去執行;
- 如果父類加載器還存在其父類加載器,則進一步向上委托,依次遞歸,請求最終將到達頂層的啟動類加載器;
- 如果父類加載器可以完成類加載任務,就成功返回,倘若父類加載器無法完成此加載任務,子加載器才會嘗試自己去加載,這就是雙親委派模式。
- 父類加載器一層一層往下分配任務,如果子類加載器能加載,則加載此類,如果將加載任務分配至系統類加載器也無法加載此類,則拋出異常
4.2 雙親委派機制代碼示例
(1)我們自己定義一個java.lang包,在其下面定義一個String類,里面聲明了靜態代碼塊
在一個測試類中加載String類,看看加載的String類是JDK自帶的,還是我們自己編寫的:
public class StringTest {public static void main(String[] args) {java.lang.String str = new java.lang.String();System.out.println("hello,hello");} }結果:程序并沒有輸出我們靜態代碼塊中的內容,可見仍然加載的是 JDK 自帶的 String 類
(2)在我們自己定義的 String 類中整個 main( ) 方法
運行main方法以后報錯:
之所以報錯是由于雙親委派機制,我們的String類是由引導類加載器加載的,而引導類加載器并沒有main方法,所以會報錯
(3)SPI接口是由引導類加載器加載的,接口具體的實現類是由線程上下文類加載器加載的,而線程上下文類加載器就是系統類加載器,所以我們在加載的時候,會先進行雙親委派,在引導類加載器加載SPI核心類,然后加載SPI接口,最后在反向委托,通過系統類加載器進行實現類 jdbc.jar 的加載
(4)java.lang 包下自定義類
運行后報錯:
出于保護機制,java.lang 包下不允許我們自定義類,所以報錯。
4.3 雙親委派機制的好處
- 避免類的重復加載
- 保護程序安全,防止核心API被隨意篡改
- 自定義類:java.lang.String 沒有調用
- 自定義類:java.lang.ShkStart(報錯:阻止創建 java.lang開頭的類)
5.沙箱安全機制
- 自定義String類時:在加載自定義String類的時候會率先使用引導類加載器加載,而引導類加載器在加載的過程中會先加載jdk自帶的文件(rt.jar包中java.lang.String.class),報錯信息說沒有main方法,就是因為加載的是rt.jar包中的String類。
- 這樣可以保證對java核心源代碼的保護,這就是沙箱安全機制。
6.其他
(1)如何判斷兩個class對象是否相同?
- 在JVM中表示兩個class對象是否為同一個類存在兩個必要條件:
- 類的完整類名必須一致,包括包名
- 加載這個類的 ClassLoader(指ClassLoader實例對象)必須相同
- 換句話說,在JVM中,即使這兩個類對象(class對象)來源同一個Class文件,被同一個虛擬機所加載,但只要加載它們的 ClassLoader 實例對象不同,那么這兩個類對象也是不相等的
(2)對類加載器的引用 - JVM必須知道一個類型是由啟動加載器加載的還是由用戶類加載器加載的
- 如果一個類型是由用戶類加載器加載的,那么JVM會將這個類加載器的一個引用作為類型信息的一部分保存在方法區中
- 當解析一個類型到另一個類型的引用的時候,JVM需要保證這兩個類型的類加載器是相同的
(3)類的主動使用和被動使用
-
主動使用,又分為七種情況:
- 創建類的實例
- 訪問某個類或接口的靜態變量,或者對該靜態變量賦值
- 調用類的靜態方法
- 反射( 比如:Class.forName(“cn.sxt.Test”) )
- 初始化一個類的子類
- Java虛擬機啟動時被標明為啟動類的類
- JDK7開始提供的動態語言支持:java.lang.invoke.MethodHandle實例的解析結果REF_getStatic、REF putStatic、REF_invokeStatic句柄對應的類沒有初始化,則初始化
-
除了以上七種情況,其他使用Java類的方式都被看作是對類的被動使用,都不會導致類的初始化,即不會執行初始化階段(不會調用 clinit( ) 方法和 init( ) 方法)
總結
以上是生活随笔為你收集整理的JVM专题(2)-类加载器子系统的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: ZooKeeper概述与原理
- 下一篇: 如何设计LRU Cache算法