從零造就JVM大牛(一)

雕爺的架構之路發表於2020-10-26

引言

從事java的小夥伴大家好,如果你是一名從事java行業的程式設計師,無論你是小白還是工作多年的老司機,我相信這篇文章一定會給你帶來

不同程度的收貨不敢說你看完我的文章從此精通jvm打遍天下無對手,但我能保證的是看完我的文章並且實踐操作加以理解,至少在jvm

的這個領域碾壓百分之80以上的程式設計師。廢話不多說我們進入正題。(此文連載,請持續關注!!!)

一:類載入

我相信很多從事java的小夥伴兒在網上或者視訊上也看了很多關於jvm的文章和講解,但總覺得缺少點兒什麼,那麼今天我來告訴你為什麼

會有這種感覺,因為很多人或者說很多文章講的jvm都沒有從最底層,沒有從人的習慣性思維上去剖析。從今天開始,我就要把jvm那點兒

事兒給小夥伴兒們說清楚。保證你會見到jvm領域的另一番美妙的天地。

廢話不多說直接進入正題。講類載入器之前,我們們先從類載入開始說起,那麼java底層是如何載入一個類的呢,他的順序是怎麼樣的呢?下面

我會細緻的手把手帶你分析。

在java程式碼中,型別的載入,連線,初始化過程都是在程式執行期間完成的.

型別載入:這裡的型別指的的是什麼呢?

答:型別就是指的我們Java原始碼通過編譯後的class檔案

型別的來源有哪些?型別的來源我總結了以下7點內容

答:

1:本地磁碟

2:網路下載.class檔案

3:war,jar下載入.class檔案

4:從專門的資料庫中讀取.class檔案(少見)

5:將java原始檔動態編譯成class檔案

6:典型的就是動態代理,通過執行期生成class檔案

7:我們的jsp會被轉換成servlet,而我們的serlvet是一個java檔案,會被編譯成class檔案

那麼我們的是通過什麼進行載入的呢?是如何被載入到jvm中的呢?

答:通過我們的類載入器(classLoader)進行載入

首先我們來了解以下類載入器的種類?系統級別的類載入器如下

1:啟動類載入器 (Bootstrap Classloader)

2:擴充套件類載入器(Extention ClassLoader)

3:應用類載入器

非系統級別類載入器如下:

1:自定義類載入器

接下來我們一個一個類載入剖析,好好的講講他們的前世今生,以及作用,讓你徹底的瞭解類載入器到底是什麼。

一:啟動類載入器(Bootstrap Classloader)重點剖析

載入的職責:負責載入JAVA_HOME/jre/lib/rt.jar。該載入器是有C++實現,不是ClassLoader類的子類。

接下來我們會以程式碼穿插的方式來講清楚類載入器到底是什麼。很多人只是用嘴說,這樣當時聽懂了,然而並不能

真正的體會到類載入器以及類載入的精髓。只有用示例去驗證你的說法,這才是正確的學習姿勢。廢話不多說開搞。

首先建立一個java工程 jvm-classloader jdk版本採用1.8.0_144 包名為src/com.test 建立類 MainClass01,結構如下圖:

首先我們通過程式碼的形式來看看,我們的啟動類載入器到底載入了哪些包

package com.test;

import java.util.Arrays;
import java.util.List;

/**
 * jvm 類載入器 第一章
 * @author 奇客時間-時光
 * 列印啟動類載入器載入的路徑
 */
public class MainClass01 {
    
    public static void main(String[] args) {

        String bootStrapLoadingPath = System.getProperty("sun.boot.class.path");
        List<String> bootLoadingPathList = Arrays.asList(bootStrapLoadingPath.split(";"));
        for(String bootPath:bootLoadingPathList) {
            System.out.println("啟動類載入器載入的目錄:{}"+bootPath);
        }
    }
}



"C:\Program Files\Java\jdk1.8.0_144\bin\java.exe" "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2019.2\lib\idea_rt.jar=60329:C:\Program Files\JetBrains\IntelliJ IDEA 2019.2\bin" -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk1.8.0_144\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\rt.jar;I:\jvm\out\production\jvm-classloader" com.test.MainClass01
啟動類載入器載入的目錄:{}C:\Program Files\Java\jdk1.8.0_144\jre\lib\resources.jar
啟動類載入器載入的目錄:{}C:\Program Files\Java\jdk1.8.0_144\jre\lib\rt.jar
啟動類載入器載入的目錄:{}C:\Program Files\Java\jdk1.8.0_144\jre\lib\sunrsasign.jar
啟動類載入器載入的目錄:{}C:\Program Files\Java\jdk1.8.0_144\jre\lib\jsse.jar
啟動類載入器載入的目錄:{}C:\Program Files\Java\jdk1.8.0_144\jre\lib\jce.jar
啟動類載入器載入的目錄:{}C:\Program Files\Java\jdk1.8.0_144\jre\lib\charsets.jar
啟動類載入器載入的目錄:{}C:\Program Files\Java\jdk1.8.0_144\jre\lib\jfr.jar
啟動類載入器載入的目錄:{}C:\Program Files\Java\jdk1.8.0_144\jre\classes

