日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

JVM — 类加载机制

發布時間:2025/4/16 编程问答 39 豆豆
生活随笔 收集整理的這篇文章主要介紹了 JVM — 类加载机制 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

1. 引言

  java 類被虛擬機編譯之后成為一個 Class 的字節碼文件,該字節碼文件中包含各種描述信息,最終都需要加載到虛擬機中之后才能運行和使用。那么虛擬機是如何加載這些 Class 文件?Class 文件中的信息進入虛擬機之后會發生什么變化?接下來我們一個一個探討。

2. 類加載的時機

  類的整個生命周期包括:加載、驗證、準備、解析、初始化、使用和卸載七個階段,其中驗證、準備、解析 3 個部分統稱為連接。

  在上圖中,加載、驗證、準備、初始化和卸載這 5 個階段的順序是確定的,類的加載過程必須按照這個過程按部就班的開始,中間可以再插入另一個類的加載過程。那么,什么情況下需要開始類加載過程的第一個階段呢?虛擬機規范嚴格規定了有且只有?5 種情況必須立即對類進行「初始化」。

  • 遇到 new、getstatic、putstatic 或 invokestatic 這 4 條字節碼指令時,如果類沒有初始化,則需要先觸發其初始化。生成這 4 條指令的最常見的 java 代碼場景是:使用 new 關鍵字實例化對象的時候、讀取或設置一個類的靜態字段(被 final、已在編譯器把結果放入常量池的靜態字段除外)的時候,以及調用一個類的靜態方法時。
  • 使用 java.lang.reflec 包的方法對類進行反射調用的時候,如果類沒有進行過初始化,則需要先觸發其初始化。
  • 當初始化一個類的時候,如果發現父類沒有初始化過,則需要先觸發其父類的初始化
  • 當虛擬機啟動時,用戶需要指定一個執行的類(包含 main 方法的那個類),虛擬機會先初始化這個主類
  • 當使用 JDK1.7 的動態語言支持時,如果一個 java.lang.invoke.MethodHandle 實例后的解析結果 REF_getStatic、REF_pubStatic、REF_invokeStatic 的方法句柄,并且這個方法句柄所對應的類沒有進行過初始化,則需要先初始化這個類。(這一點還不理解是什么)

