類加載流程,類加載機制及自定義類加載器詳解(面試不再怕了)

1、引言2、類的加載、連接、初始化一、加載1.一、加載的class來源二、類的連接2.一、驗證2.二、準備2.三、解析三、類的初始化3.一、< clinit>方法相關3.二、類初始化時機3.三、final定義的初始化3.四、ClassLoader只會對類進行加載,不會進行初始化3、類加載器一、JVM類加載器分類1.一、Bootstrap ClassLoader1.2 、Extension ClassLoader1.三、 System ClassLoader4、類加載機制1.一、JVM主要的類加載機制。1.二、類加載流程圖5、建立並使用自定義類加載器一、自定義類加載分析二、實現自定義類加載器6、總結java

1、引言

當程序使用某個類時,若是該類還未被加載到內存中,則JVM會經過加載、連接、初始化三個步驟對該類進行類加載。bootstrap

2、類的加載、連接、初始化

一、加載

類加載指的是將類的class文件讀入內存,併爲之建立一個java.lang.Class對象。類的加載過程是由類加載器來完成,類加載器由JVM提供。咱們開發人員也能夠經過繼承ClassLoader來實現本身的類加載器。數組

1.一、加載的class來源
  • 從本地文件系統內加載class文件
  • 從JAR包加載class文件
  • 經過網絡加載class文件
  • 把一個java源文件動態編譯,並執行加載。

二、類的連接

經過類的加載,內存中已經建立了一個Class對象。連接負責將二進制數據合併到 JRE中。連接須要經過驗證、準備、解析三個階段。緩存

2.一、驗證

驗證階段用於檢查被加載的類是否有正確的內部結構,並和其餘類協調一致。便是否知足java虛擬機的約束。安全

2.二、準備

類準備階段負責爲類的類變量分配內存,並設置默認初始值。網絡

2.三、解析

咱們知道,引用其實對應於內存地址。思考這樣一個問題,在編寫代碼時,使用引用,方法時,類知道這些引用方法的內存地址嗎?顯然是不知道的,由於類還未被加載到虛擬機中,你沒法得到這些地址。舉例來講,對於一個方法的調用,編譯器會生成一個包含目標方法所在的類、目標方法名、接收參數類型以及返回值類型的符號引用,來指代要調用的方法。多線程

解析階段的目的,就是將這些符號引用解析爲實際引用。若是符號引用指向一個未被加載的類,或者未被加載類的字段或方法,那麼解析將觸發這個類的加載(但未必會觸發解析與初始化)。app

三、類的初始化

類的初始化階段,虛擬機主要對類變量進行初始化。虛擬機調用< clinit>方法,進行類變量的初始化。jvm

java類中對類變量進行初始化的兩種方式:ide

  1. 在定義時初始化
  2. 在靜態初始化塊內初始化
3.一、< clinit>方法相關
  • 虛擬機會收集類及父類中的類變量及類方法組合爲< clinit>方法,根據定義的順序進行初始化。虛擬機會保證子類的< clinit>執行以前,父類的< clinit>方法先執行完畢。所以,虛擬機中第一個被執行完畢的< clinit>方法確定是java.lang.Object方法。
public class Test {
    static int A = 10;
    static {
        A = 20;
    }
}

class Test1 extends Test {
    private static int B = A;
    public static void main(String[] args) {
        System.out.println(Test1.B);
    }
}
//輸出結果
//20
複製代碼

從輸出中看出,父類的靜態初始化塊在子類靜態變量初始化以前初始化完畢,因此輸出結果是20,不是10。

  • 若是類或者父類中都沒有靜態變量及方法,虛擬機不會爲其生成< clinit>方法。

  • 接口與類不一樣的是,執行接口的<clinit>方法不須要先執行父接口的<clinit>方法。 只有當父接口中定義的變量使用時,父接口才會初始化。 另外,接口的實現類在初始化時也同樣不會執行接口的<clinit>方法。

public interface InterfaceInitTest {
    long A = CurrentTime.getTime();

}

interface InterfaceInitTest1 extends InterfaceInitTest {
    int B = 100;
}

class InterfaceInitTestImpl implements InterfaceInitTest1 {
    public static void main(String[] args) {
        System.out.println(InterfaceInitTestImpl.B);
        System.out.println("---------------------------");
        System.out.println("當前時間:"+InterfaceInitTestImpl.A);
    }
}

class CurrentTime {
    static long getTime() {
        System.out.println("加載了InterfaceInitTest接口");
        return System.currentTimeMillis();
    }
}
//輸出結果
//100
//---------------------------
//加載了InterfaceInitTest接口
//當前時間:1560158880660
複製代碼