Process finished with exit code 0


通過以上執行程式碼可知,啟動類載入器載入如下的這些jar包。包含了大家常說的rt.jar。那麼現在有個問題,從執行結果上我們看啟動類載入載入了C:\Program Files\Java\jdk1.8.0_144\jre\classes這個目錄下的類。那麼我們自己寫一個類,編譯成Class檔案以後丟到這個目錄下它會被載入嘛?我們不妨來試一下。我們在com.test包下面建立一個Cat類。然後找到編譯好的Cat.class檔案,扔到C:\Program Files\Java\jdk1.8.0_144\jre\classes目錄下。

如圖可知,我們jre目錄並沒有classes目錄,所以我們手動建立一個,並且把Cat.class檔案扔進去。然後在測試類中獲取一下Cat.classLoader是什麼,由此就可以知道Cat是不是被啟動類載入器載入了。

由上面的執行結果可知,我們的Cat類確實被啟動類載入器給載入了。為什麼是null?當一個類被啟動類載入器載入以後,那麼他的getClassLoader返回的結果就是null,這也是啟動類載入器和其他類載入器不一樣的地方。

補充說明:Bootstrap Classloader載入器是由C++去載入的,然後Bootstrap Classloader載入rt等jar包,Bootstrap Classloader載入器也載入了系統類載入器和擴充套件類類載入器他們都位於sun.boot.class.path這個路徑。

二:擴充套件類載入器(Extention ClassLoader)重點剖析

載入的職責:載入java平臺擴充套件的jar包,\lib\ext,可以通過-Djava.ext.dirs指定載入的路徑

該載入器是有java程式碼編寫的,並且是ClassLoader的子類,位於sun.misc.Launcher$ExtClassLoader 是我們launch類的一個內部類

同樣的我們在MainClass01中測試一下

package com.test;

import java.util.Arrays;
import java.util.List;

/**
 * jvm 類載入器 第一章
 * @author 奇客時間-時光
 * 列印啟動類載入器載入的目錄
 * 列印擴充套件類載入器載入的目錄
 */
public class MainClass01 {

    public static void main(String[] args) {

        String extLoadingPath = System.getProperty("java.ext.dirs");
        List<String> list = Arrays.asList(extLoadingPath.split(";"));
        for(String extpath:list) {
            System.out.println("擴充套件類載入器載入的目錄:{}"+extpath);
        }
    }
}


"C:\Program Files\Java\jdk1.8.0_144\bin\java.exe" "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2019.2\lib\idea_rt.jar=59761:C:\Program Files\JetBrains\IntelliJ IDEA 2019.2\bin" -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk1.8.0_144\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\rt.jar;I:\jvm\out\production\jvm-classloader" com.test.MainClass01
擴充套件類載入器載入的目錄:{}C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext
擴充套件類載入器載入的目錄:{}C:\WINDOWS\Sun\Java\lib\ext

Process finished with exit code 0

三:應用類載入器(APP ClassLoader)重點剖析

載入的職責:負責載入我們工程目錄下classpath下的檔案下.class以及jar包

該載入器也是由Java程式碼編寫的也是我們ClassLoader的一個子類,sun.misc.Launcher$AppClassLoader 是我們的launcher的內部類

String appLoadingPath = System.getProperty("java.class.path");

同樣的,我們在MainClass01中執行如下程式碼:

package com.test;

import java.util.Arrays;
import java.util.List;

/**
 * jvm 類載入器 第一章
 * @author 奇客時間-時光
 * 列印啟動類載入器載入的目錄
 * 列印擴充套件類載入器載入的目錄
 * 列印應用類載入器載入的目錄
 */
public class MainClass01 {

