Java類加載

本篇筆記的目標(biāo)是理解類加載器的架構(gòu),學(xué)會實現(xiàn)類加載器并理解熱替換的底層原理。

什么是類加載

類從被加載到虛擬機內(nèi)存中開始,到卸載出內(nèi)存為止,包括了以下幾個生命周期:

image.png

什么時候會觸發(fā)類加載的第一個階段(加載)?虛擬機規(guī)范沒有強制規(guī)定,這一點依據(jù)不同的虛擬機實現(xiàn)來定。但對于初始化階段,虛擬機規(guī)范規(guī)定了有且只有5種>情況必須立即對類進行初始化(加載階段自然要在此之前開始):

1.使用new關(guān)鍵字實例化對象、讀取或設(shè)置一個類的靜態(tài)字段(被final修飾的常量字段除外)、調(diào)用一個類的靜態(tài)方法。

2.使用反射方法對類進行調(diào)用

3.初始化一個類的時候,發(fā)現(xiàn)其父類未初始化,則觸發(fā)父類的初始化

4.虛擬機啟動時,用戶需指定一個要執(zhí)行的主類(包含main的那個類),虛擬機先初始化該類

5.當(dāng)使用jdk1.7的動態(tài)語言支持時,如果一個java.lang.invoke。MethodHandle實例最后的解析結(jié)果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄所對應(yīng)的類沒有進行過初始化,則先觸發(fā)其初始化(不懂...)

--《深入理解jvm虛擬機》

這篇筆記所要學(xué)習(xí)的內(nèi)容,僅僅是類加載的第一個階段:加載。在加載階段,虛擬機會完成下面三件事:

1.通過一個類的全限定名獲取定義此類的二進制字節(jié)流

2.將這個字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運行時數(shù)據(jù)結(jié)構(gòu)

3.在內(nèi)存中生成一個代表這個類的java.lang.Class對象,作為方法區(qū)中這個類的各種數(shù)據(jù)的訪問入口

在上面的三個階段中,通過一個類的全限定名獲取定義此類的二進制字節(jié)流 是開發(fā)人員可以控制的部分,也是我們這篇筆記所要探討的內(nèi)容。

虛擬機設(shè)計團隊將通過一個類的全限定名獲取定義此類的二進制字節(jié)流這個動作放到j(luò)ava虛擬機外部去實現(xiàn),以便讓應(yīng)用程序自己決定去如何獲取所需要的類。實現(xiàn)這個動作的代碼模塊被稱為"類加載器"。定義此類的二進制字節(jié)流可以來自class文件、網(wǎng)絡(luò)、zip包、或者運行時生成等。

類加載器實現(xiàn)類的加載動作,比較兩個類是否相等,只有在這兩個類是由同一個類加載器加載的前提下才有意義,否則,即使兩個類源自于同一份class文件,被同一個虛擬機加載,只要加載他們的類加載器不同,那么這兩個類必定不相等。

public class ClassLocaderTest {
    public static void main(String[] args) {
        Object testClassLoader1 = getMyClassLoader1();
        System.out.println(testClassLoader1.getClass());
        System.out.println(testClassLoader1 instanceof space.kyu.TestClass);
    }
    static Object getMyClassLoader1() {
        Object obj = null;
        try {
            MyClassLoader1 loader = new MyClassLoader1();
            obj = loader.loadClass("space.kyu.TestClass").newInstance();
        } catch (Exception e) {
            System.out.println(e);
        }
        return obj;
    }
}
class MyClassLoader1 extends ClassLoader{
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        try {
            String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
            InputStream stream = getClass().getResourceAsStream(fileName);
            if (stream == null) {
//              System.out.println("ClassLoader load class" + name);
                return super.loadClass(name);
            }
            byte[] bs = new byte[stream.available()];
            stream.read(bs);
//          System.out.println("MyClassLoader1 load class: " + name);
            return defineClass(name, bs, 0, bs.length);
        } catch (IOException e) {
            throw new ClassNotFoundException(name);
        }
    }
    
}

輸出:

class space.kyu.TestClass
false

在上面的例子中,虛擬機中存在兩個space.kyu.TestClass類,一個是由系統(tǒng)應(yīng)用程序類加載器加載的,一個是由我們自己實現(xiàn)的類加載器加載的。雖然來自同一個class文件,但依然是兩個獨立的類,故不相等。

類加載器應(yīng)用于類層次劃分、OSGI、熱部署、代碼加密等方面。

類加載器層次結(jié)構(gòu)

從java虛擬機的角度來看,類加載器分為兩類:

1.啟動類加載器

使用c++實現(xiàn),是虛擬機自身的一部分

2.其他類加載器

由java語言實現(xiàn),獨立于虛擬機外部,全都繼承自抽象類java.lang.ClassLoader

從類加載器的實現(xiàn)來看,類加載器又可分為系統(tǒng)提供的類加載器與我們自己實現(xiàn)的類加載器。系統(tǒng)提供的類加載器主要有三個:

  • 引導(dǎo)類加載器,用來加載java核心類庫。主要是放在JAVA_HOME\lib目錄中或被-Xbootclasspath所指定的目錄。

  • 擴展類加載器,由sun.misc.Launcher$ExtClassLoader實現(xiàn)。負(fù)責(zé)加載JAVA_HOME\lib\ext目錄中,或java.ext.dirs所指定的路徑中的類庫。

  • 應(yīng)用程序類加載器,由sun.misc.Launcher$AppClassLoader實現(xiàn)。這個類也是ClassLoader中g(shù)etSystemClassLoader()方法的返回值。負(fù)責(zé)加載classpath上指定的類庫。

除了系統(tǒng)提供的類加載器以外,我們可以通過繼承 java.lang.ClassLoader類的方式實現(xiàn)自己的類加載器,以滿足一些特殊的需求。

除了引導(dǎo)類加載器之外,所有的類加載器都有一個父類加載器。這種父子關(guān)系構(gòu)成了類加載器的層次結(jié)構(gòu)。

對于系統(tǒng)提供的類加載器來說,應(yīng)用程序類加載器的父類加載器是擴展類加載器,而擴展類加載器的父類加載器是引導(dǎo)類加載器。

因為類加載器 Java 類如同其它的 Java 類一樣,也是要由類加載器來加載的。對于開發(fā)人員編寫的類加載器來說,其父類加載器是加載此類加載器 Java 類的類加載器。

這種類加載器之間的層次關(guān)系,稱為類加載器的雙親委派模型:

image.png

注意,上圖中的樹狀結(jié)構(gòu)并不意味著繼承關(guān)系,而是使用委托實現(xiàn)的。

雙親委派模型的工作過程是:如果一個類加載器收到了類加載的請求,他會首先把這個請求委托給自己的父類加載器去完成,每一層次的加載器都是如此,最后所有的類加載請求最終都會傳遞到頂層的引導(dǎo)類加載器中去,只有當(dāng)父類加載器無法完成這個加載請求(所請求加載的類不在 他加載的范圍內(nèi))時,子類加載器會嘗試自己加載。

雙親委派機制保證了java核心類庫的安全,如果嘗試加載與rt.jar類庫中已有的類重名的java類,該類永遠(yuǎn)無法被加載運行,因為請求被傳遞到引導(dǎo)類加載器之后,引導(dǎo)類加載器會返回加載到的rt.jar中的類。

我們觀察一下雙親委派機制的實現(xiàn):

首先看一下ClassLoader中的方法:

findLoadedClass:每個類加載器都維護有自己的一份已加載類名字空間,其中不能出現(xiàn)兩個同名的類。凡是通過該類加載器加載的類,無論是直接的還是間接的,都保存在自己的名字空間中,該方法就是在該名字空間中尋找指定的類是否已存在,如果存在就返回給類的引用,否則就返回 null。這里的直接是指,存在于該類加載器的加載路徑上并由該加載器完成加載,間接是指,由該類加載器把類的加載工作委托給其他類加載器完成類的實際加載。

getSystemClassLoader:Java2 中新增的方法。該方法返回系統(tǒng)使用的 ClassLoader??梢栽谧约憾ㄖ频念惣虞d器中通過該方法把一部分工作轉(zhuǎn)交給系統(tǒng)類加載器去處理。

defineClass:該方法是 ClassLoader 中非常重要的一個方法,它接收以字節(jié)數(shù)組表示的類字節(jié)碼,并把它轉(zhuǎn)換成 Class 實例,該方法轉(zhuǎn)換一個類的同時,會先要求裝載該類的父類以及實現(xiàn)的接口類。

loadClass:加載類的入口方法,調(diào)用該方法完成類的顯式加載。通過對該方法的重新實現(xiàn),我們可以完全控制和管理類的加載過程。

findClass(String name): 查找名稱為 name的類,返回的結(jié)果是 java.lang.Class類的實例。

resolveClass(Class<?> c): 鏈接指定的 Java 類。

實現(xiàn)雙親委派機制的代碼集中在ClassLoader的loadClass方法中。

 protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                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.
                    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;
        }
    }

