類加載機制(硬核詳解)


之前學JAVA高併發的時候有一定的瞭解過類加載機制。
在這裏插入圖片描述
那麼加載到方法區裏面去的呢?

一、類生命週期

我們得從類生命週期開始說起,類的生命週期總共分七歩,接下來我們先了解下從加載到初始化。如下圖
在這裏插入圖片描述

二、類加載器

在java中其實專門有一個東西負責裝入類,它叫做類加載器

類加載器負責裝入類,搜索網絡、jar、zip文件夾、二進制數據、內存類等指定位置的類資源。

一個java程序運行,至少需要三個類加載器實例,負責加載不同的類的加載,那怎麼去理解至少需要三個類加載器實例呢?

這個我覺得也很好理解,比如我們後端程序員不可能讓我們去做行政,程序員鼓勵師不能去敲代碼,每個人的工作分工不同,要各司其職,這樣的話才能更好的去維護,更好的去管理,在java中也是一樣的。如下圖

在這裏插入圖片描述

  1. Bootstrap loader核心類庫加載器

    核心類庫加載器是由C語言寫的,什麼叫核心類庫呢?如我們的Object它就是一個類,它就是用C語言寫的,爲什麼要用C語
    言寫呢?因爲java虛擬機最開始是由C語言實現的,先有了C然後再有了java,少了它就無法運行的類就叫核心類庫,對應
    JRE_HOME/jre/lib目錄

  2. Extension Class loader擴展類庫加載器

    擴展類庫加載器在新的版本中有一些調整,這個先放一邊,畢竟也不是很大的調整,它是專門加載JRE_HOME/jre/lib/ext
    目錄下的擴展類庫的,什麼叫擴展類庫呢?就是說這個類可能在某些平臺上沒有的類,就是說不是缺它不可的類,比如
    我們談戀愛,沒有女朋友不行,但是發現真的少了女朋友照樣過得很瀟灑,就是這麼一回事。

  3. Appliction class loader用戶應用程序加載器

    應用程序加載器是爲了加載我們開發人員寫的代碼,那它是怎麼知道我們寫的代碼在哪些位置呢?有一個java.class.path
    參數,用戶應用程序加載器通過加載這個參數,就能得到對應的目錄

三、驗證問題

1、 如何查看類對應的加載器?

我們可以通過JDK-API查看:java.lang.Class.getClassloader()返回類的類加載器;如果這個類是BootstrapLoader
加載的,那麼這個方法在這種實現中就會返回null。
demo
/**
 * 查看類的加載器實例
 */
public class ClassLoaderView {
    public static void main(String[] args) throws Exception {
        // 加載核心類庫的 BootStrap ClassLoader
        System.out.println(" 核心類庫加載器:"
                + ClassLoaderView.class.getClassLoader().loadClass("java.lang.String").getClassLoader());
        System.out.println(" 核心類庫加載器:"
                + ClassLoaderView.class.getClassLoader().loadClass("java.lang.Object").getClassLoader());

        
        // 加載拓展庫的 Extension ClassLoader
        System.out.println("拓展類庫加載器:" + ClassLoaderView.class.getClassLoader()
                .loadClass("com.sun.nio.zipfs.ZipCoder").getClassLoader());
        // 加載應用程序的
        System.out.println("應用程序庫加載器:" + ClassLoaderView.class.getClassLoader());


        // 雙親委派模型 Parents Delegation Model
        System.out.println("應用程序庫加載器的父類:" + ClassLoaderView.class.getClassLoader().getParent());
        System.out.println(
                "應用程序庫加載器的父類的父類:" + ClassLoaderView.class.getClassLoader().getParent().getParent());
    }
}

運行結果

核心類庫加載器:null
核心類庫加載器:null
拓展類庫加載器:sun.misc.Launcher$ExtClassLoader@3e3abc88
應用程序庫加載器:sun.misc.Launcher$AppClassLoader@18b4aac2
應用程序庫加載器的父類:sun.misc.Launcher$ExtClassLoader@3e3abc88
應用程序庫加載器的父類的父類:null

2、JVM如何知道我們的類在何方?

查找AppClassLoader可以看到如下代碼段,讀取了java.classs.path參數,所以發現jvm還是比較傻的。

在這裏插入圖片描述

還可以利用jps、jcmd兩個命令進行驗證.
一、使用jmcd命令進行驗證,運行如下代碼
package classloader_demo1;

import java.io.IOException;

public class Main {

    public static void main(String[] args) throws IOException {
        System.out.println("Hello World!");
        System.in.read();//這段代碼會阻塞
    }
}

打開命名窗口
在這裏插入圖片描述
然後運行jcmd help命令
在這裏插入圖片描述
找到對應main函數的ID運行:jcmd 17389 help看看支持哪些命令
在這裏插入圖片描述

可以看到有非常多的命令,可以看到有很多命令,GC.heap_dump可以查看堆信息、VM.flags可以看到jvm的參數配置等等…,這次我們就先看看jvm系統參數的配置,運行jcmd 17389 VM.system_propertites命令
在這裏插入圖片描述