    public static void main(String[] args) {

        String appLoadingPath = System.getProperty("java.class.path");
        List<String> appLoadingPathList = Arrays.asList(appLoadingPath.split(";"));
        for(String appPath:appLoadingPathList) {
            System.out.println("應用類載入器載入的目錄:{}"+appPath);
        }

    }
}


"C:\Program Files\Java\jdk1.8.0_144\bin\java.exe" "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2019.2\lib\idea_rt.jar=52479:C:\Program Files\JetBrains\IntelliJ IDEA 2019.2\bin" -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk1.8.0_144\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\rt.jar;I:\jvm\out\production\jvm-classloader" com.test.MainClass01
應用類載入器載入的目錄:{}C:\Program Files\Java\jdk1.8.0_144\jre\lib\charsets.jar
應用類載入器載入的目錄:{}C:\Program Files\Java\jdk1.8.0_144\jre\lib\deploy.jar
應用類載入器載入的目錄:{}C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\access-bridge-64.jar
應用類載入器載入的目錄:{}C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\cldrdata.jar
應用類載入器載入的目錄:{}C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\dnsns.jar
應用類載入器載入的目錄:{}C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\jaccess.jar
應用類載入器載入的目錄:{}C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\jfxrt.jar
應用類載入器載入的目錄:{}C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\localedata.jar
應用類載入器載入的目錄:{}C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\nashorn.jar
應用類載入器載入的目錄:{}C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\sunec.jar
應用類載入器載入的目錄:{}C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\sunjce_provider.jar
應用類載入器載入的目錄:{}C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\sunmscapi.jar
應用類載入器載入的目錄:{}C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\sunpkcs11.jar
應用類載入器載入的目錄:{}C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\zipfs.jar
應用類載入器載入的目錄:{}C:\Program Files\Java\jdk1.8.0_144\jre\lib\javaws.jar
應用類載入器載入的目錄:{}C:\Program Files\Java\jdk1.8.0_144\jre\lib\jce.jar
應用類載入器載入的目錄:{}C:\Program Files\Java\jdk1.8.0_144\jre\lib\jfr.jar
應用類載入器載入的目錄:{}C:\Program Files\Java\jdk1.8.0_144\jre\lib\jfxswt.jar
應用類載入器載入的目錄:{}C:\Program Files\Java\jdk1.8.0_144\jre\lib\jsse.jar
應用類載入器載入的目錄:{}C:\Program Files\Java\jdk1.8.0_144\jre\lib\management-agent.jar
應用類載入器載入的目錄:{}C:\Program Files\Java\jdk1.8.0_144\jre\lib\plugin.jar
應用類載入器載入的目錄:{}C:\Program Files\Java\jdk1.8.0_144\jre\lib\resources.jar
應用類載入器載入的目錄:{}C:\Program Files\Java\jdk1.8.0_144\jre\lib\rt.jar
應用類載入器載入的目錄:{}I:\jvm\out\production\jvm-classloader
應用類載入器載入的目錄:{}C:\Program Files\JetBrains\IntelliJ IDEA 2019.2\lib\idea_rt.jar

Process finished with exit code 0

四:我們自定義的類載入器

由此可知,三種類載入所載入對應的目錄已經介紹完畢,後面還會對這三種類載入器進行深入的解析。下面我們再看看自定義類載入器。為什麼需要自定義類載入器呢?因為有些場景下jdk給我們提供的三種類載入器沒有辦法實現我們的個性化需求。關於為什麼,這個我後面會詳細闡述。我們首先看看自定義類載入器以及原始碼。

如何寫自定義類載入器呢?首先我們看看jdk 提供的java原版 doc文件是如何描述的,這裡要說一下,無論各位小夥伴以後學什麼技術,第一手資料很重要。因為是最權威的,也沒有經過任何加工的,也是最準確的。所以我們直接看原始碼中的doc文件即可。

那麼我們要寫自己的類載入器,對於小白來說肯定是不清楚如何下手的。這個時候,我們就要想,自定義類載入肯定是和類載入器有關係,不放我們看看java有沒有提供關於類載入器相關的類。於是我們可以通過IDEA的編輯器進行搜尋一下,類載入器顧名思義 ClassLoader先搜尋一下。結果還真有。說明這是一個跟類載入器有關的類,我們直接閱讀他的java doc。

我把之前編輯好的中文註釋貼在下面。大家可以閱讀,不過這裡還是建議學習jdk原始碼的小夥伴還是自己閱讀比較好,這樣記得也比較深刻。

4.1:ClassLoader抽象類 文件解讀