先檢查是否已經(jīng)加載過,若沒有則調(diào)用父類加載器的loadClass方法,若父類加載器為空則默認(rèn)使用啟動類加載器作為父加載器。如果父類加載器加載失敗,拋出ClassNotFoundException異常后,則調(diào)用自己的findClass方法進行加載。

雙親委托機制的不足

雙親委派機制很好的解決了各個類加載器的基礎(chǔ)類統(tǒng)一的問題,基礎(chǔ)類總是作為被用戶代碼調(diào)用的API(比如rt.jar中的類)。但是如果基礎(chǔ)類要調(diào)用用戶的代碼時會發(fā)生什么?

首先要搞明白一點:當(dāng)我們使用 new 關(guān)鍵字或者 Class.forName 來加載類時,所要加載的類都是由調(diào)用 new 或者 Class.forName 的類的類加載器進行加載的。比如我們使用JDBC標(biāo)準(zhǔn)接口時,JDBC標(biāo)準(zhǔn)接口存在于rt.jar中,在這個接口中又需要調(diào)用各個數(shù)據(jù)庫廠商提供的jdbc驅(qū)動程序來達(dá)到管理驅(qū)動的目的,這些驅(qū)動程序的jar包一般置于claspath路徑下。問題出現(xiàn)了:JDBC標(biāo)準(zhǔn)接口是由引導(dǎo)類加載器加載的,故在這些接口中調(diào)用classpath路徑下的jdbc驅(qū)動代碼時,也會嘗試使用引導(dǎo)類加載器進行加載。但是引導(dǎo)類加載器根本不可能認(rèn)識這些代碼(只負(fù)責(zé)rt.jar)。

為了解決這個問題,引入了線程上下文類加載器。

這個類加載器可以通過java.lang.Thread類的setContextClassLoader()方法進行設(shè)置,如果創(chuàng)建線程時沒有設(shè)置,將會從父線程中繼承一個,如果在應(yīng)用程序的全局范圍內(nèi)都沒有設(shè)置,那么這個類加載器默認(rèn)就是應(yīng)用程序類加載器。

使用java.lang.Thread.getContextClassLoader()可以獲得線程上下文類加載器,故可以使用這個加載器加載classpath路徑下的代碼,也就是父類加載器請求子類加載器完成類加載動作,破壞了雙親委托模型。

實現(xiàn)自己的類加載器

上面提到的系統(tǒng)提供的類加載器在大多數(shù)情況下可以滿足我們的需求,但是在某些情況下,我們需要開發(fā)自己的類加載器,比如,加載網(wǎng)絡(luò)傳輸?shù)玫降念愖止?jié)碼、對字節(jié)碼進行加密解碼、加載運行時生成的字節(jié)碼、實現(xiàn)類的熱替換等。這些情況下類的字節(jié)碼僅僅依靠上述的三種系統(tǒng)類加載器是無法加載的。

我自己實現(xiàn)了一些測試代碼,現(xiàn)在將他們貼到這里,順便對前面的總結(jié)做一個印證。下面的幾個類都位于包space.kyu下面:

class MyClassLoader1 extends ClassLoader{
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        try {
            String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
            InputStream stream = getClass().getResourceAsStream(fileName);
            if (stream == null) {
//              System.out.println("ClassLoader load class" + name);
                return super.loadClass(name);
            }
            byte[] bs = new byte[stream.available()];
            stream.read(bs);
//          System.out.println("MyClassLoader1 load class: " + name);
            return defineClass(name, bs, 0, bs.length);
        } catch (IOException e) {
            throw new ClassNotFoundException(name);
        }
    }
}

public class MyClassLoader2 extends ClassLoader {
    public Class<?> loadDirectly(String name) throws ClassNotFoundException {
        try {
            String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
            InputStream stream = getClass().getResourceAsStream(fileName);
            if (stream == null) {
//              System.out.println("ClassLoader load class" + name);
                return super.loadClass(name);
            }
            byte[] bs = new byte[stream.available()];
            stream.read(bs);
//          System.out.println("MyClassLoader2 load class: " + name);
            return defineClass(name, bs, 0, bs.length);
        } catch (IOException e) {
            throw new ClassNotFoundException(name);
        }
    }
}

public interface Operation {
    void doSomething();
}

public class Test {
    public String str;
    public Test(String str) {
        this.str = str;
    }
    
    public void test() {
        System.out.println(str);
    }
}

public class TestClass implements Operation{
    public Test test;
    @Override
    public void doSomething() {
        System.out.println("hello");
    }
    
