1. 程式人生 > >Day18 (一)類的加載器

Day18 (一)類的加載器

例如 語言 8.0 檢查 訪問 ger 多個 main 時機

  一個運行時的Java虛擬機(JVM)負責運行一個Java程序。

  當啟動一個Java程序時,一個虛擬機實例誕生;當程序關閉退出,這個虛擬機實例也就隨之消亡。

  如果在同一臺計算機上同時運行多個Java程序,將得到多個Java虛擬機實例,每個Java程序都運行於它自己的Java虛擬機實例中。

  在如下幾種情況下,Java虛擬機將結束生命周期:

  1.執行了System.exit()方法

  2.程序正常執行結束

  3.程序在執行過程中遇到了異常或錯誤而異常終止

  4.由於操作系統出現錯誤而導致Java虛擬機進程終止

類的生命周期

技術分享圖片

類的加載

xx.class的字節碼文件,把字節碼文件加載到方法區中

類的加載指的是將類的.class文件中的二進制數據讀入到內存中,將其放在運行時數據區的方法區內,然後在堆區創建一個java.lang.Class對象,用來封裝類在方法區內的數據結構。

加載.class文件的方式

  1.從本地系統中直接加載

  2.通過網絡下載.class文件

  3.從zip,jar等歸檔文件中加載.class文件

  4.從專有數據庫中提取.class文件

  5.將Java源文件動態編譯為.class文件

  類的加載的最終產品是位於堆區中的Class對象。

  Class對象封裝了類在方法區內的數據結構,並且向Java程序員提供了訪問方法區內的數據結構的接口。

技術分享圖片

連接(驗證、準備、解析)

連接就是將已經讀入到內存的類的二進制數據合並到虛擬機的運行時環境中去。

驗證:驗證字節碼文件的準確性。

類的驗證內容:

  1.類文件的結構檢查

  確保類文件遵從Java類文件的固定格式。

  2.語義檢查

  確保類本身符合Java語言的語法規定,比如驗證final類型的類沒有子類,以及final類型的方法沒有被覆蓋。

  註意,語義檢查的錯誤在編譯器編譯階段就會通不過,但是如果有程序員通過非編譯的手段生成了類文件,其中有可能會含有語義錯誤,此時的語義檢查主要是防止這種沒有編譯而生成的class文件引入的錯誤。

  3.字節碼驗證

  確保字節碼流可以被Java虛擬機安全地執行。

  字節碼流代表Java方法(包括靜態方法和實例方法),它是由被稱作操作碼的單字節指令組成的序列,每一個操作碼後都跟著一個或多個操作數。

  字節碼驗證步驟會檢查每個操作碼是否合法,即是否有著合法的操作數。

  4.二級制兼容性的驗證

  確保相互引用的類之間的協調一致。

  例如,在Worker類的gotoWork()方法中會調用Car類的run()方法,Java虛擬機在驗證Worker類時,會檢查在方法區內是否存在Car類的run()方法,假如不存在(當Worker類和Car類的版本不兼容就會出現這種問題),就會拋出NoSuchMethodError錯誤。

準備:給類變量(靜態變量)分配空間並且進行默認初始化。

在準備階段,Java虛擬機為類的靜態變量分配內存,並設置默認的初始值。

  例如對於以下Sample類,在準備階段,將為int類型的靜態變量a分配4個字節的內存空間,並且賦予默認值0,為long類型的靜態變量b分配8個字節的內存空間,並且賦予默認值0

public class Sample {

    private static int a = 1;
    private static long b;

    static {
        b = 2;
    }
}

解析:把字節碼中的一些符號轉換成指針

在解析階段,Java虛擬機會把類的二級制數據中的符號引用替換為直接引用。

  例如在Worker類的gotoWork()方法中會引用Car類的run()方法。

public void gotoWork() {

             car.run();// 這段代碼在Worker類的二進制數據中表示為符號引用

      }

  在Worker類的二進制數據中,包含了一個對Car類的run()方法的符號引用,它由run()方法的全名和相關描述符組成。

  在解析階段,Java虛擬機會把這個符號引用替換為一個指針,該指針指向Car類的run()方法在方法區內的內存位置,這個指針就是直接引用