如下圖可以看到自己類的信息有這麼多,那麼爲什麼有這麼多呢,其實是idea開發工具幫我們做了了很多的配置,有了這些配置信息jvm就能找到我們程序編譯之後的目錄,進而運行我們的代碼。
在這裏插入圖片描述

3、類會不會重複加載?

那麼類會不會重複加載呢?答案肯定是否定的。

類不會重複加載

類的唯一性:同一個加載器,類名一樣,代表是同一個類。
識別方式:Classloader Insance id + PackgeName + ClassName
驗證方式:使用類加載器,對同一個class類的不同版本,進行多次加載,檢查是否會加載到最新的代碼。


①、新建HelloService類加載測試類
/** 類加載測試類 */
public class HelloService {

    public static String value = getValue();

    static {
        System.out.println("靜態代碼塊運行");
    }

    private static String getValue() {
        System.out.println("靜態方法被運行");
        return "netease";
    }

    public void test() {
        System.out.println("hello.." + value);
    }
}

②、新建LoaderTest類
package classloader_demo1;

import java.net.URL;
import java.net.URLClassLoader;

/**
 * 指定class 進行加載
 */
public class LoaderTest {
    public static void main(String[] args) throws Exception {
        //指定jvm查找類的位置
        URL classUrl = new URL("file:///ideaWorkspace/");

        URLClassLoader loader = new URLClassLoader(new URL[]{classUrl});

        while (true) {
            if(loader == null){
                break;
            }
            // 問題:靜態塊什麼時候觸發?
            Class clazz = loader.loadClass("HelloService");
            System.out.println("HelloService所使用的類加載器:" + clazz.getClassLoader());

            //反射創建對象
            Object newInstance = clazz.newInstance();
            //調用test方法
            Object value = clazz.getMethod("test").invoke(newInstance);
            System.out.println("調用getValue獲得的返回值爲:" + value);

            Thread.sleep(3000L);
            System.out.println();

        }
    }
}


運行結果

HelloService所使用的類加載器:java.net.URLClassLoader@1c20c684
靜態方法被運行
靜態代碼塊運行
hello..netease
調用getValue獲得的返回值爲:null

HelloService所使用的類加載器:java.net.URLClassLoader@1218025c
靜態方法被運行
靜態代碼塊運行
hello..netease
調用getValue獲得的返回值爲:null

代碼是一直循環創建類加載器創建對象的,接下來我們修改HelloService中的代碼,加了幾個1
在這裏插入圖片描述
重新編譯

HelloService所使用的類加載器:java.net.URLClassLoader@1218025c
靜態方法被運行
靜態代碼塊運行
hello..netease
調用getValue獲得的返回值爲:null

運行結果還是一樣,進一步的證明了類不會重複加載

4、類如何卸載?

類被卸載需要滿足兩個條件
①:該Class所有的實例都已經被GC銷燬;
②:加載該類的所有ClassLoader實例都已經被GC;
驗證方式:jvm啓動參數中增加-verbose:class參數,輸出加載和卸載的日誌信息,同手手動的觸發GC的操作

下面開始進行驗證:

首先給我們運行的類加上-verbose:class參數,加上字後就能看到日誌信息了

在這裏插入圖片描述
修改LoaderTest代碼

package classloader_demo1;

import java.net.URL;
import java.net.URLClassLoader;

/**
 * 指定class 進行加載
 */
public class LoaderTest {
    public static void main(String[] args) throws Exception {
        //指定jvm查找類的位置
        URL classUrl = new URL("file:///ideaWorkspace/");

        URLClassLoader loader = new URLClassLoader(new URL[]{classUrl});

        while (true) {
            //  通過URLClassLoader創建一個新的類加載器
            if (loader == null) {
                break;
            }
            // 問題:靜態塊什麼時候觸發?
            Class clazz = loader.loadClass("HelloService");
            System.out.println("HelloService所使用的類加載器:" + clazz.getClassLoader());

            //反射創建對象
            Object newInstance = clazz.newInstance();
            //調用test方法
            Object value = clazz.getMethod("test").invoke(newInstance);
            System.out.println("調用getValue獲得的返回值爲:" + value);

            Thread.sleep(3000L);
            System.out.println();

            //對象置爲空方便GC直接回收
            newInstance = null;
            //類加載器也置爲空
            loader = null;
        }

        //手動觸發GC,此方法不一定有用,但能讓jvm更主動的去做一次GC
        System.gc();
        Thread.sleep(10000L);
    }
}

可以看到信息已經打印出來了,說明Class所有的實例和該類的所有ClassLoader實例都已經被GC;
在這裏插入圖片描述

5、雙親委派模型是什麼?

