Java--類加載機制與類加載器

Java的核心是 JVM ,了解并熟悉JVM對于我們理解Java語言非常重要。

一、類加載機制

當程序主動使用某個類時,如果該類還未被加載到內存中,則系統會通過加載、連接、初始化三個步驟來對該類進行初始化。

JVM把描述類的數據從class文件加載到內存,并對數據進行校驗,轉換解析和初始化,最終形成可以被虛擬機直接使用的Java類型,這就是JVM的類加載機制。

1、JVM和類

當調用Java命令運行某個Java程序時,該命令將會啟動一個Java虛擬機進程,不管該Java程序有多么復雜,該程序啟動了多少個線程,它們都處于該Java虛擬機進程里。

同一個JVM的所有線程、所有變量都處于同一個進程里,它們都使用該JVM進程的內存區。

當系統出現以下幾種情況時,JVM進程將被終止:

  • 程序運行到最后正常結束。
  • 程序運行到使用System.exit()Runtime.getRuntime().exit() 處程序結束。
  • 程序執行過程中遇到未捕獲的異常或錯誤而結束。
  • 程序所在平臺強制結束了JVM進程。

從上面介紹可以知道,當Java程序運行結束時,JVM進程結束,該進程在內存中的狀態將會丟失。

2、類的加載

類加載:將類的class文件讀入內存,并為之創建一個java.lang.Class 對象,也就是說,當程序中使用任何類時,系統都會為之建立一個java.lang.Class對象。

類的加載由類加載器完成,類加載器通常由JVM提供,這些類加載器也是前面所有程序運行的基礎,JVM提供的這些類加載器通常被稱為系統類加載器。除此之外,開發者可以通過繼承 ClassLoader 基類來創建自己的類加載器。

通過使用不同的類加載器,可以從不同來源加載類的二進制數據,通常有如下幾種來源:

  • 從本地文件系統加載class文件。
  • 從 JAR 包加載class文件。
  • 通過網絡加載class文件。
  • 把一個 java 源文件動態編譯,并進行加載。

在加載階段虛擬機需要完成以下三件事

  1. 通過一個類的全限定名稱來獲取此類的二進制字節流,并加載到內存中(需要使用類加載器)
  2. 將這個字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構
  3. 在堆中生成一個代表這個類的java.lang.Class對象,作為方法區這個類的各種數據的訪問入口
3、類的鏈接

將Java類的二進制數據合并到 JVM 的運行狀態之中。

類的連接分為三個階段:

  1. 驗證:驗證被加載后的類是否有正確的結構,類數據是否符合虛擬機的要求,確保不會危害虛擬機安全。
    包含四個階段的校驗動作:a.文件格式驗證;b.原數據信息進行語義校驗;c.字節碼驗證;d.符號引用驗證。

  2. 準備:為類的靜態變量(static filed)在方法區分配內存,并設置默認初始值(0值或null值),這些內存都將在方法區分配。對于一般的成員變量是在類實例化時候,隨對象一起分配在堆內存中。
    另外,靜態常量(static final filed)會在準備階段賦程序設定的初值,對于靜態變量,這個操作是在初始化階段進行的。

  3. 解析:將類的二進制數據內的符號引用替換為直接引用。

4、類的初始化

在該階段,虛擬機負責對類進行初始化,主要是對類變量進行初始化。

在Java類中,對類變量指定初始值有兩種方式:
(1)在聲明類變量時指定初始值。
(2)在使用靜態初始化塊時,為類變量指定初始值。

JVM會按照這些語句在程序中的排列順序依次執行他們。

JVM初始化一個類的步驟

  1. 假如這個類還沒有被加載和連接,則程序先加載并連接該類。
  2. 假如該類的直接父類還沒有被初始化,則先初始化其直接父類。若該直接父類又有直接父類,依次類推。所以JVM最先初始化的總是 java.lang.Object 類。
    當程序主動使用任何一個類時,系統會保證該類以及所有父類(包括直接父類和間接父類)都會被初始化。
  3. 假如類中有初始化語句,則系統依次執行這些初始化語句。