/**
* A class loader is an object that is responsible for loading classes(類載入器是一個物件,作用就是用來載入器類的). The
* class ClassLoader is an abstract class(本類是一個抽象的類). Given the binary name of a class
(給定一個類的二進位制名字,比如java.lang.String就是String類的二進位制名字)
, a class loader should attempt to
* locate or generate data that constitutes a definition for the class(那麼類載入器嘗試去定位(已經存在我們的磁碟檔案中)
或者生成(為啥有生成,存在動態代理)構成class的定義資料).
A typical strategy is to transform the name into a file name and then read a
* "class file" of that name from a file system.(通常的策略就是把我們的類的二進位制名稱轉換為我們的檔名稱 
比如java.lang.String 轉換為java/lang/String)
那麼就會根據這個檔名稱去檔案系統找到該class檔案。
*
* <p> Every {@link Class <tt>Class</tt>} object contains a {@link
* Class#getClassLoader() reference} to the <tt>ClassLoader</tt> that defined
* it. (每一個通過類載入器載入的class檔案後會返回一個Class物件,該Class物件會包含一個載入他的ClassLoader的物件引用)
*
* <p> <tt>Class</tt> objects for array classes are not created by class
* loaders, but are created automatically as required by the Java runtime.
我們的陣列物件的Class物件不是由我們的類載入器建立的,而是由我們的jvm根據需要在執行時間建立出來的.
* The class loader for an array class, as returned by {@link
* Class#getClassLoader()} is the same as the class loader for its element
* type;(我們獲取到了陣列型別的CLass物件,通過該Class物件呼叫getClassLoader()返回的是跟我們陣列元素中的類載入器是一樣的)
if the element type is a primitive type, then the array class has no
* class loader.(如果我們原生的陣列 那麼該陣列的Class沒有類載入器)
* <p> Applications implement subclasses of <tt>ClassLoader</tt> in order to
* extend the manner in which the Java virtual machine dynamically loads
* classes.(我們通過實現ClassLoader,可以動態來擴充套件我們類載入的方式)
* <p> Class loaders may typically be used by security managers to indicate
* security domains.
* <p> The <tt>ClassLoader</tt> class uses a delegation model to search for
* classes and resources.(ClassLoader使用雙親委派模型來尋找類和資源) Each instance of <tt>ClassLoader</tt> has an
* associated parent class loader(classloader的每一個例項都會有一個parent的classLoader). When requested to find a class or
* resource, a <tt>ClassLoader</tt> instance will delegate the search for the
* class or resource to its parent class loader before attempting to find the
* class or resource itself(當我們發起類載入的請求,那麼類載入器自己去尋找資源之前委託給父類). The virtual machine's built-in class loader,
* called the "bootstrap class loader", (虛擬機器內嵌的classLoader是叫做啟動類載入器)does not itself have a parent but may
* serve as the parent of a <tt>ClassLoader</tt> instance.(啟動類載入器是沒有雙親的,但是他可以作為其他類載入器的雙親)
* <p> Class loaders that support concurrent loading of classes are known as
* <em>parallel capable</em> class loaders(若一個類載入器支援並行載入的話,那麼這個類載入器就叫做並行類載入器) and are required to register
* themselves at their class initialization time by invoking the
* {@link
* #registerAsParallelCapable <tt>ClassLoader.registerAsParallelCapable</tt>}
* method.(若一個類載入器想要成為一個並行類載入器的話,那麼在該類載入器初始化的時候要求去呼叫ClassLoader.registerAsParallelCapable方法)
Note that the <tt>ClassLoader</tt> class is registered as parallel
* capable by default(預設情況下當前的這個類ClassLoader這個抽象類模式是並行載入器器). However, its subclasses still need to register themselves
* if they are parallel capable.(然而我們的子載入器需要成為並行的內載入器,需要註冊自己為並行的類載入器) <br>
* In environments in which the delegation model is not strictly
* hierarchical(若我們的類載入器不是屬於雙親委派的模型情況下), class loaders need to be parallel capable(類載入器需要註冊為並行的載入器), otherwise class
* loading can lead to deadlocks because the loader lock is held for the
* duration of the class loading process (see {@link #loadClass
* <tt>loadClass</tt>} methods).(不然在內載入的階段會導致死鎖)
* <p> Normally, the Java virtual machine loads classes from the local file
* system in a platform-dependent manner(通常情況下 jvm從本地磁碟下去載入類). For example, on UNIX systems, the
* virtual machine loads classes from the directory defined by the
* <tt>CLASSPATH</tt> environment variable.(在unix系統中,虛擬機器從classpath下載入類)
* <p> However, some classes may not originate from a file(然而,有些class檔案不是存在我們的磁碟檔案中); they may originate
* from other sources, such as the network(比如從網路上), or they could be constructed by an
* application(動態代理生產的). The method {@link #defineClass(String, byte[], int, int)
* <tt>defineClass</tt>} converts an array of bytes into an instance of class
* <tt>Class</tt>. (我們的defineClass方法會把位元組陣列轉為我們一個class的例項)Instances of this newly defined class can be created using
* {@link Class#newInstance <tt>Class.newInstance</tt>}.這個例項可以通過我們的newInstance來呼叫
全盤委託模型:由我們classloader載入出來的class那麼該類中的方法或建構函式可能引用其他類,jvm會呼叫同一個類載入器去載入被應用的類
* <p> The methods and constructors of objects created by a class loader may
* reference other classes(). To determine the class(es) referred to, the Java
* virtual machine invokes the {@link #loadClass <tt>loadClass</tt>} method of
* the class loader that originally created the class.
* <p> For example, an application could create a network class loader to
* download class files from a server. Sample code might look like:
* <blockquote><pre>
* ClassLoader loader&nbsp;= new NetworkClassLoader(host,&nbsp;port);
* Object main&nbsp;= loader.loadClass("Main", true).newInstance();
* &nbsp;.&nbsp;.&nbsp;.
* </pre></blockquote>
我們自定義的class laoder 需要重寫我們的ClassLoader類的findClass 和我們的loadClassData方法
* <p> The network class loader subclass must define the methods {@link
* #findClass <tt>findClass</tt>} and <tt>loadClassData</tt> to load a class
* from the network. Once it has downloaded the bytes that make up the class,
* it should use the method {@link #defineClass <tt>defineClass</tt>} to
* create a class instance. A sample implementation is:
* <blockquote><pre>
* class NetworkClassLoader extends ClassLoader {
* String host;
* int port;
* public Class findClass(String name) {
* byte[] b = loadClassData(name);
* return defineClass(name, b, 0, b.length);
* }
* private byte[] loadClassData(String name) {
* // load the class data from the connection
* &nbsp;.&nbsp;.&nbsp;.
* }
* }
* </pre></blockquote>
* <h3> <a name="name">Binary names</a> </h3>
* <p> Any class name provided as a {@link String} parameter to methods in
* <tt>ClassLoader</tt> must be a binary name as defined by
* <cite>The Java&trade; Language Specification</cite>.
* <p> Examples of valid class names include:
* <blockquote><pre>
* "java.lang.String" 表示我們的String類的二進位制名稱
* "javax.swing.JSpinner$DefaultEditor" 表示JSpinner的內部類DefaultEditor的二進位制名稱
* "java.security.KeyStore$Builder$FileBuilder$1" 表示java.security.KeyStore類的內部類Builder類的內部類的FileBuilder的第一個內部類
* "java.net.URLClassLoader$3$1" 表示java.net.URLClassLoader類中第三個內部類中的第一個內部類
* </pre></blockquote>
* @see #resolveClass(Class)
* @since 1.0
*/
public abstract class ClassLoader {}