從輸出驗證了:對於接口,只有真正使用父接口的類變量纔會真正的加載父接口。這跟普通類加載不同。

  • 虛擬機會保證一個類的< clinit>方法在多線程環境中被正確地加鎖和同步,若是多個線程同時去初始化一個類,那麼只有一個線程去執行這個類的< clinit>方法,其餘線程都須要阻塞等待,直到活動線程執行< clinit>方法完畢。
public class MultiThreadInitTest {
    static int A = 10;
    static {
           System.out.println(Thread.currentThread()+"init MultiThreadInitTest");
        try {
            TimeUnit.SECONDS.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        Runnable runnable = () -> {
            System.out.println(Thread.currentThread() + "start");
            System.out.println(MultiThreadInitTest.A);
            System.out.println(Thread.currentThread() + "run over");
        };
        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        thread1.start();
        thread2.start();
    }
}
//輸出結果
//Thread[main,5,main]init MultiThreadInitTest
//Thread[Thread-0,5,main]start
//10
//Thread[Thread-0,5,main]run over
//Thread[Thread-1,5,main]start
//10
//Thread[Thread-1,5,main]run over
複製代碼

從輸出中看出驗證了:只有第一個線程對MultiThreadInitTest進行了一次初始化,第二個線程一直阻塞等待等第一個線程初始化完畢。

3.二、類初始化時機
  1. 當虛擬機啓動時,初始化用戶指定的主類;
  2. 當遇到用以新建目標類實例的new指令時,初始化new指令的目標類;
  3. 當遇到調用靜態方法或者使用靜態變量,初始化靜態變量或方法所在的類;
  4. 子類初始化過程會觸發父類初始化;
  5. 若是一個接口定義了default方法,那麼直接實現或者間接實現該接口的類的初始化,會觸發該接口初始化;
  6. 使用反射API對某個類進行反射調用時,初始化這個類;
  7. Class.forName()會觸發類的初始化
3.三、final定義的初始化

注意:對於一個使用final定義的常量,若是在編譯時就已經肯定了值,在引用時不會觸發初始化,由於在編譯的時候就已經肯定下來,就是「宏變量」。若是在編譯時沒法肯定,在初次使用纔會致使初始化。

public class StaticInnerSingleton {
    /**
     * 使用靜態內部類實現單例:
     * 1:線程安全
     * 2:懶加載
     * 3:非反序列化安全,即反序列化獲得的對象與序列化時的單例對象不是同一個,違反單例原則
     */

    private static class LazyHolder {
        private static final StaticInnerSingleton INNER_SINGLETON = new StaticInnerSingleton();
    }

    private StaticInnerSingleton() {
    }

