JVM之類載入器ClassLoader

張風捷特烈發表於2019-03-07

寫本篇的動因只是一段看起來很詭異的程式碼,讓我感覺有必要認識一下ClassLoader

----[Counter.java]-------------------------
public class Counter {
    private static Counter sCounter = new Counter();//<---- tag1
    public static int count = 10;//<---- tag2
    private Counter() {
        count++;
    }
    public static Counter getInstance() {
        return sCounter;
    }
}

----[Client.java]-------------------------
public class Client {
    public static void main(String[] args) {
        Counter counter = Counter.getInstance();
        System.out.println(counter.count);//10
    }
}

|-- 當tag1和tag2換一下位置,得到的是11
複製程式碼

一、Java類載入流程

1.Java虛擬機器結構

上一篇講了Java虛擬機器,關於類載入器一筆帶過,本篇詳細講一下
java檔案通過javac可以編譯成.class檔案,類載入器就是將.calss載入到記憶體裡

虛擬機器的內部體系結構.png


2.類載入的流程

關於Class例項在堆中還是方法區中?這裡找了一篇文章,講得挺深

class位元組碼的載入.png


2.1:載入
將位元組碼(二進位制流)載入方法區
堆記憶體中生成java.lang.Class物件,作為方法區中該類各種資料的操作入口

|-- .class檔案主要來源--------------------
    -– 磁碟中直接載入
    -– 網路載入.class檔案
    -– 從zip ,jar 等檔案中載入.class 檔案
    -– 從專有資料庫中提取.class檔案
    -– 將Java原始檔動態編譯為.class檔案
複製程式碼

2.2:連線 - 驗證

驗證載入進來的位元組流資訊是否符合虛擬機器規範

[1].檔案格式驗證: 位元組流是否符合class檔案格式規範
[2].後設資料驗證: 是否符合java的語言語法的規範
[3].位元組碼驗證:方法體進行校驗分析,保證執行時沒危害出現
[4].符號引用驗證 :常量池中的各種符號引用資訊進行匹配性校驗
複製程式碼

2.3:連線 - 準備

為類靜態變數分配記憶體並設定為[對應型別的初始值]

----[Counter.java]-------------------------
public class Counter {
    private static Counter sCounter = new Counter();
    public static int count = 1;
    private Counter() {
        count++;
    }
    public static Counter getInstance() {
        return sCounter;
    }
}

如上:在準備階段 count 的值為int的預設值 = 0
複製程式碼

2.4:連線 - 解析

常量池內的符號引用替換為直接引用的過程,也就是字面量轉化為指標。
主要解析:類,介面,欄位,類方法,介面方法,方法型別,方法控制程式碼和呼叫點限定符引用


2.5 : 初始化

按順序查詢靜態變數以及靜態程式碼塊對使用者自定義類變數的賦值,

//現在count=0,呼叫後new Counter()時count++,變為1
private static Counter sCounter = new Counter();
public static int count = 10;// 此時count賦值為10 
複製程式碼

二、類被初始化的時機

1.類被初始化的時機程式碼測試
1.建立例項
2.訪問靜態變數或者對該靜態變數賦值
3.呼叫靜態方法
4.反射
5.初始化一個類的子類
6.JVM啟動時被標明為啟動類(main)

---->[Shape類]------------------
public class Shape {
    public static String color = "白色";
    static {
        System.out.println("-----初始化於Shape-----");
    }
    public static void draw() {
    }
}

---->[Shape子類:Rect]------------------
public class Rect extends Shape {
    public static int radius = 20;
    static {
        System.out.println("-----初始化於Rect-----");
    }
}

new Shape(); //1.建立例項
String color = Shape.color;//2.訪問靜態變數
Shape.color = "黑色";//2.對該靜態變數賦值
Shape.draw();//3.呼叫靜態方法
Class.forName("classloader.Shape");//4.反射
Rect.radius = 10;//5.初始化一個類的子類
複製程式碼

2.final對初始化的影響
|-- 訪問編譯期靜態常量[不會]觸發初始化
|-- 訪問執行期靜態常量[會]觸發初始化

public class Shape {
    ...
    public static final int weight = 1;
    public static final int height = new Random(10).nextInt();
    ...
}
int w = Shape.weight;//編譯期靜態常量不會觸發初始化
int h = Shape.height;//執行期靜態常量會觸發初始化
|-- 其中height在執行時才可以確定值,訪問會觸發初始化
複製程式碼

3.初始化的其他小點
|-- 類初始化時並不會初始化它的介面
|-- 子介面初始化不會初始化父介面
|-- 宣告類變數時不會初始化
|-- 子類再呼叫父類的靜態方法或屬性時,子類不會被初始化