通過上面文件的描述我們可以實現ClassLoader這個類就可以動態來擴充套件我們類載入的方式。從上面的文件中也可以看出,載入一個class檔案需要用到loadClass方法,並且還要重寫ClassLoader類的findClass 和我們的loadClassData方法

接下來對上述的幾個重要的方法的java doc進行翻譯

4.2:我們的ClassLoader類載入器重要方法詳解

  • loadClass方法詳解
/**
* Loads the class with the specified <a href="#name">binary name</a>. (根據二進位制名稱進行載入) The
* default implementation of this method searches for classes in the(下面是呼叫方法的順序)
* following order:
* <ol>
* <li><p> Invoke {@link #findLoadedClass(String)} to check if the class
* has already been loaded. </p></li> 呼叫findloaderClass檢查類是否被載入過
* <li><p> Invoke the {@link #loadClass(String) <tt>loadClass</tt>} method
* on the parent class loader(首先會呼叫父類的loaderClass來載入). If the parent is <tt>null</tt> the class
* loader built-in to the virtual machine is used(若發現父類是null,那就說明已經到了頂層的啟動類載入器),
instead. </p></li>
* <li><p> Invoke the {@link #findClass(String)} method to find the
* class. </p></li> 接下來就呼叫我們的findClass方法來查詢class
* </ol>
* <p> If the class was found using the above steps(若通過上述步驟找到了class檔案), and the
* <tt>resolve</tt> flag is true(那麼就設定resolve為true), this method will then invoke the {@link
* #resolveClass(Class)} method on the resulting <tt>Class</tt> object.(解析來那麼就通過呼叫resolverClass
返回物件)
* <p> Subclasses of <tt>ClassLoader</tt> are encouraged to override {@link
* #findClass(String)}, rather than this method. </p>
強烈的要求我們子類實現ClassLoader 那麼我們必須要從寫findClass方法
* <p> Unless overridden, this method synchronizes on the result of
* {@link #getClassLoadingLock <tt>getClassLoadingLock</tt>} method
* during the entire class loading process.
* @param name
* The <a href="#name">binary name</a> of the class
* @param resolve
* If <tt>true</tt> then resolve the class
* @return The resulting <tt>Class</tt> object
* @throws ClassNotFoundException
* If the class could not be found
*/
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
//檢查根據二進位制名稱是否被載入過
Class<?> c = findLoadedClass(name);
//沒有載入過
if (c == null) {
long t0 = System.nanoTime();
try {
// 判斷有沒有父類,有父類 呼叫父類的loadClass
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();
//呼叫findClass去找
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) {
//解析我們的class進行連線 等操作
resolveClass(c);
}
return c;
}
}

