JVM篇2:[-加載器ClassLoader-]

寫本篇的動因只是一段看起來很詭異的代碼,讓我感受有必要認識一下ClassLoaderhtml

----[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
複製代碼

1、Java類加載流程

1.Java虛擬機結構

上一篇講了Java虛擬機,關於類加載器一筆帶過,本篇詳細講一下
java文件經過javac能夠編譯成.class文件,類加載器就是將.calss加載到內存裏java

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


2.類加載的流程

關於Class實例在堆中仍是方法區中?這裏找了一篇文章,講得挺深git

class字節碼的加載.png


2.1:加載
將字節碼(二進制流)載入方法區
堆內存中生成java.lang.Class對象,做爲方法區中該類各類數據的操做入口

|-- .class文件主要來源--------------------
    -– 磁盤中直接加載
    -– 網絡加載.class文件
    -– 從zip ,jar 等文件中加載.class 文件
    -– 從專有數據庫中提取.class文件
    -– 將Java源文件動態編譯爲.class文件
複製代碼

2.2:鏈接 - 驗證

驗證加載進來的字節流信息是否符合虛擬機規範github

[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 
複製代碼

2、類被初始化的時機

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
複製代碼

3、關於類加載器

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

經過ClassLoader.getSystemClassLoader()能夠獲取系統類類加載器
debug一下,能夠看到系統類加載器:類名爲AppClassLoader,因此也稱應用類加載器bash

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
複製代碼

4、自定義類本地磁盤類加載器

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文件移到磁盤的哪一個位置,均可以的經過指定路徑加載


5、自定義類網絡類加載器

將剛纔的class文件放到服務器上:www.toly1994.com:8089/imgs/HelloW…
而後訪問路徑來讀取字節流,進行類的加載

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也加載不了,就崩了


6、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
捷文連接:juejin.im/post/5c7a95…

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

個人github:github.com/toly1994328
個人簡書:www.jianshu.com/u/e4e52c116…
個人簡書:www.jianshu.com/u/e4e52c116…
我的網站:www.toly1994.com

3.聲明

1----本文由張風捷特烈原創,轉載請註明
2----歡迎廣大編程愛好者共同交流
3----我的能力有限,若有不正之處歡迎你們批評指證,一定虛心改正
4----看到這裏,我在此感謝你的喜歡與支持

icon_wx_200.png
相關文章
相關標籤/搜索