Shape shape;//宣告類變數,不會初始化
String color = Rect.color;//只初始化Shape
Rect.draw();//只初始化Shape
複製程式碼

三、關於類載入器

1.系統類載入器(應用類載入器)

通過ClassLoader.getSystemClassLoader()可以獲取系統類類載入器
debug一下,可以看到系統類載入器:類名為AppClassLoader,所以也稱應用類載入器

debug.png

ClassLoader loader = ClassLoader.getSystemClassLoader();
System.out.println(loader);

Shape shape = new Shape();
////sun.misc.Launcher$AppClassLoader@18b4aac2
ClassLoader loader = shape.getClass().getClassLoader();

String name = "toly";
ClassLoader loaderSting = name.getClass().getClassLoader();
System.out.println(loaderSting);//null
//可見String的類載入器為null,先說一下,為null時由Bootstrap類載入器載入

|-- 還有一點想強調一下,類載入器載入類後,不會觸發類的初始化
ClassLoader loader = ClassLoader.getSystemClassLoader();
Class<?> shapeClazz = loader.loadClass("classloader.Shape");//此時不初始化
Shape shape = (Shape) shapeClazz.newInstance();//建立例項時才會初始化
複製程式碼

2.父委託機制(或雙親委託機制)

這裡的父並不是指繼承,而是ClassLoader類中有一個parent屬性是ClassLoader型別
所以是認乾爹,而不是親生的。就像Android中的ViewGroup和View的父子View關係
認了乾爹之後,有事先讓乾爹來擺平,乾爹擺不平,再自己來,都擺不平,就崩了唄。