首先要了解什麼是雙親委派模型?

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-T2sCcYz6-1585758473101)(https://note.youdao.com/src/930E982EF7264D36B222FD7ABE89B52E)]

爲了避免重複加載,由下到上逐級委託,由上倒下逐級查找

首先不會自己去嘗試加載類,而是把這個請求,委派給父加載器去完成;每一個層次的加載器都是如此,因此所有的類加載請求都會傳給上層的啓動類加載。
只有當父加載器反饋自己無法完成該加載的請求時(該加載器的搜索範圍中沒有找到對應的類),子加載器纔會嘗試自己去加載。

==注:類加載器之間不存在父類子類的關係,“雙親”是翻譯,可以理解爲邏輯定義的上下級關係。==

下面用代碼具體去體現

首先看一個問題,如果我把類加載器的創建放到while循環中去,每次都會創建新的類加載器,那麼能不能實現類的動態加載呢?

package classloader_demo1;

import java.net.URL;
import java.net.URLClassLoader;

/**
 * 指定class 進行加載
 */
public class LoaderTest {
    public static void main(String[] args) throws Exception {
        //指定jvm查找類的位置
        URL classUrl = new URL("file:///ideaWorkspace/");

        while (true) {
            //每次都新建一個類加載器
            URLClassLoader loader = new URLClassLoader(new URL[]{classUrl});

            //  通過URLClassLoader創建一個新的類加載器
            if (loader == null) {
                break;
            }
            // 問題:靜態塊什麼時候觸發?
            Class clazz = loader.loadClass("HelloService");
            System.out.println("HelloService所使用的類加載器:" + clazz.getClassLoader());

            //反射創建對象
            Object newInstance = clazz.newInstance();
            //調用test方法
            Object value = clazz.getMethod("test").invoke(newInstance);
            System.out.println("調用getValue獲得的返回值爲:" + value);

            Thread.sleep(3000L);
            System.out.println();

            //對象置爲空方便GC直接回收
            newInstance = null;
            //類加載器也置爲空
            loader = null;
        }

        //手動觸發GC,此方法不一定有用,但能讓jvm更主動的去做一次GC
        System.gc();
        Thread.sleep(10000L);
    }
}

HelloService所使用的類加載器:java.net.URLClassLoader@119d7047
靜態方法被運行
靜態代碼塊運行
hello..4444netease
調用getValue獲得的返回值爲:null

[Loaded HelloService from file:/ideaWorkspace/]
HelloService所使用的類加載器:java.net.URLClassLoader@3b07d329
靜態方法被運行
靜態代碼塊運行
hello..5555netease
調用getValue獲得的返回值爲:null

可以看到編譯之後,類確實被重新加載了,eclipse/idea的熱部署和jsp頁面就是通過不同的類加載器實現的。

敲重點!!!下面思考一個問題,如果我新創建一個parentLoader,然後傳入while循環中的類加載器,還能不能實現動態加載?

package classloader_demo1;

import java.net.URL;
import java.net.URLClassLoader;

/**
 * 指定class 進行加載
 */
public class LoaderTest {
    public static void main(String[] args) throws Exception {
        //指定jvm查找類的位置
        URL classUrl = new URL("file:///ideaWorkspace/");

        URLClassLoader parentLoader = new URLClassLoader(new URL[]{classUrl});
        while (true) {
            //每次都新建一個類加載器
            URLClassLoader loader = new URLClassLoader(new URL[]{classUrl},parentLoader);

            //  通過URLClassLoader創建一個新的類加載器
            if (loader == null) {
                break;
            }
            // 問題:靜態塊什麼時候觸發?
            Class clazz = loader.loadClass("HelloService");
            System.out.println("HelloService所使用的類加載器:" + clazz.getClassLoader());

            //反射創建對象
            Object newInstance = clazz.newInstance();
            //調用test方法
            Object value = clazz.getMethod("test").invoke(newInstance);
            System.out.println("調用getValue獲得的返回值爲:" + value);

            Thread.sleep(3000L);
            System.out.println();

            //對象置爲空方便GC直接回收
            newInstance = null;
            //類加載器也置爲空
            loader = null;
        }

        //手動觸發GC,此方法不一定有用,但能讓jvm更主動的去做一次GC
        System.gc();
        Thread.sleep(10000L);
    }
}

答案是否定的,其實這裏傳入parentLoader就是使用了委派雙親模式,雖然每次都新創建了一個類加載器,但是lodaer不會自己去查找類,而是會把查找交給parentLoader去查找,所以每次使用的還是parentLoader類加載器,也就不會動態的加載HelloService類了。
在這裏插入圖片描述

如果我把parentLoader刪掉,其實他指定的父級加載器就會是ExtClassLoader/AppclassLoader,希望大家能夠理解,有問題的話可以隨時私信我進行溝通。

四、總結:

今天我們學習了整個類加載器的相關知識,學習了類的生命週期、加載順序,學會了
1、如何查看類加載器
2、JVM如何知道我們的類在何方?
3、類會不會重複加載?
4、類如何卸載條件是什麼?
5、雙親委派模型是什麼?
最後希望大家能夠多多交流,一起學習一起進步!
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章