jvm類加載器詳解和如何打破雙親委派機(jī)制

類加載過程:

項(xiàng)目啟動(dòng)的時(shí)候,并不是加載項(xiàng)目中的所有類,是在使用的時(shí)候加載,類加載器加載類的時(shí)候首先加載父類,所以O(shè)bject類一定先被加載.
類加載器
AppClassLoader:
應(yīng)用類加載器,又稱為系統(tǒng)類加載器,負(fù)責(zé)在JVM啟動(dòng)時(shí),加載來自在命令java中的classpath或者java.class.path系統(tǒng)屬性或者CLASSPATH操作系統(tǒng)屬性所指定的JAR類包和類路徑.
ExtClassLoade:
稱為擴(kuò)展類加載器,主要負(fù)責(zé)加載Java的擴(kuò)展類庫,默認(rèn)加載JAVA_HOME/jre/lib/ext/目錄下的所有jar包或者由java.ext.dirs系統(tǒng)屬性指定的jar包.放入這個(gè)目錄下的jar包對(duì)AppClassLoader加載器都是可見的(因?yàn)镋xtClassLoader是AppClassLoader的父加載器,并且Java類加載器采用了委托機(jī)制).
BootstrapClassloader:
是Java類加載層次中最頂層的類加載器,負(fù)責(zé)加載JDK中的核心類庫,如:rt.jar、resources.jar、charsets.jar等
加載過程
C++先調(diào)用sun.misc包下的Launcher類,在初始化中賦值,Launcher為單例模式,初始化AppClassLoader和ExtClassLoader,并將AppClassLoader的parent 賦值為ExtClassLoader,將ExtClassLoader的parent賦值為空,因?yàn)镋xtClassLoader的parent是BootstrapClassloader,但是BootstrapClassloader是C語言編寫,所以為null(父加載器并不是繼承關(guān)系).
繼承圖


image.png

首先虛擬機(jī)調(diào)用Launcher構(gòu)造函數(shù),初始化類加載器, ClassLoader的初始化,跟進(jìn)源碼發(fā)現(xiàn)AppClassLoader的parent時(shí)ExtClassLoader, ExtClassLoader的parent是null


image.png
image.png

圖上可以看到,首先調(diào)用父類的loadClass類,父類為null,則是bootStrap類加載器,如果返回的c為null,證明父類沒有加載到,則調(diào)用自己的findClass

LoadClass的類加載過程

引用(https://baijiahao.baidu.com/s?id=1636309817155065432&wfr=spider&for=pc)
加載->驗(yàn)證->準(zhǔn)備->解析->初始化->使用->卸載

加載

”加載“是”類加機(jī)制”的第一個(gè)過程,在加載階段,虛擬機(jī)主要完成三件事:
(1)通過一個(gè)類的全限定名來獲取其定義的二進(jìn)制字節(jié)流
(2)將這個(gè)字節(jié)流所代表的的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)
(3)在堆中生成一個(gè)代表這個(gè)類的Class對(duì)象,作為方法區(qū)中這些數(shù)據(jù)的訪問入口。
相對(duì)于類加載的其他階段而言,加載階段是可控性最強(qiáng)的階段,因?yàn)槌绦騿T可以使用系統(tǒng)的類加載器加載,還可以使用自己的類加載器加載。我們?cè)谧詈笠徊糠謺?huì)詳細(xì)介紹這個(gè)類加載器。在這里我們只需要知道類加載器的作用就是上面虛擬機(jī)需要完成的三件事,僅此而已就好了。

驗(yàn)證