---->[ClassLoader#成員變數]----------------
private final ClassLoader parent;

---->[ClassLoader#建構函式一參]----------------
|-- 可以在一參建構函式中傳入parent,認個乾爹,瞟了一下原始碼,貌似是parent初始化的唯一途徑
protected ClassLoader(ClassLoader parent) {
    this(checkCreateClassLoader(), parent);
}

|--關於父委託機制loadClass方法完美詮釋:
---->[ClassLoader#loadClass]------------------
public Class<?> loadClass(String name) throws ClassNotFoundException {
    return loadClass(name, false);
}

---->[ClassLoader#loadClass(String,boolean)]------------------------------
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 {//沒有乾爹,讓大佬Bootstrap類載入器載入
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
            }
            if (c == null) {//乾爹和大佬都載入不了
                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;
    }
}

複製程式碼

Java類載入流程.png


3.三個JVM中的類載入器

JVM內建類載入器.png

Bootstrap ClassLoader   : 引導類載入器(啟動類載入器/根類載入器)
|-- C++語言實現, 負責載入jre/lib路徑下的核心類庫
System.out.println(System.getProperty("sun.boot.class.path"));
//D:\M\JDK1.8\jre\lib\resources.jar;
// D:\M\JDK1.8\jre\lib\rt.jar;
// D:\M\JDK1.8\jre\lib\sunrsasign.jar;
// D:\M\JDK1.8\jre\lib\jsse.jar;
// D:\M\JDK1.8\jre\lib\jce.jar;
// D:\M\JDK1.8\jre\lib\charsets.jar;
// D:\M\JDK1.8\jre\lib\jfr.jar;
// D:\M\JDK1.8\jre\classes

Launcher$ExtClassLoader : 擴充類載入器
|-- Java語言實現,負責載入jre/lib/ext
System.out.println(System.getProperty("java.ext.dirs"));
//D:\M\JDK1.8\jre\lib\ext;C:\Windows\Sun\Java\lib\ext

Launcher$AppClassLoader : 系統類載入器
|-- Java語言實現,載入環境變數路徑classpath或java.class.path 指定路徑下的類庫
String property = System.getProperty("java.class.path");
//D:\M\JDK1.8\jre\lib\charsets.jar;
// D:\M\JDK1.8\jre\lib\deploy.jar;
...略若干jre的jar路徑...
// J:\FileUnit\file_java\base\out\production\classes;  <--- 當前專案的輸出路徑
// C:\Program Files\JetBrains\IntelliJ IDEA 2018.1.3\lib\idea_rt.jar
複製程式碼

四、自定義類本地磁碟類載入器

1.自定義類載入器的乾爹
---->[ClassLoader#建構函式]------------------------------------------
protected ClassLoader(ClassLoader parent) {
    this(checkCreateClassLoader(), parent);
}

protected ClassLoader() {
    this(checkCreateClassLoader(), getSystemClassLoader());
}

這裡可以看出無參構造是預設乾爹是:getSystemClassLoader,也就是系統類載入器載入器
當然也可以使用一參構造認乾爹

|-- 上面分析:在ClassLoader#loadClass方法中,當三個JVM的類載入器都找不到時
|-- 會呼叫findClass方法來初始化c ,那我們來看一下findClass:
---->[在ClassLoader#findClass]------------------------
protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
}
就問你一句:人家直接拋異常,你敢不覆寫嗎?
複製程式碼

2.自定義LocalClassLoader
/**
 * 作者:張風捷特烈
 * 時間:2019/3/7/007:14:05
 * 郵箱:1981462002@qq.com
 * 說明:本地磁碟類載入器
 */
public class LocalClassLoader extends ClassLoader {
    private String path;
    public LocalClassLoader(String path) {
        this.path = path;
    }
    @Override
    protected Class<?> findClass(String name) {
        byte[] data = getBinaryData(name);
        if (data == null) {
            return null;
        }
        return defineClass(name, data, 0, data.length);
    }
    /**
     * 讀取位元組流
     *
     * @param name 全類名
     * @return 位元組碼陣列
     */
    private byte[] getBinaryData(String name) {
        InputStream is = null;
        byte[] result = null;
        ByteArrayOutputStream baos = null;
        try {
            if (name.contains(".")) {
                String[] split = name.split("\\.");
                name = split[split.length - 1];
            }
            String path = this.path + "\\" + name + ".class";
            File file = new File(path);
            if (!file.exists()) {
                return null;
            }
            is = new FileInputStream(file);
            baos = new ByteArrayOutputStream();
            byte[] buff = new byte[1024];
            int len = 0;
            while ((len = is.read(buff)) != -1) {
                baos.write(buff, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (is != null) {
                    is.close();
                }
                if (baos != null) {
                    result = baos.toByteArray();
                    baos.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return result;
    }
}
複製程式碼

3.測試類的位元組碼檔案

新建一個類HelloWorld,有一個公共方法say,注意包名和資料夾名

測試類.png

package com.toly1994.classloader;
public class HelloWorld {
    public void say() {
        System.out.println("HelloWorld");
    }
}
複製程式碼

4.使用LocalClassLoader

使用LocalClassLoader載入剛才的位元組碼檔案,通過反射呼叫say方法,執行無誤
這裡要提醒一下:使用javac編譯時的jdk版本,要和工程的jdk版本一致,不然會報錯

LocalClassLoader loader = new LocalClassLoader("G:\\Out\\java\\com\\toly1994\\classloader");
try {
    Class<?> clazz = loader.loadClass("com.toly1994.classloader.HelloWorld");;
    Constructor<?> constructor = clazz.getConstructor();
    Object obj = constructor.newInstance();
    Method say = clazz.getMethod("say");
    say.invoke(obj);//HelloWorld
} catch (NoSuchMethodException | InvocationTargetException e) {
    e.printStackTrace();
}

|-- 這裡可以測試一下obj的類載入器     
System.out.println(obj.getClass().getClassLoader());
//classloader.LocalClassLoader@6b71769e
複製程式碼

這樣無論.java檔案移到磁碟的哪個位置,都可以的通過指定路徑載入


五、自定義類網路類載入器

將剛才的class檔案放到伺服器上:http://www.toly1994.com:8089/imgs/HelloWorld.class
然後訪問路徑來讀取位元組流,進行類的載入

1.自定義NetClassLoader

核心也就是獲取到流,然後findClass中通過defineClass生成Class物件

/**
 * 作者:張風捷特烈
 * 時間:2019/3/7/007:14:05
 * 郵箱:1981462002@qq.com
 * 說明:網路類載入器
 */
public class NetClassLoader extends ClassLoader {
    private String urlPath;
    public NetClassLoader(String urlPath) {
        this.urlPath = urlPath;
    }
    @Override
    protected Class<?> findClass(String name)  {
        byte[] data = getDataFromNet(urlPath);
        if (data == null) {
            return null;
        }
        return defineClass(name, data, 0, data.length);
    }
    private byte[] getDataFromNet(String urlPath) {
        byte[] result = null;
        InputStream is = null;
        ByteArrayOutputStream baos = null;
        try {
            URL url = new URL(urlPath);
            is = url.openStream();
            baos = new ByteArrayOutputStream();
            byte[] buff = new byte[1024];
            int len = 0;
            while ((len = is.read(buff)) != -1) {
                baos.write(buff, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (is != null) {
                    is.close();
                }
                if (baos != null) {
                    result = baos.toByteArray();
                    baos.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return result;
    }
}
複製程式碼

2.使用

使用上基本一致

NetClassLoader loader = new NetClassLoader("http://www.toly1994.com:8089/imgs/HelloWorld.class");
try {
    Class<?> clazz =  loader.loadClass("com.toly1994.classloader.HelloWorld");
    Constructor<?> constructor = clazz.getConstructor();
    Object obj = constructor.newInstance();
    Method say = clazz.getMethod("say");
    say.invoke(obj);//HelloWorld
} catch (NoSuchMethodException | InvocationTargetException e) {
    e.printStackTrace();
}

|-- 這裡可以測試一下obj的類載入器 
System.out.println(obj.getClass().getClassLoader());
//classloader.NetClassLoader@66d2e7d9
複製程式碼

3.父委派機制測試

現在網路和本地都可以,我們讓本地的loader當做網路載入的父親

---->[NetClassLoader#新增構造]------------------------
public NetClassLoader(ClassLoader parent, String urlPath) {
    super(parent);
    this.urlPath = urlPath;
}

---->[測試類]-----------------------------
LocalClassLoader localLoader = new LocalClassLoader("G:\\Out\\java\\com\\toly1994\\classloader");
//這裡講NetClassLoader的乾爹設定為localLoader
NetClassLoader netLoader = new NetClassLoader(localLoader, "http://www.toly1994.com:8089/imgs/HelloWorld.class");
try {
    Class<?> clazz = netLoader.loadClass("com.toly1994.classloader.HelloWorld");
    Constructor<?> constructor = clazz.getConstructor();
    Object obj = constructor.newInstance();
    System.out.println(obj.getClass().getClassLoader());
    //這裡列印classloader.LocalClassLoader@591f989e
    Method say = clazz.getMethod("say");
    say.invoke(obj);//HelloWorld
} catch (NoSuchMethodException | InvocationTargetException e) {
    e.printStackTrace();
}
|-- 可以看到,老爹LocalClassLoader能載入,作為孩子的NetClassLoader就沒載入

|--- 現在將本地的[刪了],老爹LocalClassLoader載入不了,NetClassLoader自己搞
System.out.println(obj.getClass().getClassLoader());
classloader.NetClassLoader@4de8b406
複製程式碼

現在應該很明白父委派機制是怎麼玩的了吧,如果NetClassLoader也載入不了,就崩了


六、class物件的解除安裝

1.一個類被class被能被GC回收(即:解除安裝)的條件
[1].該類所有的例項都已經被GC。
[2].載入該類的ClassLoader例項已經被GC。
[3].該類的java.lang.Class物件沒有在任何地方被引用。
複製程式碼

2.使用自定義載入器時JVM中的引用關係

自定義載入器的引用關係.png

LocalClassLoader localLoader = new LocalClassLoader("G:\\Out\\java\\com\\toly1994\\classloader");
Class<?> clazz = localLoader.loadClass("com.toly1994.classloader.HelloWorld");
Constructor<?> constructor = clazz.getConstructor();
Object obj = constructor.newInstance();
System.out.println(obj.getClass().getClassLoader());
Method say = clazz.getMethod("say");
say.invoke(obj);//HelloWorld

|-- 使用上面的類載入器再載入一次com.toly1994.classloader.HelloWorld可見兩個class物件一致
System.out.println(clazz.hashCode());//1265210847
Class<?> clazz2 = localLoader.loadClass("com.toly1994.classloader.HelloWorld");
System.out.println(clazz2.hashCode());//1265210847
複製程式碼

2.解除安裝
LocalClassLoader localLoader = new LocalClassLoader("G:\\Out\\java\\com\\toly1994\\classloader");
Class<?> clazz = localLoader.loadClass("com.toly1994.classloader.HelloWorld");
Constructor<?> constructor = clazz.getConstructor();
Object obj = constructor.newInstance();
Method say = clazz.getMethod("say");
say.invoke(obj);//HelloWorld

// 清除引用
obj = null;  //清除該類的例項
localLoader = null;  //清楚該類的ClassLoader引用
clazz = null;  //清除該class物件的引用
複製程式碼

後記:捷文規範

參考文章:

深入理解Java類載入器(ClassLoader)
Java --ClassLoader建立、載入class、解除安裝class
關於Class例項在堆中還是方法區中?


1.本文成長記錄及勘誤表
專案原始碼 日期 附錄
V0.1--無 2018-3-7

釋出名:JVM之類載入器ClassLoader
捷文連結:https://juejin.im/post/5c7a9595f265da2db66df32c

2.更多關於我
筆名 QQ 微信
張風捷特烈 1981462002 zdl1994328

我的github:https://github.com/toly1994328
我的簡書:https://www.jianshu.com/u/e4e52c116681
我的簡書:https://www.jianshu.com/u/e4e52c116681
個人網站:http://www.toly1994.com

3.宣告

1----本文由張風捷特烈原創,轉載請註明
2----歡迎廣大程式設計愛好者共同交流
3----個人能力有限,如有不正之處歡迎大家批評指證,必定虛心改正
4----看到這裡,我在此感謝你的喜歡與支援

icon_wx_200.png

相關文章