    public static StaticInnerSingleton getInstance() {
        return LazyHolder.INNER_SINGLETON;
    }
}
複製代碼

看這個例子,單例模式靜態內部類實現方式。咱們能夠看到單例實例使用final定義,但在編譯時沒法肯定下來,因此在第一次使用StaticInnerSingleton.getInstance()方法時,纔會觸發靜態內部類的加載,也就是延遲加載。這裏想指出,若是final定義的變量在編譯時沒法肯定,則在使用時仍是會進行類的初始化。

3.四、ClassLoader只會對類進行加載,不會進行初始化
public class Tester {
    static {
        System.out.println("Tester類的靜態初始化塊");
    }
}

class ClassLoaderTest {
    public static void main(String[] args) throws ClassNotFoundException {
        ClassLoader classLoader = ClassLoader.getSystemClassLoader();
        //下面語句僅僅是加載Tester類
        classLoader.loadClass("loader.Tester");
        System.out.println("系統加載Tester類");
        //下面語句纔會初始化Tester類
        Class.forName("loader.Tester");
    }
}
//輸出結果
//系統加載Tester類
//Tester類的靜態初始化塊
複製代碼

從輸出證實:ClassLoader只會對類進行加載,不會進行初始化;使用Class.forName()會強制致使類的初始化。

3、類加載器

類加載器負責將.class文件(不論是jar,仍是本地磁盤,仍是網絡獲取等等)加載到內存中,併爲之生成對應的java.lang.Class對象。一個類被加載到JVM中,就不會第二次加載了。

那怎麼判斷是同一個類呢?

每一個類在JVM中使用全限定類名(包名+類名)與類加載器聯合爲惟一的ID,因此若是同一個類使用不一樣的類加載器,能夠被加載到虛擬機,但彼此不兼容。

一、JVM類加載器分類

1.一、Bootstrap ClassLoader

Bootstrap ClassLoader爲根類加載器,負責加載java的核心類庫。根加載器不是ClassLoader的子類,是有C++實現的。

public class BootstrapTest {
    public static void main(String[] args) {
        //獲取根類加載器所加載的所有URL數組
        URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
        Arrays.stream(urLs).forEach(System.out::println);
    }
}
//輸出結果
//file:/C:/SorftwareInstall/java/jdk/jre/lib/resources.jar
//file:/C:/SorftwareInstall/java/jdk/jre/lib/rt.jar
//file:/C:/SorftwareInstall/java/jdk/jre/lib/sunrsasign.jar
//file:/C:/SorftwareInstall/java/jdk/jre/lib/jsse.jar
//file:/C:/SorftwareInstall/java/jdk/jre/lib/jce.jar
//file:/C:/SorftwareInstall/java/jdk/jre/lib/charsets.jar
//file:/C:/SorftwareInstall/java/jdk/jre/lib/jfr.jar
//file:/C:/SorftwareInstall/java/jdk/jre/classes
複製代碼

根類加載器負責加載%JAVA_HOME%/jre/lib下的jar包(以及由虛擬機參數 -Xbootclasspath 指定的類)。

咱們將rt.jar解壓,能夠看到咱們常用的類庫就在這個jar包中。

1.2 、Extension ClassLoader

Extension ClassLoader爲擴展類加載器,負責加載%JAVA_HOME%/jre/ext或者java.ext.dirs系統熟悉指定的目錄的jar包。你們能夠將本身寫的工具包放到這個目錄下,能夠方便本身使用。

1.三、 System ClassLoader

System ClassLoader爲系統(應用)類加載器,負責加載加載來自java命令的-classpath選項、java.class.path系統屬性,或者CLASSPATH環境變量所指定的JAR包和類路徑。程序能夠經過ClassLoader.getSystemClassLoader()來獲取系統類加載器。若是沒有特別指定,則用戶自定義的類加載器默認都以系統類加載器做爲父加載器。

4、類加載機制

1.一、JVM主要的類加載機制。

  1. 全盤負責:當一個類加載器負責加載某個Class時,該Class所依賴和引用的其餘Class也由該類加載器負責載入,除非顯示使用另外一個類加載器來載入。
  2. 父類委託(雙親委派):先讓父加載器試圖加載該Class,只有在父加載器沒法加載時該類加載器纔會嘗試從本身的類路徑中加載該類。
  3. 緩存機制:緩存機制會將已經加載的class緩存起來,當程序中須要使用某個Class時,類加載器先從緩存區中搜尋該Class,只有當緩存中不存在該Class時,系統纔會讀取該類的二進制數據,並將其轉換爲Class對象,存入緩存中。這就是爲何更改了class後,須要重啓JVM才生效的緣由。

注意:類加載器之間的父子關係並非類繼承上的父子關係,而是實例之間的父子關係。

public class ClassloaderPropTest {
    public static void main(String[] args) throws IOException {
        //獲取系統類加載器
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        System.out.println("系統類加載器:" + systemClassLoader);
        /*
        獲取系統類加載器的加載路徑——一般由CLASSPATH環境變量指定,若是操做系統沒有指定
        CLASSPATH環境變量,則默認以當前路徑做爲系統類加載器的加載路徑
         */

        Enumeration<URL> eml = systemClassLoader.getResources("");
        while (eml.hasMoreElements()) {
            System.out.println(eml.nextElement());
        }
        //獲取系統類加載器的父類加載器,獲得擴展類加載器
        ClassLoader extensionLoader = systemClassLoader.getParent();
        System.out.println("系統類的父加載器是擴展類加載器:" + extensionLoader);
        System.out.println("擴展類加載器的加載路徑:" + System.getProperty("java.ext.dirs"));
        System.out.println("擴展類加載器的parant:" + extensionLoader.getParent());
    }
}
//輸出結果
//系統類加載器:sun.misc.Launcher$AppClassLoader@18b4aac2
//file:/C:/ProjectTest/FengKuang/out/production/FengKuang/
//系統類的父加載器是擴展類加載器:sun.misc.Launcher$ExtClassLoader@1540e19d
//擴展類加載器的加載路徑:C:\SorftwareInstall\java\jdk\jre\lib\ext;C:\WINDOWS\Sun\Java\lib\ext
//擴展類加載器的parant:null
複製代碼

從輸出中驗證了:系統類加載器的父加載器是擴展類加載器。但輸出中擴展類加載器的父加載器是null,這是由於父加載器不是java實現的,是C++實現的,因此獲取不到。但擴展類加載器的父加載器是根加載器。

1.二、類加載流程圖

圖中紅色部分,能夠是咱們自定義實現的類加載器來進行加載。

5、建立並使用自定義類加載器

一、自定義類加載分析

除了根類加載器,全部類加載器都是ClassLoader的子類。因此咱們能夠經過繼承ClassLoader來實現本身的類加載器。

ClassLoader類有兩個關鍵的方法:

  1. protected Class loadClass(String name, boolean resolve):name爲類名,resove若是爲true,在加載時解析該類。
  2. protected Class findClass(String name) :根據指定類名來查找類。

因此,若是要實現自定義類,能夠重寫這兩個方法來實現。但推薦重寫findClass方法,而不是重寫loadClass方法,由於loadClass方法內部回調用findClass方法。

咱們來看一下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 {
                //第二步,判斷父加載器是否爲null
                    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) {
                   //第三步,若是前面都沒有找到,就會調用findClass方法
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                   sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
複製代碼

loadClass加載方法流程:

  1. 判斷此類是否已經加載;
  2. 若是父加載器不爲null,則使用父加載器進行加載;反之,使用根加載器進行加載;
  3. 若是前面都沒加載成功,則使用findClass方法進行加載。

因此,爲了避免影響類的加載過程,咱們重寫findClass方法便可簡單方便的實現自定義類加載。

二、實現自定義類加載器

基於以上分析,咱們簡單重寫findClass方法進行自定義類加載。

public class Hello {
   public void test(String str){
       System.out.println(str);
   }
}

public class MyClassloader extends ClassLoader {

    /**
     * 讀取文件內容
     *
     * @param fileName 文件名
     * @return
     */

    private byte[] getBytes(String fileName) throws IOException {
        File file = new File(fileName);
        long len = file.length();
        byte[] raw = new byte[(int) len];
        try (FileInputStream fin = new FileInputStream(file)) {
            //一次性讀取Class文件的所有二進制數據
            int read = fin.read(raw);
            if (read != len) {
                throw new IOException("沒法讀取所有文件");
            }
            return raw;
        }
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        Class clazz = null;
        //將包路徑的(.)替換爲斜線(/)
        String fileStub = name.replace(".""/");
        String classFileName = fileStub + ".class";
        File classFile = new File(classFileName);

        //若是Class文件存在,系統負責將該文件轉換爲Class對象
        if (classFile.exists()) {
            try {
                //將Class文件的二進制數據讀入數組
                byte[] raw = getBytes(classFileName);
                //調用ClassLoader的defineClass方法將二進制數據轉換爲Class對象
                clazz = defineClass(name, raw, 0, raw.length);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        //若是clazz爲null,代表加載失敗,拋出異常
        if (null == clazz) {
            throw new ClassNotFoundException(name);
        }
        return clazz;
    }

    public static void main(String[] args) throws Exception {
        String classPath = "loader.Hello";
        MyClassloader myClassloader = new MyClassloader();
        Class<?> aClass = myClassloader.loadClass(classPath);
        Method main = aClass.getMethod("test", String.class);
        System.out.println(main);
        main.invoke(aClass.newInstance(), "Hello World");
    }
}
//輸出結果
//Hello World
複製代碼

ClassLoader還有一個重要的方法defineClass(String name, byte[] b, int off, int len)。此方法的做用是將class的二進制數組轉換爲Calss對象。

此例子很簡單,我寫了一個Hello測試類,而且編譯事後放在了當前路徑下(你們能夠在findClass中加入判斷,若是沒有此文件,能夠嘗試查找.java文件,並進行編譯獲得.class文件;或者判斷.java文件的最後更新時間大於.class文件最後更新時間,再進行從新編譯等邏輯)。

6、總結

本篇從類加載的三大階段:加載、連接、初始化開始細說每一個階段的過程;詳細講解了JVM經常使用的類加載器的區別與聯繫,以及類加載機制流程,最後經過自定義的類加載器例子結束本篇。小弟能力有限,你們看出有問題請指出,讓博主學習改正。歡迎討論啊。

注意:本篇博客總結主要來源。若有轉載,請註明出處

  1. 《瘋狂java講義(第3版)》
  2. 《深刻理解java虛擬機++JVM高級特性與最佳實踐》
相關文章
相關標籤/搜索