初始化

類變量(靜態變量)進行聲明處或靜態塊處初始化。

類的初始化步驟

  1.假如這個類還沒有被加載和連接,那就先進行加載和連接

  2.假如類存在直接的父類,並且這個父類還沒有被初始化,那就先初始化直接的父類

  3.假如類中存在初始化語句,那就依次執行這些初始化語句。

類的初始化時機

Java程序對類的使用方式可以分為兩種:

  1.主動使用

  2.被動使用

  所有的Java虛擬機實現必須在每個類或接口被Java程序首次主動使用才初始化它們。

主動使用的六種情況:

  1.創建類的實例。

  2.訪問某個類或接口的靜態變量,或者對該靜態變量賦值。

  3.調用類的靜態方法

  4.當使用反射方法強制創建某個類或接口的對象時

  5.當初始化某個子類時,該子類的所有父類都會被初始化

  6.當虛擬機Java命令運行啟動類

除了以上六種情況,其他使用Java類的方式都被看作是對類的被動使用,都不會導致類的初始化

使用

調用類中的方法

卸載

類的加載器

根類加載器

Bootstrap、ClassLoader

負責加載核心類庫 lib下(C:\Program Files\Java\jdk1.8.0_151\jre\lib),是用原生代碼來實現的(C實現的),並不繼承自java.lang.ClassLoader。

加載擴展類和應用程序類加載器,並指定它們的父類加載器。

擴展類加載器

Extension ClassLoader,父類是根類加載器