驗(yàn)證的主要作用就是確保被加載的類的正確性。也是連接階段的第一步。說白了也就是我們加載好的.class文件不能對(duì)我們的虛擬機(jī)有危害,所以先檢測(cè)驗(yàn)證一下。他主要是完成四個(gè)階段的驗(yàn)證:
(1)文件格式的驗(yàn)證:驗(yàn)證.class文件字節(jié)流是否符合class文件的格式的規(guī)范,并且能夠被當(dāng)前版本的虛擬機(jī)處理。這里面主要對(duì)魔數(shù)、主版本號(hào)、常量池等等的校驗(yàn)(魔數(shù)、主版本號(hào)都是.class文件里面包含的數(shù)據(jù)信息、在這里可以不用理解)。
(2)元數(shù)據(jù)驗(yàn)證:主要是對(duì)字節(jié)碼描述的信息進(jìn)行語義分析,以保證其描述的信息符合java語言規(guī)范的要求,比如說驗(yàn)證這個(gè)類是不是有父類,類中的字段方法是不是和父類沖突等等。
(3)字節(jié)碼驗(yàn)證:這是整個(gè)驗(yàn)證過程最復(fù)雜的階段,主要是通過數(shù)據(jù)流和控制流分析,確定程序語義是合法的、符合邏輯的。在元數(shù)據(jù)驗(yàn)證階段對(duì)數(shù)據(jù)類型做出驗(yàn)證后,這個(gè)階段主要對(duì)類的方法做出分析,保證類的方法在運(yùn)行時(shí)不會(huì)做出威海虛擬機(jī)安全的事。
(4)符號(hào)引用驗(yàn)證:它是驗(yàn)證的最后一個(gè)階段,發(fā)生在虛擬機(jī)將符號(hào)引用轉(zhuǎn)化為直接引用的時(shí)候。主要是對(duì)類自身以外的信息進(jìn)行校驗(yàn)。目的是確保解析動(dòng)作能夠完成。
對(duì)整個(gè)類加載機(jī)制而言,驗(yàn)證階段是一個(gè)很重要但是非必需的階段,如果我們的代碼能夠確保沒有問題,那么我們就沒有必要去驗(yàn)證,畢竟驗(yàn)證需要花費(fèi)一定的的時(shí)間。當(dāng)然我們可以使用-Xverfity:none來關(guān)閉大部分的驗(yàn)證。

準(zhǔn)備

準(zhǔn)備階段主要為類變量分配內(nèi)存并設(shè)置初始值。這些內(nèi)存都在方法區(qū)分配。在這個(gè)階段我們只需要注意兩點(diǎn)就好了,也就是類變量和初始值兩個(gè)關(guān)鍵詞:
(1)類變量(static)會(huì)分配內(nèi)存,但是實(shí)例變量不會(huì),實(shí)例變量主要隨著對(duì)象的實(shí)例化一塊分配到j(luò)ava堆中,
(2)這里的初始值指的是數(shù)據(jù)類型默認(rèn)值,而不是代碼中被顯示賦予的值。比如
public static int value = 1; //在這里準(zhǔn)備階段過后的value值為0,而不是1。賦值為1的動(dòng)作在初始化階段。
當(dāng)然還有其他的默認(rèn)值。


image.png

注意,在上面value是被static所修飾的準(zhǔn)備階段之后是0,但是如果同時(shí)被final和static修飾準(zhǔn)備階段之后就是1了。我們可以理解為static final在編譯器就將結(jié)果放入調(diào)用它的類的常量池中了。

解析

解析階段主要是虛擬機(jī)將常量池中的符號(hào)引用轉(zhuǎn)化為直接引用的過程。什么是符號(hào)應(yīng)用和直接引用呢?
符號(hào)引用:以一組符號(hào)來描述所引用的目標(biāo),可以是任何形式的字面量,只要是能無歧義的定位到目標(biāo)就好,就好比在班級(jí)中,老師可以用張三來代表你,也可以用你的學(xué)號(hào)來代表你,但無論任何方式這些都只是一個(gè)代號(hào)(符號(hào)),這個(gè)代號(hào)指向你(符號(hào)引用)直接引用:直接引用是可以指向目標(biāo)的指針、相對(duì)偏移量或者是一個(gè)能直接或間接定位到目標(biāo)的句柄。和虛擬機(jī)實(shí)現(xiàn)的內(nèi)存有關(guān),不同的虛擬機(jī)直接引用一般不同。解析動(dòng)作主要針對(duì)類或接口、字段、類方法、接口方法、方法類型、方法句柄和調(diào)用點(diǎn)限定符7類符號(hào)引用進(jìn)行。