    public Test test(){
        test = new Test("haha");
        System.out.println(test.str);
        return test;
    }
}

public class ClassLocaderTest {
    public static void main(String[] args) {
        Object testClassLoader1 = getMyClassLoader1();
        Object testClassLoader2 = getMyClassLoader2();
        System.out.println("*****************testClassLoader1*******************");
        printClassLoader(testClassLoader1);
        reflectInvoke(testClassLoader1);
        interfaceInvoke(testClassLoader1);
        System.out.println("*****************testClassLoader2*******************");
        printClassLoader(testClassLoader2);
        reflectInvoke(testClassLoader2);
        interfaceInvoke(testClassLoader2);
        
    }

    static void printClassLoader(Object object) {
        System.out.println("*********printClassLoader:");
        ClassLoader classLoader = object.getClass().getClassLoader();
        while (classLoader != null) {
            System.out.println(classLoader);
            classLoader = classLoader.getParent();
        }
    }

    static void reflectInvoke(Object obj) {
        System.out.println("*********reflectInvoke:");
        try {
            Method test = obj.getClass().getMethod("test", new Class[] {});
            test.invoke(obj, new Object[] {});
            Method doSomething = obj.getClass().getMethod("doSomething", new Class[] {});
            doSomething.invoke(obj, new Object[] {});
        } catch (InvocationTargetException e) {
            Throwable t = e.getTargetException();// 獲取目標(biāo)異常
            System.out.println(t);
        } catch (Exception e) {
            System.out.println(e);
        }
    }
    
    static void interfaceInvoke(Object obj) {
        System.out.println("*********interfaceInvoke:");
        try {
            Operation operation = (Operation) obj;
            operation.doSomething();
        } catch (Exception e) {
            System.out.println(e);
        }
    }

    static Object getMyClassLoader1() {
        Object obj = null;
        try {
            MyClassLoader1 loader = new MyClassLoader1();
            obj = loader.loadClass("space.kyu.TestClass").newInstance();
        } catch (Exception e) {
            System.out.println(e);
        }
        return obj;
    }

    static Object getMyClassLoader2() {
        Object obj = null;
        try {
            MyClassLoader2 loader = new MyClassLoader2();
            obj = loader.loadDirectly("space.kyu.TestClass").newInstance();
        } catch (Exception e) {
            System.out.println(e);
        }
        return obj;
    }
}

上述六個類位于space.kyu下不同的類文件當(dāng)中。ClassLocaderTest運行結(jié)果:

*****************testClassLoader1*******************
*********printClassLoader:
space.kyu.MyClassLoader1@76e2d0ab
sun.misc.Launcher$AppClassLoader@52a53948
sun.misc.Launcher$ExtClassLoader@5d53d05b
*********reflectInvoke:
haha
hello
*********interfaceInvoke:
java.lang.ClassCastException: space.kyu.TestClass cannot be cast to space.kyu.Operation
*****************testClassLoader2*******************
*********printClassLoader:
space.kyu.MyClassLoader2@6c618821
sun.misc.Launcher$AppClassLoader@52a53948
sun.misc.Launcher$ExtClassLoader@5d53d05b
*********reflectInvoke:
haha
hello
*********interfaceInvoke:
hello

一般來說,我們自己開發(fā)的類加載器只要繼承ClassLoader并覆蓋findClass方法即可。這樣的話就會自動使用雙親委派機制,我們可以在findClass方法中填寫我們自己的加載邏輯:從網(wǎng)絡(luò)上或者是硬盤上加載一個類的字節(jié)碼。