用來加載java的擴展庫(JAVA_HOME/jre/lib/ext/*.jar,或java.ext.dirs路徑下的內容)java虛擬機的實現會提供一個擴展庫目錄。該類加載器在此目錄裏面查找並加載java類。

由sun.miscLauncher$ExtClassLoader實現,繼承自java.lang.ClassLoader

系統類加載器

System ClassLoader,父類是擴展類加載器

負責加載自己編寫的類 classpath下

自定義類加載器

負責加載非classpath下的自己編寫的類,父類是系統類加載器

public static void main(String[] args) {
        //AppClassLoader    系統類加載器
        System.out.println(TestLoader1.class.getClassLoader());
        //ExtClassLoader    擴張類加載器
        System.out.println(TestLoader1.class.getClassLoader().getParent());
        //null根類加載器無法查看
        System.out.println(TestLoader1.class.getClassLoader().getParent().getParent());
}

類加載的父委托機制

技術分享圖片

  loader2首先從自己的命名空間中查找Sample類是否已經被加載,如果已經加載,就直接返回代表Sample類的Class對象的引用。

  如果Sample類還沒有被加載,loader2首先請求loader1代為加載,loader1再請求系統類加載器代為加載,系統類加載器再請求擴展類加載器代為加載,擴展類加載器再請求根類加載器代為加載。

  若根類加載器和擴展類加載器都不能加載,則系統類加載器嘗試加載,若能加載成功,則將Sample類所對應的Class對象的引用返回給loader1,loader1再返回給loader2,從而成功將Sample類加載進虛擬機。

  若系統加載器不能加載Sample類,則loader1嘗試加載Sample類,若loader1也不能成功加載,則loader2嘗試加載。

  若所有的父加載器及loader2本身都不能加載,則拋出ClassNotFoundException異常。

  總結下來就是:

  每個加載器都優先嘗試用父類加載,若父類不能加載則自己嘗試加載;若成功則返回Class對象給子類,若失敗則告訴子類讓子類自己加載。所有都失敗則拋出異常。

類加載器的工作原理

類加載器的工作原理基於三個機制:委托、可見性和單一性。

委托機制

當一個類加載和初始化的時候,類僅在有需要加載的時候被加載。假設你有一個應用需要的類叫作Abc.class,首先加載這個類的請求由Application類加載器委托給它的父類加載器Extension類加載器,然後再委托給Bootstrap類加載器。Bootstrap類加載器會先看看rt.jar中有沒有這個類,因為並沒有這個類,所以這個請求由回到Extension類加載器,它會查看jre/lib/ext目錄下有沒有這個類,如果這個類被Extension類加載器找到了,那麽它將被加載,而Application類加載器不會加載這個類;而如果這個類沒有被Extension類加載器找到,那麽再由Application類加載器從classpath中尋找。記住classpath定義的是類文件的加載目錄,而PATH是定義的是可執行程序如javac,java等的執行路徑。

技術分享圖片

可見性機制

根據可見性機制,子類加載器可以看到父類加載器加載的類,而反之則不行。所以下面的例子中,當Abc.class已經被Application類加載器加載過了,然後如果想要使用Extension類加載器加載這個類,將會拋出java.lang.ClassNotFoundException異常。

package test;
 
    import java.util.logging.Level;
    import java.util.logging.Logger;
 
    /**
     * Java program to demonstrate How ClassLoader works in Java,
     * in particular about visibility principle of ClassLoader.
     *
     * @author Javin Paul
     */
 
    public class ClassLoaderTest {
 
        public static void main(String args[]) {
            try {          
                //printing ClassLoader of this class
                System.out.println("ClassLoaderTest.getClass().getClassLoader() : "
                                     + ClassLoaderTest.class.getClassLoader());
 
                //trying to explicitly load this class again using Extension class loader
                Class.forName("test.ClassLoaderTest", true
                                ,  ClassLoaderTest.class.getClassLoader().getParent());
            } catch (ClassNotFoundException ex) {
                Logger.getLogger(ClassLoaderTest.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
 
    }

輸出

ClassLoaderTest.getClass().getClassLoader() : sun.misc.Launcher$AppClassLoader@601bb1
16/08/2012 2:43:48 AM test.ClassLoaderTest main
SEVERE: null
java.lang.ClassNotFoundException: test.ClassLoaderTest
        at java.net.URLClassLoader$1.run(URLClassLoader.java:202)
        at java.security.AccessController.doPrivileged(Native Method)
        at java.net.URLClassLoader.findClass(URLClassLoader.java:190)
        at sun.misc.Launcher$ExtClassLoader.findClass(Launcher.java:229)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:306)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:247)
        at java.lang.Class.forName0(Native Method)
        at java.lang.Class.forName(Class.java:247)
        at test.ClassLoaderTest.main(ClassLoaderTest.java:29)

單一性機制

根據這個機制,父加載器加載過的類不能被子加載器加載第二次。雖然重寫違反委托和單一性機制的類加載器是可能的,但這樣做並不可取。你寫自己的類加載器的時候應該嚴格遵守這三條機制。

如何顯式的加載類

Java提供了顯式加載類的API:Class.forName(classname)和Class.forName(classname, initialized, classloader)。就像上面的例子中,你可以指定類加載器的名稱以及要加載的類的名稱。類的加載是通過調用java.lang.ClassLoader的loadClass()方法,而loadClass()方法則調用了findClass()方法來定位相應類的字節碼。在這個例子中Extension類加載器使用了java.net.URLClassLoader,它從JAR和目錄中進行查找類文件,所有以”/”結尾的查找路徑被認為是目錄。如果findClass()沒有找到那麽它會拋出java.lang.ClassNotFoundException異常,而如果找到的話則會調用defineClass()將字節碼轉化成類實例,然後返回。

什麽地方使用類加載器

類加載器是個很強大的概念,很多地方被運用。最經典的例子就是AppletClassLoader,它被用來加載Applet使用的類,而Applets大部分是在網上使用,而非本地的操作系統使用。使用不同的類加載器,你可以從不同的源地址加載同一個類,它們被視為不同的類。J2EE使用多個類加載器加載不同地方的類,例如WAR文件由Web-app類加載器加載,而EJB-JAR中的類由另外的類加載器加載。有些服務器也支持熱部署,這也由類加載器實現。你也可以使用類加載器來加載數據庫或者其他持久層的數據。

Day18 (一)類的加載器