上面這段java doc文件中有幾個重要的點需要注意一下

1:根據二進位制名稱進行載入

2:呼叫findloaderClass檢查類是否被載入過

3:首先會呼叫父類的loaderClass來載入

4:若發現父類是null,那就說明已經到了頂層的啟動類載入器

5:接下來就呼叫我們的findClass方法來查詢class

這幾部是類載入器載入類的載入過程,比較重要的過程。希望各位小夥伴兒一定要結合原始碼牢記。

上述又提到了另一個方法

  • findClass()方法
/**
* Finds the class with the specified <a href="#name">binary name</a>.通過給定的二進位制名稱查詢出class檔案
* This method should be overridden by class loader implementations(這個方法應該被遵循雙親委託模型的子類重寫) that
* follow the delegation model for loading classes(), and will be invoked by
* the {@link #loadClass <tt>loadClass</tt>} method after checking the
* parent class loader for the requested class(這個方法會被loadClass方法呼叫,在父類載入器檢查了是否載入過後). The default implementation
* throws a <tt>ClassNotFoundException</tt>.
*
* @param name
* The <a href="#name">binary name</a> of the class
*
* @return The resulting <tt>Class</tt> object
*
* @throws ClassNotFoundException
* If the class could not be found
*
* @since 1.2
*/
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
  • defineClass()方法
/**
這個方法的作用就是用來把位元組陣列轉為一個Class物件,這個物件
* Converts an array of bytes into an instance of class <tt>Class</tt>.
* Before the <tt>Class</tt> can be used it must be resolved(這個Class物件能被使用必須是經過解析的因為在解析階段會進行驗證). This method
* is deprecated in favor of the version that takes a <a
* href="#name">binary name</a> as its first argument, and is more secure. 就是不推薦用這個方法,而是
推薦用它重寫的方法,因為重寫方法安全些(因為重寫方法中一個引數是我們的是傳入二進位制模型,他是通過安全域進行保護的)
*
* @param b
* The bytes that make up the class data. The bytes in positions
* <tt>off</tt> through <tt>off+len-1</tt> should have the format
* of a valid class file as defined by
* <cite>The Java&trade; Virtual Machine Specification</cite>.
*
* @param off
* The start offset in <tt>b</tt> of the class data
*
* @param len
* The length of the class data
*
* @return The <tt>Class</tt> object that was created from the specified
* class data
*
* @throws ClassFormatError
* If the data did not contain a valid class
*
* @throws IndexOutOfBoundsException
* If either <tt>off</tt> or <tt>len</tt> is negative, or if
* <tt>off+len</tt> is greater than <tt>b.length</tt>.
*
* @throws SecurityException
* If an attempt is made to add this class to a package that
* contains classes that were signed by a different set of
* certificates than this class, or if an attempt is made
* to define a class in a package with a fully-qualified name
* that starts with "{@code java.}".
*
* @see #loadClass(String, boolean)
* @see #resolveClass(Class)
*
* @deprecated Replaced by {@link #defineClass(String, byte[], int, int)
* defineClass(String, byte[], int, int)}
*/
@Deprecated
protected final Class<?> defineClass(byte[] b, int off, int len)
throws ClassFormatError
{
return defineClass(null, b, off, len, null);
}