3. 類加載過程

  了解了類是什么時候開始加載之后,我們來了解一下類加載的全過程。也就是加載、驗證、準備、解析和初始化這個 5 個階段的具體動作。

 3.1 加載

 注?:「加載」是「類加載」過程的一個階段,在加載階段,虛擬機需要完成下面 3 件事情:

  • 通過一個類的全限定名來獲取定義此類的二進制字節流,其中,類的全限定名可 多個以從途徑獲得,例如 ZIP 包、網絡、動態代理等等。
  • 將這個字節流所代表的的靜態存儲結構轉化為方法區的運行時數據結構
  • 在內存中生成一個代表這個類的 java.lang.Class 對象,作為方法區這個類的各種數據的訪問入口。
  •  3.2 驗證

      在加載階段中,Class 文件并不一定要求用 java 源碼編譯而來,可以使用任何途徑產生,甚至包括用十六進制編輯器直接編寫來產生 Class 文件。因此,為了保證虛擬機的安全,驗證階段是非常有必要的,驗證階段的工作量在虛擬機的類加載子系統中占了相當大的一部分。其大致會完成下面 4 個階段的檢驗動作。

    • 文件格式驗證
    • 元數據驗證,即文件描述信息是否符合 java 的語法規則,主要驗證類的數據類型是否正確,例如這個類是否有父類,該類的父類是否繼承了不允許被繼承的類等等。
    • 字節碼驗證,這個驗證過程主要是針對類的方法體,保證被檢驗的類方法不會做出危害虛擬機的事。
    • 符號引用驗證,判斷該類中引用的類信息能否訪問,或者有權訪問(更具 private、protected 修飾符訪問)。

     3.3 準備

      準備階段是正式為?類變量?分配內存并設置類變量初始化的階段,這些變量所使用的內存都將在方法區中進行分配。

      這里需要注意兩點,首先,這個階段初始化的變量是類變量,即 static 修飾的變量,不包括實例變量。實例變量將會在對象初始化時隨對象一起分配到 java 堆中。其次,這里的初始化「通常情況」下是數據類型的?零值,例如一個變量?public static int value = 123,其先初始化為 0,等到這個類首次被初始化之后才變為 123。

      上面說了通常情況下是那樣,當然也存在一些「不通常的情況」,例如public static final int value = 123。final 修飾的變量在此階段就會生產對應的值。

     3.4 解析

     解析階段是虛擬機將常量池內的符號引用替換為直接引用的過程。其完成的任務是驗證階段的符號引用驗證。主要由下面 4 種解析過程

    • 類或接口解析
    • 字段解析
    • 類方法解析
    • 接口方法解析

     3.5 初始化

      在準備階段,主要是對類變量進行賦值(一般類型賦為 0,boolean 賦為 false 等等),而初始化階段是初始化類變量和其他資源,這是執行?類構造器<clinit>()?方法的過程。下面介紹一些可能會影響到程序運行行為的特點和細節:

    • <clinit>() 方法收集類變量的賦值動作和執行靜態語句塊的語句。靜態語句塊只能為定義在語句塊后面的變量賦值,但是不能訪問定義在語句塊后面的變量。例如
    public class Test{static{i = 0; // 給變量賦值可以正常編譯通過System.out.println(i); // 這句編譯器會提示「非法向前引用」}static int i = 1; }
    • <clinit>() 方法與類的構造函數不同,它不需要顯示地調用父類構造函數,虛擬機保證子類的 <clinit>() 方法執行之前,父類的 <clinit>() 已經執行完畢。因此,第一個在虛擬機中被執行 <clinit>() 方法的類一定是?java.lang.Object。
    • 由于父類的 <clinit>() 方法先執行,因此父類定義的靜態語句塊要先于子類的靜態語句塊。
    • 虛擬機會保證一個類的<clinit>() 方法在多線程的環境中被正確的加鎖、同步,如果多個線程同時初始化一個類,剛好這個類的<clinit>() 方法耗時很長的操作,就可能造成多個進程的阻塞。例如
    static class DeadLoadClass{static{if(true){System.out.println(Thread.currentThread()+"init DeadLoopClass");while(true){}}} }public static void main(String[] args){Runnable srcipt = new Runnable(){public void run(){System.out.println(Thread.currentThread()+"start");DeadLoadClass dlc = DeadLoadClass();System.out.println(Thread.currentThread()+"run over");}};Thread t1 = new Thead(script);Thread t2 = new Thead(script);t1.start();t2.start(); }

    運行結果如下,即一個線程在死循環中長時間操作,另一個線程發生阻塞,一直等待。

    Thread[Thread-0,5,main]start Thread[Thread-1,5,main]start Thread[Thread-0,5,main]init DeadLoopClass

    4. 類加載器

      虛擬機設計團隊把類加載階段中的「通過一個類的全限定名來獲取描述此類的二進制字節流」這個動作放在了 java 虛擬機外部去實現,以便讓應用程序自己決定如何去獲取需要的類。實現這個動作的代碼模塊稱為「類加載器」。

     4.1 類與類加載器

      類加載器在 java 程序中起到的作用遠遠不限于類的加載階段。在運行階段,比較兩個類是否「相等」只有在這兩個類來源于用一個 Class 文件,被同一個虛擬機加載,并且使同一個類加載器加載,這兩個類才會相等。

      這里所指的「相等」,包括代表類的 Class 對象的?equals?方法、isAssignableFrom方法、isInstance?方法的返回結果,也包括使用?instanceof關鍵字做對象所屬關系判定等情況。

     4.2 雙親委派模型

     絕大部分 java 程序都會使用到以下 3 種系統提供的類加載器

    • 啟動類加載器:這個類加載器負責將存放在?<JAVA_HOME>\lib?目錄中的類庫加載到虛擬機內存中。
    • 擴展類加載器:這個加載器由?sun.misc.Launcher$ExtClassLoader?實現,它負責加載?<JAVA_HOME>\lib\ext?目錄中,或被 java.ext.dirs 系統變量所指定的所有類庫。
    • 應用程序類加載器:這個類庫加載器由?sun.misc.Launcher$AppClassLoader?實現,它負責加載用戶路徑上(ClassPath)所指定的類庫。如果應用程序中沒有自定的類加載器,一般情況下這個就是默認的類加載器。

     我們的應用程序都是由這 3 種類加載器相互配合進行加載的,如果有必要,還可以自定義類加載器。這些類加載器之間的關系如下圖所示,這種層次關系被稱為類加載器的雙親委派模型。

      雙親委派模型的工作過程是:如果一個類加載器收到了類加載的要求,它首先自己不會去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此,因此,所有的加載請求最終都應該傳送到頂層的啟動類加載器中,只有當父加載器反饋自己無法加載這個請求(它的搜索范圍中沒有找到所需的類)時,子加載器才會嘗試自己去加載。

      這種模型的一個好處就是由于類加載器有一種層次關系,導致類也有一種層次關系,從而有了優先級。比如類java.lang.Object,它存放在rt.jar中,無論哪個類加載器要加載這個類,最終都要委派給啟動類加載器去加載,因此Object類在各個類加載器環境中都是同一個類。相反,如果沒有使用雙親委派模型,由各個類加載器去自行加載,如果用戶自己編寫了一個稱為java.lang.Object的類,并放在程序的ClassPath中,那系統將會有多個不同的Object類,java類型體系中最基礎的行為也就沒有辦法保證了。

    ?

    自定義類加載器

     為什么需要自定義類加載器? 

      網上的大部分自定義類加載器文章,幾乎都是貼一段實現代碼,然后分析一兩句自定義ClassLoader的原理。但是我覺得首先得把為什么需要自定義加載器這個問題搞清楚,因為如果不明白它的作用的情況下,還要去學習它顯然是很讓人困惑的。

     首先介紹自定義類的應用場景

      (1)加密:Java代碼可以輕易的被反編譯,如果你需要把自己的代碼進行加密以防止反編譯,可以先將編譯后的代碼用某種加密算法加密,類加密后就不能再用Java的ClassLoader去加載類了,這時就需要自定義ClassLoader在加載類的時候先解密類,然后再加載。?

      (2)從非標準的來源加載代碼:如果你的字節碼是放在數據庫、甚至是在云端,就可以自定義類加載器,從指定的來源加載類。?

      (3)以上兩種情況在實際中的綜合運用:比如你的應用需要通過網絡來傳輸 Java 類的字節碼,為了安全性,這些字節碼經過了加密處理。這個時候你就需要自定義類加載器來從某個網絡地址上讀取加密后的字節代碼,接著進行解密和驗證,最后定義出在Java虛擬機中運行的類。

     1. 雙親委派模型

      在實現自己的ClassLoader之前,我們先了解一下系統是如何加載類的,那么就不得不介紹雙親委派模型的實現過程。

    //雙親委派模型的工作過程源碼 protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{// First, check if the class has already been loadedClass c = findLoadedClass(name);if (c == null) {try {if (parent != null) {c = parent.loadClass(name, false);} else {c = findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {// ClassNotFoundException thrown if class not found// from the non-null parent class loader//父類加載器無法完成類加載請求 }if (c == null) {// If still not found, then invoke findClass in order to find the class//子加載器進行類加載 c = findClass(name);}}if (resolve) {//判斷是否需要鏈接過程,參數傳入 resolveClass(c);}return c; }

     2.?雙親委派模型的工作過程如下

      (1)當前類加載器從自己已經加載的類中查詢是否此類已經加載,如果已經加載則直接返回原來已經加載的類。

      (2)如果沒有找到,就去委托父類加載器去加載(如代碼c = parent.loadClass(name,?false)所示)。父類加載器也會采用同樣的策略,查看自己已經加載過的類中是否包含這個類,有就返回,沒有就委托父類的父類去加載,一直到啟動類加載器。因為如果父加載器為空了,就代表使用啟動類加載器作為父加載器去加載。

      (3)如果啟動類加載器加載失敗(例如在$JAVA_HOME/jre/lib里未查找到該class),則會拋出一個異常ClassNotFoundException,然后再調用當前加載器的findClass()方法進行加載。?

    ?

     3.?雙親委派模型的好處:

      (1)主要是為了安全性,避免用戶自己編寫的類動態替換?Java的一些核心類,比如?String。

      (2)同時也避免了類的重復加載,因為?JVM中區分不同類,不僅僅是根據類名,相同的?class文件被不同的?ClassLoader加載就是不同的兩個類。?

    2. 自定義類加載器

      (1)從上面源碼看出,調用loadClass時會先根據委派模型在父加載器中加載,如果加載失敗,則會調用當前加載器的findClass來完成加載。

      (2)因此我們自定義的類加載器只需要繼承ClassLoader,并覆蓋findClass方法,下面是一個實際例子,在該例中我們用自定義的類加載器去加載我們事先準備好的class文件。

    ?

     2.1?自定義一個People.java類做例子

    public class People { //該類寫在記事本里,在用javac命令行編譯成class文件,放在d盤根目錄下private String name;public People() {}public People(String name) {this.name = name;}public String getName() {return name;}public void setName(String name) {this.name = name;}public String toString() {return "I am a people, my name is " + name;}}

     2.2?自定義類加載器

      自定義一個類加載器,需要繼承ClassLoader類,并實現findClass方法。其中defineClass方法可以把二進制流字節組成的文件轉換為一個java.lang.Class(只要二進制字節流的內容符合Class文件規范)。

    import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.nio.ByteBuffer; import java.nio.channels.Channels; import java.nio.channels.FileChannel; import java.nio.channels.WritableByteChannel;public class MyClassLoader extends ClassLoader{public MyClassLoader() {super(null);}public MyClassLoader(ClassLoader parent) {super(parent);}@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {File file = new File("D:/People.class");try {byte[] bytes = getClassBytes(file);//defineClass方法可以把二進制流字節組成的文件轉換為一個java.lang.ClassClass<?> c = this.defineClass(name, bytes, 0, bytes.length);return c;} catch (ClassFormatError e) {e.printStackTrace();} catch (Exception e) {e.printStackTrace();}return super.findClass(name);}private byte[] getClassBytes(File file) throws Exception{// 這里要讀入.class的字節,因此要使用字節流FileInputStream fis = new FileInputStream(file);FileChannel fc = fis.getChannel();ByteArrayOutputStream baos = new ByteArrayOutputStream();WritableByteChannel wbc = Channels.newChannel(baos);ByteBuffer by = ByteBuffer.allocate(1024);while (true){int i = fc.read(by);if (i == 0 || i == -1)break;by.flip();wbc.write(by);by.clear();}fis.close();return baos.toByteArray();}}

     2.3?在主函數里使用

    MyClassLoader mcl = new MyClassLoader(); Class<?> clazz = Class.forName("com.gdut.classLoader1.People", true, mcl); Object obj = clazz.newInstance();System.out.println(obj); System.out.println(obj.getClass().getClassLoader());//打印出我們的自定義類加載器

     2.4?運行結果

    ?

    資料:??https://www.liangzl.com/get-article-detail-13809.html

       ? https://blog.csdn.net/SEU_Calvin/article/details/52315125

    ?

    轉載于:https://www.cnblogs.com/myseries/p/10913456.html

    總結

    以上是生活随笔為你收集整理的JVM — 类加载机制的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。