初始化

這是類加載機(jī)制的最后一步,在這個(gè)階段,java程序代碼才開始真正執(zhí)行。我們知道,在準(zhǔn)備階段已經(jīng)為類變量賦過一次值。在初始化階端,程序員可以根據(jù)自己的需求來賦值了。一句話描述這個(gè)階段就是執(zhí)行類構(gòu)造器< clinit >()方法的過程。
在初始化階段,主要為類的靜態(tài)變量賦予正確的初始值,JVM負(fù)責(zé)對(duì)類進(jìn)行初始化,主要對(duì)類變量進(jìn)行初始化。在Java中對(duì)類變量進(jìn)行初始值設(shè)定有兩種方式:
①聲明類變量是指定初始值
②使用靜態(tài)代碼塊為類變量指定初始值
JVM初始化步驟
1、假如這個(gè)類還沒有被加載和連接,則程序先加載并連接該類
2、假如該類的直接父類還沒有被初始化,則先初始化其直接父類
3、假如類中有初始化語句,則系統(tǒng)依次執(zhí)行這些初始化語句
類初始化時(shí)機(jī):只有當(dāng)對(duì)類的主動(dòng)使用的時(shí)候才會(huì)導(dǎo)致類的初始化,類的主動(dòng)使用包括以下六種:
創(chuàng)建類的實(shí)例,也就是new的方式訪問某個(gè)類或接口的靜態(tài)變量,或者對(duì)該靜態(tài)變量賦值調(diào)用類的靜態(tài)方法反射(如 Class.forName(“com.shengsiyuan.Test”))初始化某個(gè)類的子類,則其父類也會(huì)被初始化Java虛擬機(jī)啟動(dòng)時(shí)被標(biāo)明為啟動(dòng)類的類( JavaTest),直接使用 java.exe命令來運(yùn)行某個(gè)主類

雙親委派

上面提到了3中默認(rèn)類加載器,看一下他們之間的關(guān)系


image.png

下面兩張圖解釋getSystemClassLoader的返回值是AppClassLoader


image.png
image.png

l.getClassLoader()在文章的最開始可知,返回的就是AppClassLoader,
那么getSystemClassLoader的返回值是AppClassLoader也就證明了系統(tǒng)默認(rèn)的類加載器為AppClassLoader

protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
//首先判斷類是否已經(jīng)被加載
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
//如果parent不為空則調(diào)用parent的classLoader方法
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
//直到調(diào)用到BootstrapClassCladdLoader,使用頂級(jí)的類加載器
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }
                //如果沒加載調(diào)用自己的findClass,并返回,由下一級(jí)的類加載器繼續(xù)處理
                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;
        }
    }

上面就是雙親委派的核心,先調(diào)用AppClassLoader判斷類是否被加載而不是直接BootstrapClassloader 加載(這樣設(shè)計(jì)應(yīng)該是絕大部分的類加載是我們自己類路徑下的類,核心包和擴(kuò)展包下的類少),在一級(jí)一級(jí)向上調(diào)用,如果沒有再一級(jí)一級(jí)向下調(diào)用
這樣做是 沙箱安全機(jī)制和避免類被重復(fù)加載.
沙箱安全機(jī)制: 保證核心包不能被篡改
避免類被重復(fù)加載:保證類只被加載一次

比如我這里定義Ineger類,包名也是java.lang,發(fā)現(xiàn)是不能使用print方法的,因?yàn)槭褂玫倪€是rt.jar包下的類,
讀者還可以嘗試,定義main方法,直接執(zhí)行,發(fā)現(xiàn)也是不行的.