以上兩個方法在註釋中均已經解釋。

看完以上的java doc文件,那麼我們就可以來寫一個自定義的類載入器了。

建立自定義類載入的步驟如下:

1:建立一個類名為Test01ClassLoader,這個是我們自己的類載入器

2:繼承ClassLoader類,並且重寫findClass方法。

3:建立方法loadClassData構建一個byte[]陣列。

4:在findClass中呼叫父類的defineClass方法,傳入相關資訊(引數name是名稱,傳入構建好的byte陣列,起始下標從0開始,最後是陣列長度)

5:建構函式不能少。如果帶引數,意為你指定雙親載入器是誰(類載入器雙親委託模型後面文章中介紹,先記住自定義類載入器的寫法和步驟即可)

6:最後就是在main函式中用我們自定義的類載入器去載入我們指定的類來測試。。。

package com.test;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;

/**
 * jvm 類載入器 第一章
 * @author 奇客時間-時光
 * 自定義類載入器
 * 1,繼承ClassLoader
 * 2,通過構造器指定當前類載入器的父載入器
 * 3,重寫findClass方法
 * 4,構造一個byte[]字數
 * 5,呼叫this.defineClass方法建立classLoader的例項
 */
public class Test01ClassLoader extends ClassLoader {

    private String classLoaderName;

    //宣告檔案字尾
    private final String x = ".class";

    //宣告檔案路徑
    private String path;

    public void setPath(String path) {
        this.path = path;
    }

    /**
     * 使用預設的類載入器作為當前類載入器的雙親{APPClassLoader作為雙親}
     * @param classLoaderName
     */
    public Test01ClassLoader(String classLoaderName){
        super();//指定系統類載入器為我們的父載入器
        this.classLoaderName = classLoaderName;
    }

    /**
     * 使用我們自己指定的類載入器作為當前類載入器的雙親
     * @param parentClassLoaderNane
     * @param classLoaderName
     */
    public Test01ClassLoader(ClassLoader parentClassLoaderNane,String classLoaderName){
        super(parentClassLoaderNane);
        this.classLoaderName = classLoaderName;
    }

    /**
     * 重寫findClass方法,這一步非常重要必不可少
     * @param name
     * @return
     * @throws ClassNotFoundException
     */
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        System.out.println("自己的類載入器被載入了");
        byte[] bytes = this.loadClassData(name);
        return this.defineClass(name,bytes,0,bytes.length);
    }

    /**
     * 返回一個位元組數字通過給定的類名
     * @param name
     * @return
     */
    private byte[] loadClassData(String name ){

        InputStream inputStream = null;
        byte[] bytes = null;
        ByteArrayOutputStream byteArrayOutputStream = null;
        try {
            name = name.replace(".","\\");
            inputStream= new FileInputStream(this.path+new File(name+this.x));
            byteArrayOutputStream = new ByteArrayOutputStream();

            int ch = 0;
            while(-1 !=(ch= inputStream.read())){
                byteArrayOutputStream.write(ch);
            }
            bytes=byteArrayOutputStream.toByteArray();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            try {
                inputStream.close();
                byteArrayOutputStream.close();
            }catch (Exception e){
                e.printStackTrace();
            }
        }
        return bytes;
    }


    public static void main(String[] args) throws Exception {
        //建立自定義類載入器的一個例項,並且通過構造器指定名稱
        Test01ClassLoader myClassLoader = new Test01ClassLoader("loader1");
        //設定載入路徑
        myClassLoader.setPath("I:\\jvm\\out\\production\\jvm-classloader\\");
        //呼叫loadClass方法來載入我們的class檔案
        Class<?> classz = myClassLoader.loadClass("com.test.Dog");
        //通過構造器建立例項
        Object object = classz.getDeclaredConstructor().newInstance();
        //檢視我們建立的例項是由哪個類載入器載入的
        System.out.println(object.getClass().getClassLoader());

        /*myClassLoader.setPath("D:\\classes\\");
        Class classz1 = myClassLoader.loadClass("com.jdyun.jvm05.Test");
        Object object1 = classz1.getDeclaredConstructor().newInstance();
        System.out.println(classz1.getClassLoader());
        */
        /*System.out.println(System.getProperty("sun.boot.class.path"));
        System.out.println(System.getProperty("java.ext.dirs"));
        System.out.println(System.getProperty("java.class.path"));
        System.out.println(Test12.getSystemClassLoader());*/

        /*MyTest02[] list = new MyTest02[1];
        System.out.println(list.getClass().getClassLoader());*/

    }

}