5、類初始化的時機

當Java程序首次通過下面的 6種 方式來使用某個類或接口時,系統就會初始化該類或接口,也稱為主動初始化。

觸發類加載的條件

  1. 創建類的實例。
  • 使用 new 來創建實例。
  • 通過反射創建實例。
  • 通過反序列化來創建實例。
  1. 調用類的類變量(靜態屬性),或為該類變量賦值。
  2. 調用類的靜態方法。
  3. 通過class文件反射創建對象。
    例如:Class.forName("Person");
  4. 初始化一個子類的時候,該子類的所有父類都會被初始化。
  5. java虛擬機啟動時被標記為啟動類的類,就是 main 方法所在的類。

同時還需要注意幾點

  1. 在同一個類加載器下面只能初始化類一次,如果已近初始化了就不要初始化了。
    因為累加載的最終結果就是在堆中存有唯一的一個Class對象,這樣通過Class對象就能找到類的相關信息。
  2. 在編譯時能夠確定下來的 final修飾的靜態變量(編譯常量)不會對類進行初始化。
  3. 在編譯時無法確定下來的 final修飾的靜態變量(運行時常量)會對類進行初始化。
  4. 如果這個類沒有被加載和連接,那就需要進行加載和連接。
  5. 如果這個類有父類并且這個父類沒有被初始化,則先初始化父類。
  6. 如果類中存在初始化語句,依次執行初始化語句。

一個有關的小例子:

public class Single {
    private static Single single = new Single();
    public static int counter1;
    public static int counter2 = 0;
    
    private Single () {
        counter1++;
        counter2++;
    }
    
    public static Single getSingle() {
        return single;
    }
    
}
public class SingleTest {
    public static void main(String[] args) {
        Single single = Single.getSingle();
        System.out.println("counter1=" + single.counter1);
        System.out.println("counter2=" + single.counter2);
    }
}

輸出是:
counter1=1
counter2=0

例子分析:

  1. 在執行SIngleTest第一句的時候,還沒有對Single類進行加載和連接,所以首先需要對它進行加載和連接。
    在連接——準備階段,要給靜態變量賦默認的初始值。
singel=null
counter1=0
counter2=0
  1. 加載和連接完畢之后,再進行初始化工作。這時會依次執行。
    首先第一個靜態屬性single = new Single();會執行構造方法內部的邏輯操作,此時
counter1=1
counter2=1

接下來第二個靜態屬性counter1,程序并沒有對它進行初始化賦值,所以它沒辦法進行初始化。
第三個屬性counter2我們初始化復制為0,因此可以初始化為 counter2=1。

  1. 初始化完畢之后,就調用了靜態方法Single.getSingle(); 放回的 single 已經初始化了。

輸出的內容也理所當然就是counter1=1,counter2=0

二、類加載器

類加載器負責將 .class 文件加載到內存中,并為之生成對應的 Class 對象。

在JVM中,一個類用其全限定類名和其類加載器作為唯一的標識。這樣保證同一個類不會再次被載入。

1、類加載器的層級結構
引導類加載器(Bootstrap ClassLoader)
  • 它用來加載Java的核心庫(JAVA_HOME/jre/lib/rt.jar或sun.boot.class.Path路徑下的內容),是用C++代碼來實現的,并不繼承自java.lang.Classloader。

  • 加載擴展類和應用程序類加載器,并指定他們的父類加載器。

  • 啟動類加載器無法被Java程序直接引用