image.png

全盤委托機(jī)制:
當(dāng)類被一個(gè)ClassLoader加載的時(shí)候,該類所依賴的類也將由該ClassLoader加載,除非顯示的調(diào)用要使用哪個(gè)類加載器.當(dāng)然核心類和擴(kuò)展類還是由各自的類加載器加載.
這句話的理解呢,就是A類中由成員變量B,A是被自定義類加載器LoaderA加載的,那B也是由LoaderA加載

自定義類加載器

自定義MyClassLoaser重寫findClass方法,當(dāng)調(diào)用ClassLoader的loadClass方法時(shí),會(huì)調(diào)用自己重寫的findClass方法,從而達(dá)到自定義的目的

public class MyClassLoader extends ClassLoader{
    String classPath;
    public MyClassLoader(String classPath){
        this.classPath = classPath;
    }

    @SneakyThrows
    @Override
//重寫findClass方法
    protected Class<?> findClass(String name) throws ClassNotFoundException{
        byte[] bytes = loadByte(name, classPath);
        Class<?> aClass = defineClass(name, bytes, 0, bytes.length);
        return aClass;
    }

    private byte[] loadByte (String name,String classPath) throws IOException{
        name = name.replaceAll("\\.", "/");
         FileInputStream fis = new FileInputStream(classPath + "/" + name+ ".class");
         int len = fis.available();
         byte[] data = new byte[len];
         fis.read(data);
         fis.close();
         return data;
    }
}

代碼準(zhǔn)備好了,下面說明如何達(dá)到自定義類加載目的
通過debug發(fā)現(xiàn)當(dāng)用MyClassLoader加載類的時(shí)候,MyClassLoader的parent時(shí)AppClassLoader,


image.png

根據(jù)雙親委派機(jī)制,AppClassLoader->ExtClassLoader-> BootstrapClassloader向上委托,
先有BootstrapClassloader加載,沒有,再ExtClassLoader沒有,再AppClassLoader發(fā)現(xiàn)有User.class則加載到方法區(qū),不會(huì)再用MyClassLoader來加載.下圖是在target有User.class的情況下,這里稍微解釋一下自定義的MyClassLoader是要去D盤下的test目錄去找User.class類,target是項(xiàng)目本身存在class文件,也就是AppClassLoader要去加載的類


image.png

將target的Use.class去掉的時(shí)候AppClassLoader找不到User.class類,那么類加載器ClassLoader已經(jīng)變?yōu)镸yClassLoader


image.png

打破雙親委派機(jī)制

錯(cuò)誤示范 打破雙親委派需要重寫loadClass方法,因?yàn)榧虞dUser類之前要加載Object類,但是在D:\test\java\lang目錄下沒有Object.class類文件,當(dāng)我們手動(dòng)的將Object.class類放到D:\test\java\lang的時(shí)候,依然會(huì)報(bào)錯(cuò),因?yàn)榧虞d父類的時(shí)候默認(rèn)使用的還是加載本類的類加載器,所以在加載Object.class類的時(shí)候依然使用的是MyClassLoader類加載器,因?yàn)榇蚱屏穗p親委派,所以不能向上委托,jvm是不允許我們自己加載核心類的(沙箱安全機(jī)制)


image.png
image.png
image.png

因?yàn)樯诚錂C(jī)制,就算存放Object.class類之后依然報(bào)錯(cuò),因?yàn)檫@種核心類必須是rt.jar下才可以


image.png

如何解決上面的問題呢,就是除了加載我們自己的類時(shí)候,打破雙親委派,其余的類包括擴(kuò)展類和核心類都有jvm自己去加載


image.png

那在實(shí)際項(xiàng)目中我們也很少的去打破雙親委派機(jī)制,除非像tomcat那樣的中間件才有這個(gè)需求
那真的想要打破雙親委派機(jī)制,可以用spi技術(shù),下篇文章繼續(xù)分析.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。