上面的例子中并沒有使用這個套路,MyClassLoader1直接復(fù)寫loadClass方法,MyClassLoader2添加了方法loadDirectly,如果不這樣做的話,我們在加載space.kyu.TestClass這個類的時候,因為這個類在classpath上,由于雙親委派機制,這個類會被應(yīng)用程序類加載器先進行加載,達(dá)不到測試的效果。

  • 觀察上面printClassLoader部分,通過getParent方法打印了類加載器的層次結(jié)構(gòu)??梢婋m然我們并未顯示指定這兩個自定義加載器的父類加載器,但是他們的父類加載器已經(jīng)被默認(rèn)設(shè)置為sun.misc.Launcher$AppClassLoader,也就是加載這兩個個自定義類加載器所使用的加載器。印證上面的結(jié)論:對于開發(fā)人員編寫的類加載器來說,其父類加載器是加載此類加載器 Java 類的類加載器。

  • reflectInvoke方法是使用反射機制調(diào)用了加載出來類的方法,如果去掉上面自定義類加載器中注掉的System.out方法,就會看到,在反射調(diào)用TestClass的test方法的時候,類加載器加載了space.kyu.Test這個類,并且加載他的類加載器正是我們自定義的類加載器,印證了我們上面的結(jié)論:當(dāng)我們使用 new 關(guān)鍵字或者 Class.forName 來加載類時,所要加載的類都是由調(diào)用 new 或者 Class.forName 的類的類加載器進行加載的

  • 思考上面的反射用法,為什么不直接將getMyClassLoader1()方法返回的Object對象強轉(zhuǎn)為space.kyu.TestClass呢?比如這樣:

    space.kyu.TestClass testClass = (TestClass)getMyClassLoader1();

    編譯并沒有問題,但是在運行時就會報錯:java.lang.ClassCastException

    為什么會出現(xiàn)這樣的結(jié)果呢?其實從這篇文章的一開始就已經(jīng)演示過了。space.kyu.TestClass testClass這個類是通過應(yīng)用程序類加載器加載的,而getMyClassLoader1()方法得到的是我們自定義類加載器加載的類,這兩個類是不相等的(雖然名字相同),所以強轉(zhuǎn)失敗。

  • 接下來看interfaceInvoke這部分。將自定義類加載器加載得到的對象強轉(zhuǎn)為了接口類型。注意到,MyClassLoader1加載的類對象在強轉(zhuǎn)時拋出異常,而MyClassLoader2可以正常強轉(zhuǎn)并調(diào)用接口方法。

    MyClassLoader1加載的類為什么強轉(zhuǎn)失敗?原因在于,MyClassLoader1在加載TestClass類時,觸發(fā)其父類接口Operation的加載,此時默認(rèn)使用MyClassLoader1加載Operation類。在MyClassLoader1中我們覆蓋了loadClass方法,故加載Operation時也會調(diào)用我們自己實現(xiàn)的loadClass方法進行加載。

    同樣的,MyClassLoader2在加載TestClass類時,也觸發(fā)其父類接口Operation的加載,此時默認(rèn)使用MyClassLoader2加載Operation類。不同之處在于我們并未覆蓋loadClass方法,加載Operation時調(diào)用了ClassLoader中的loadClass方法,在這個方法的實現(xiàn)中,由應(yīng)用程序類加載器加載了Operation類。

    所以,出現(xiàn)上面結(jié)果的原因也就一目了然了。

類加載器與熱替換

普通的java應(yīng)用中不能實現(xiàn)類的熱替換的原因在于同名類的不同版本的實例不能共存,因為使用了默認(rèn)的類加載機制后,一個類只會被加載一次,再次請求加載時直接返回之前加載的緩存(findLoadedClass)。故我們重新編譯生成
的class文件并不會被重新讀取并加載。

為了繞過這個加載機制,我們可以通過不同的類加載器來加載該類的不同版本。

在space.kyu包下面新增一個類HotSwapTest:

public class HotSwapTest {
    public static void main(String[] args) {
        Timer timer = new Timer(false);
        TimerTask task = new TimerTask() {
            public void run() {
                update();
            }
        };
        timer.schedule(task, 1000, 2000);
    }

    public static void update() {
        try {
            MyClassLoader2 loader = new MyClassLoader2();
            Object obj = loader.loadDirectly("space.kyu.TestClass").newInstance();
            Method doSomething = obj.getClass().getMethod("doSomething", new Class[] {});
            doSomething.invoke(obj, new Object[] {});
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

在HotSwapTest類中,我們模擬了一個定時升級的任務(wù):每隔兩秒執(zhí)行一次升級,實例化一個MyClassLoader2對象并使用該類加載器加載space.kyu.TestClass,反射調(diào)用其doSomething方法打印字符串。

編譯并運行HotSwapTest,運行過程中,每隔兩秒doSomething便打印字符串"hello",此時修改space.kyu.TestClass源碼,將打印字符串替換為"world",CTRL+S,我們的程序并未停止,但是下一次打印出的字符串已然不同了:

hello
hello
hello
hello
hello
world
world
world

上面就是一個簡單的熱替換的例子。實際的應(yīng)用中當(dāng)然不是通過一個定時任務(wù)進行升級的。把新版本類的字節(jié)碼通過網(wǎng)絡(luò)傳輸?shù)椒?wù)器上去,然后發(fā)送一個升級指令,使用上面類似的方法便可對類進行升級。

參考

Java 類的熱替換 —— 概念、設(shè)計與實現(xiàn)

深入探討 Java 類加載器

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容