擴展類加載器(Extension ClassLoader)
  • 用來加載Java的擴展庫(JAVA_HOME/jre/ext/*.jar或java.ext.dirs路徑下的內容)。 Java虛擬機的實現會提供一個擴展庫目錄。該類加載器在此目錄里面查找并加載Java類。
  • 由sun.misc.Launcher$ExtClassLoader實現。
應用程序類加載器(Application ClassLoader)
  • 它根據Java應用的類路徑(classpath,java.class.path類。 一般來說,Java應用的類都是由它來完成加載的。
  • 由sun.misc.Launcher$AppClassLoader實現。
自定義類加載器

開發人員可以用過繼承java.lang.ClassLoader類的方式實現自己的類加載器,以滿足一些特殊的要求

2、類加載機制——雙親委派模式

幾個類加載器實現類加載過程時相互配合協作的流程。

從JDK1.2開始,java虛擬機規范就推薦開發者使用雙親委派模式(ParentsDelegation Model)進行類加載,其加載過程如下

  1. 如果一個類加載器收到了類加載請求,它首先不會自己去嘗試加載這個類,而是把類加載請求委派給父類加載器去完成。
  2. 每一層的類加載器都把類加載請求委派給父類加載器,依次向上,直到所有的類加載請求都傳遞給頂層的啟動類加載器。
  3. 如果頂層的啟動類加載器無法完成加載請求,子類加載器才會嘗試自己去加載該類,如果連最初發起類加載請求的類加載器也無法完成加載請求時,將會拋出ClassNotFoundException,而不再調用其子類加載器去進行類加載。

雙親委派模式的類加載機制的優點
不同層次的類加載器具有不同優先級,比如所有Java對象的超級父類java.lang.Object,位于rt.jar,無論哪個類加載器加載該類,最終都是由啟動類加載器進行加載,保證安全。即使用戶自己編寫一個java.lang.Object類并放入程序中,雖能正常編譯,但不會被加載運行,保證不會出現混亂。

注意:

  • 并不是所有的類加載器都采用雙親委托機制。
  • tomcat服務器類加載器也是用代理模式,所不同的是它首先嘗試去加載某個類,如果找不到再找代理給父類加載器。這與一般類加載器的順序是相反的。
雙親委派模型的代碼實現

ClassLoader中loadClass方法實現了雙親委派模型

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        //檢查該類是否已經加載過
        Class c = findLoadedClass(name);
        if (c == null) {
            //如果該類沒有加載,則進入該分支
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    //當父類的加載器不為空,則通過父類的loadClass來加載該類
                    c = parent.loadClass(name, false);
                } else {
                    //當父類的加載器為空,則調用啟動類加載器來加載該類
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                //非空父類的類加載器無法找到相應的類,則拋出異常
            }

            if (c == null) {
                //當父類加載器無法加載時,則調用findClass方法來加載該類
                long t1 = System.nanoTime();
                c = findClass(name); //用戶可通過覆寫該方法,來自定義類加載器

                //用于統計類加載器相關的信息
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            //對類進行link操作
            resolveClass(c);
        }
        return c;
    }
}

整個流程大致如下:

a.首先,檢查一下指定名稱的類是否已經加載過,如果加載過了,就不需要再加載,直接返回。

b.如果此類沒有加載過,那么,再判斷一下是否有父加載器;如果有父加載器,則由父加載器加載(即調用parent.loadClass(name, false);).或者是調用bootstrap類加載器來加載。

c.如果父加載器及bootstrap類加載器都沒有找到指定的類,那么調用當前類加載器的findClass方法來完成類加載。

3、自定義類加載器

通過擴展 ClassLoader 的子類,重寫 ClassLoader 所包含的方法來實現自定義的類加載器。

ClassLoader 類有如下兩個關鍵方法:

  • loadClass(String name, boolean resolve):該方法為ClassLoader的入口點,根據指定名稱來加載類,系統就是調用 ClassLoader 的該方法來獲取指定類對應的 Class 對象。
  • findClass(String name):根據指定名稱來查找類。

通常推薦重寫 findClass() 方法。

在 ClassLoader 類中還有一個核心方法:

  • Class defineClass(String name, byte[] b, int off, int len):該方法負責將指定類的字節碼文件(即Class文件,如:Hello.class)讀入字節數組 byte[] b 內,并把它轉換為 Class 對象。

無須重寫該方法,因為該方法是 final 的。

除此之外,ClassLoader 類還有一些普通方法:

  • findSystemClass(String name):從本地系統裝入文件。
  • static getSystemClassLoader():用于返回系統類加載器。
  • getParent() :獲取該類加載器的父類加載器。
  • resolveClass(Class<?> c):鏈接指定的類。
  • findLoadClass(String name):如果Java虛擬機已經加載了名為 name 的類,則直接返回該類對應的 Class 實例,否則返回 null 。該方法是 Java 類加載緩存機制的體現。

整個的函數調用流程:

我們可以簡單地自定義一個類加載器,用于加載某個class

public class FileSystemClassLoader extends ClassLoader {
    //文件的根目錄
    private String rootDir;

    public FileSystemClassLoader(String rootDir){
        this.rootDir=rootDir;
    }

    //重寫findClass方法
    @Override
    protected Class<?> findClass(String s) throws ClassNotFoundException {
        Class c=findLoadedClass(s);
        if (c!=null){
            return c;
        }else {
            ClassLoader parent=this.getParent();
            //parent獲取不到class時會拋出異常,為了繼續執行使用try catch包裹
            try{
                c=parent.loadClass(s);
            }catch (Exception e){

            }
            if (c!=null){
                return  c;
            }else {
                byte[] classData=getClassData(s);
                if (classData==null){
                    throw new ClassNotFoundException();
                }else {
                    //將字節數組轉為Class
                    c=defineClass(s,classData,0,classData.length);
                }
            }
        }
        return c;
    }

    //將文件轉為字節數組
    private byte[] getClassData(String className) {
        //改為文件地址
        String path=rootDir+"/"+className.replace(".","/")+".class";
        System.out.println(path);
        ByteArrayOutputStream byteArrayOutputStream=new ByteArrayOutputStream();
        InputStream inputStream=null;
        try {
            inputStream=new FileInputStream(path);
            byte[] buffer=new byte[1024];
            int temp=0;
            while ((temp=inputStream.read(buffer))!=-1){
                byteArrayOutputStream.write(buffer,0,temp);
            }
            return byteArrayOutputStream.toByteArray();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        } finally {
            if (inputStream!=null){
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (byteArrayOutputStream!=null){
                try {
                    byteArrayOutputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return null;
    }
}
public class UseCustomClassLoader {
    public static void main(String[]args){
        FileSystemClassLoader loader=new FileSystemClassLoader("/home/xjk");
        FileSystemClassLoader loader2=new FileSystemClassLoader("/home/xjk");
        try {
            Class clazz1=loader.findClass("com.jk.bean.Emp");//本項目自定義的類調用AppClassLoader
            System.out.println(clazz1.getClassLoader());
            Class clazz2=loader.findClass("java.lang.String");//rt.jar里的類調用BootstrapClassLoader
            System.out.println(clazz2.getClassLoader());
            Class clazz3=loader.findClass("com.company.Main");//項目外的類調用自定義的FileSystemClassLoader
            System.out.println(clazz3.getClassLoader());
            Class clazz4=loader2.findClass("com.company.Main");//使用不同類加載器,Class對象不一致
            System.out.println(clazz4.getClassLoader());
            System.out.println(clazz3==clazz4);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

輸出結果

sun.misc.Launcher$AppClassLoader@18b4aac2
null
com.jk.jvm.FileSystemClassLoader@1d44bcfa
com.jk.jvm.FileSystemClassLoader@6f94fa3e
false

因為BootstrapClassLoader無法被Java程序直接引用,所以顯示為空。

使用自定義的類加載器,可以實現如下常見的功能

  • 執行代碼前自動驗證數字簽名。
  • 根據用戶提供的密碼解密代碼,從而可以實現代碼混淆器來避免反編譯 *.class 文件。
  • 根據用戶需求來動態的加載類。
  • 根據用戶需求把其他數據以字節碼的形式加載到應用中。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容