上述程式碼執行結果顯示還是通過AppClassLoader來載入的Dog類。那麼這是為什麼呢?因為我們在建構函式中指定的將系統類載入器也就是我們的AppClassLoader作為我們的雙親,那麼根據雙親委託模型,父載入器能載入的類,就由父載入器來載入,所以我們自定義的類載入沒有載入到Dog類。(雙親委託模型後面文章會詳細介紹。先記住這個結論就好。)

接下來我們對程式碼進行一點兒改造,來測試我們自定義的類載入是否生效了。

我在G:\jdyun-jvm\out\production\jdyun-jvm\\com\\jdyun\\jvm05\\目錄下建立一個class檔案Test.class,執行結果如下圖所示

我把載入的路徑改變,這樣我們我們的AppClassloader他只會載入classPath路徑下的檔案,而我們在外部指定的檔案,就自然而然的被我們的自定義類載入器給載入了。

接下來我們對程式碼進行一下小小的改動,我們建立兩個自定義類載入器的例項,然後看看載入出來的Class物件是不是一樣的。

public static void main(String[] args) throws Exception {
        //建立自定義類載入器的一個例項,並且通過構造器指定名稱
        Test01ClassLoader myClassLoader = new Test01ClassLoader("loader1");
        //設定載入路徑
        myClassLoader.setPath("G:\\jdyun-jvm\\out\\production\\jdyun-jvm\\");
        //呼叫loadClass方法來載入我們的class檔案
        Class<?> classz = myClassLoader.loadClass("com.jdyun.jvm05.Test");
        //通過構造器建立例項
        Object object = classz.getDeclaredConstructor().newInstance();
        //檢視我們建立的例項是由哪個類載入器載入的
        System.out.println(classz.getClassLoader());
        System.out.println(classz.hashCode());

        //建立自定義類載入器的一個例項,並且通過構造器指定名稱
        Test01ClassLoader myClassLoader2 = new Test01ClassLoader("loader1");
        //設定載入路徑
        myClassLoader2.setPath("G:\\jdyun-jvm\\out\\production\\jdyun-jvm\\");
        //呼叫loadClass方法來載入我們的class檔案
        Class<?> classz2 = myClassLoader2.loadClass("com.jdyun.jvm05.Test");
        //通過構造器建立例項
        Object object2 = classz2.getDeclaredConstructor().newInstance();
        //檢視我們建立的例項是由哪個類載入器載入的
        System.out.println(classz2.getClassLoader());
        System.out.println(classz2.hashCode());

    }


列印結果:
自己的類載入器被載入了
com.test.Test01ClassLoader@1540e19d
21685669
自己的類載入器被載入了
com.test.Test01ClassLoader@7f31245a
325040804

通過上面程式碼可知,同一個類載入器載入的同一個Class檔案載入出來的Class物件不是同一個。並且兩個class物件之間也是不能相互轉換的。

好了各位小夥伴兒,第一篇文章我們們就先介紹到這裡,大家現在瞭解一下4種類載入器即可,此專題的文章後面還會陸續連載,期待各位小夥伴的閱讀。

另外筆者在公眾號:奇客時間,給大家收錄了1000多道今年網際網路公司的面試真題
面試真題-回覆關鍵字形式:公司-部門-面試輪次,例如:阿里-螞蟻金服-一面,自動回覆面試真題;

當前已經收錄如下:
位元組跳動-抖音-面試輪次, 搜狐-搜尋組-面試輪次, OPPO-商城-面試輪次, 58同城-基礎架構部-面試輪次,湖南臺-芒果TV-面試輪次 , 騰訊-乘車碼-面試輪次 , 騰訊-微信支付-面試輪次 , 騰訊-零售新業務-面試輪次 , 騰訊-直播平臺-面試輪次, 快手-廣告業務部-面試輪次 , 貝殼找房-商品組-面試輪次 , 百度-資訊流-面試輪次 , 京東-零售-面試輪次 , 京東-物流-面試輪次 , 京東-電商-面試輪次 , 滴滴-小桔車服-面試輪次 , 滴滴-金融-面試輪次 , 阿里-高德-面試輪次 , 阿里-大文娛-面試輪次 , 阿里-健康-面試輪次 , 阿里-螞蟻金服-面試輪次 , 美團-外賣-面試輪次 , 美團-風控-面試輪次

相關文章