面試官:說說類加載的幾個階段
一、摘要
我們知道 Java 是先通過編譯器將.java類文件轉成.class字節(jié)碼文件,然后再通過虛擬機將.class字節(jié)碼文件加載到內(nèi)存中來實現(xiàn)應用程序的運行。
那么虛擬機是什么時候加載class文件?如何加載class文件?class文件進入到虛擬機后發(fā)生了哪些變化?
今天我們就一起來了解一下,虛擬機是如何加載類文件的。
二、類加載的時機
經(jīng)常有面試官問,“類什么時候加載”和“類什么時候初始化”,從內(nèi)容上來說,似乎都在問同一個問題:class文件是什么時候被虛擬機加載到內(nèi)存中,并進入可以使用的狀態(tài)?
從虛擬機角度來說,加載和初始化是類的加載過程中的兩個階段。
對于“什么時候加載”,Java 虛擬機規(guī)范中并沒有約束,每個虛擬機實例都可以按自身需要來自由實現(xiàn)。但基本上都遵循類在進行初始化之前,需要先進行加載class文件。
對于“什么時候初始化”,Java 虛擬機規(guī)范有明確的規(guī)定,當符合以下條件時(包括但不限),并且虛擬機在內(nèi)存中沒有找到對應的類信息,必須對類進行“初始化”操作:
- 使用new實例化對象時,讀取或者設置一個類的靜態(tài)字段或方法時
- 反射調(diào)用時,例如Class.forName("com.xxx.Test")
- 初始化一個類的子類,會首先初始化子類的父類
- Java 虛擬機啟動時標明的啟動類,比如main方法所在的類
- JDK8 之后,接口中存在default方法,這個接口的實現(xiàn)類初始化時,接口會在它之前進行初始化
類在初始化開始之前,需要先經(jīng)歷加載、驗證、準備、解析這四個階段的操作。
下面我們一起來看看類的加載過程。
三、類的加載過程
當一個類需要被加載到虛擬機中執(zhí)行時,虛擬機會通過類加載器,將其.class文件中的字節(jié)碼信息在內(nèi)存中轉化成一個具體的java.lang.Class對象,以便被調(diào)用執(zhí)行。
類從被加載到虛擬機內(nèi)存中開始,到卸載出內(nèi)存,整個生命周期包括七個階段:加載、驗證、準備、解析、初始化、使用和卸載,可以用如下圖來簡要概括。
圖片
其中類加載的過程,可以用三個步驟(五個階段)來簡要描述:加載 -> 連接(驗證、準備、解析)-> 初始化。(驗證、準備、解析這3個階段統(tǒng)稱為連接)
其次加載、驗證、準備和初始化這四個階段發(fā)生的順序是確定的,必須按照這種順序按部就班的開始,而解析階段則不一定。在某些情況下解析階段可以在初始化階段之后開始,這是為了支持 Java 語言的運行時綁定,也稱為動態(tài)綁定或晚期綁定。
同時,這五個階段并不是嚴格意義上的按順序完成,在類加載的過程中,這些階段會互相混合,可能有些階段完成了,有些階段沒有完成,會交叉運行,最終完成類的加載和初始化。
接下來依此分解一下加載、驗證、準備、解析、初始化這五個步驟,這五個步驟組成了一個完整的類加載過程。使用沒什么好說的,卸載通常屬于 GC 的工作,當一個類沒有被任何地方引用并且類加載器已被 GC 回收,GC 會將當前類進行卸載,在后續(xù)的文章我們會介紹 GC 的工作機制。
3.1、加載
加載是類加載的過程的第一個階段,這個階段的主要工作是查找并加載類的二進制數(shù)據(jù),在虛擬機中,類的加載有兩種觸發(fā)方式:
- 預先加載:指的是虛擬機啟動時加載,例如JAVA_HOME/lib/下的rt.jar下的.class文件,這個jar包里面包含了程序運行時常用的文件內(nèi)容,例如java.lang.*、java.util.*、java.io.*等等,因此會隨著虛擬機啟動時一起加載到內(nèi)存中。要證明這一點很簡單,自己可以寫一個空的main函數(shù),設置虛擬機參數(shù)為-XX:+TraceClassLoading,運行程序就可以獲取類加載的全部信息
- 運行時加載:虛擬機在用到一個.class文件的時候,會先去內(nèi)存中查看一下這個.class文件有沒有被加載,如果沒有,就會按照類的全限定名來加載這個類;如果有,就不會加載。
無論是哪種觸發(fā)方式,虛擬機在加載.class文件時,都會做以下三件事情:
- 1.通過類的全限定名定位.class文件,并獲取其二進制字節(jié)流
- 2.將類信息、靜態(tài)變量、字節(jié)碼、常量這些.class文件中的內(nèi)容放入運行時數(shù)據(jù)區(qū)的方法區(qū)中
- 3.在內(nèi)存中生成一個代表這個.class文件的java.lang.Class對象,作為方法區(qū)這個類的各種數(shù)據(jù)的訪問入口,一般這個java.lang.Class對象會存在 Java 堆中
虛擬機規(guī)范對這三點的要求并不具體,因此具體虛擬機實現(xiàn)的靈活度都很大。比如第一條,沒有指明二進制字節(jié)流要從哪里來,單單就這一條,就能變出許多花樣來,比如下面幾種加載方式:
- 從 zip、jar、ear、war 等歸檔文件中加載.class文件
- 通過網(wǎng)絡下載并加載.class文件,典型應用就是 Applet
- 將Java源文件動態(tài)編譯為.class文件,典型應用就是動態(tài)代理技術
- 從數(shù)據(jù)庫中提取.class文件并進行加載
總的來說,加載階段(準確地說,是加載階段獲取類的二進制字節(jié)流的動作)對于開發(fā)者來說是可控性最強的一個階段。因為開發(fā)者既可以使用系統(tǒng)提供的類加載器來完成加載,也可以自定義類加載器來完成加載。
3.2、驗證
驗證是連接階段的第一步,這一階段的目的是為了確保.class文件的字節(jié)流中包含的信息符合當前虛擬機的要求,并且不會危害虛擬機自身的安全。
Java 語言本身是比較安全的語言,但是正如上面說到的.class文件未必是從 Java 源碼編譯而來,可以使用任何途徑來生成并加載。虛擬機如果不檢查輸入的字節(jié)流,對其完全信任的話,很可能會因為載入了有害的字節(jié)流而導致系統(tǒng)崩潰,所以驗證是虛擬機對自身保護的一項重要工作。
驗證階段大致會完成 4 項檢驗工作:
- 文件格式驗證:驗證字節(jié)流是否符合Class文件格式的規(guī)范,例如:是否以0xCAFEBABE開頭、主次版本號是否在當前虛擬機的處理范圍之內(nèi)、常量池中的常量是否有不被支持的類型等
- 元數(shù)據(jù)驗證:對字節(jié)碼描述的元數(shù)據(jù)信息進行語義分析,要符合 Java 語言規(guī)范,例如:是否繼承了不允許被繼承的類(例如 final 修飾過的)、類中的字段、方法是否和父類產(chǎn)生矛盾等等
- 字節(jié)碼驗證:對類的方法體進行校驗分析,確保這些方法在運行時是合法的、符合邏輯的
- 符號引用驗證:確保解析動作能正確執(zhí)行,例如:確保符號引用的全限定名能找到對應的類,符號引用中的類、字段、方法允許被當前類所訪問等等
驗證階段是非常重要的,但不是必須的,它對程序運行期沒有影響,如果所引用的類經(jīng)過反復驗證,那么可以考慮采用-Xverify:none參數(shù)來關閉大部分的類驗證措施,以縮短虛擬機類加載的時間。
3.3、準備
準備是連接階段的第二步,這個階段的主要工作是正式為類變量分配內(nèi)存并設置其初始值的階段,這些變量所使用的內(nèi)存都將在方法區(qū)中分配。
不過這個階段,有幾個知識點需要注意一下:
- 1.這時候進行內(nèi)存分配的僅僅是類變量(被static修飾的變量),而不是實例變量,實例變量將會在對象實例化的時候隨著對象一起分配在 Java 堆中
- 2.這個階段會設置變量的初始值,值為數(shù)據(jù)類型默認的零值(如 0、0L、null、false 等),不是在代碼中被顯式地賦予的值;但是當字段被final修飾時,這個初始值就是代碼中顯式地賦予的值
- 3.在 JDK1.8 取消永久代后,方法區(qū)變成了一個邏輯上的區(qū)域,這些類變量的內(nèi)存實際上是分配在 Java 堆中的,跟 JDK1.7 及以前的版本稍有不同
關于第二個知識點,我們舉個簡單的例子進行講解,比如public static int value = 123,value在準備階段過后是0而不是123。
因為這時候尚未開始執(zhí)行任何 Java 方法,把value賦值為123的public static指令是在程序編譯后存放于類構造器<clinit>()方法之中的,因此把value賦值為123的動作將在初始化階段才會執(zhí)行。
假如被final修飾,比如public static final int value = 123就不一樣了,編譯時Javac將會為value生成ConstantValue屬性,在準備階段,虛擬機就會給value賦值為123,因為這個變量無法被修改,會存入類的常量池中。
各個數(shù)據(jù)類型的零值如下圖:
數(shù)據(jù)類型 | 零值 |
byte | 0 |
short | 0 |
int | 0 |
long | 0L |
float | 0.0f |
double | 0.0d |
boolean | false |
char | \u0000 |
reference | null |
3.4、解析
解析是連接階段的第三步,這個階段的主要工作是虛擬機會把這個.class文件中常量池內(nèi)的符號引用轉換為直接引用。
主要解析的是類或接口、字段、方法等符號引用,我們可以把解析階段中符號引用轉換為直接引用的過程,理解為當前加載的這個類和它所引用的類,正式進行“連接“的過程。
我們先來了解一下符號引用和直接引用有什么區(qū)別:
- 符號引用:這個其實是屬于編譯原理方面的概念,Java 代碼在編譯期間,是不知道最終引用的類型,具體指向內(nèi)存中哪個位置的,這時候會使用一個符號引用來表示具體引用的目標是"誰",符號引用和虛擬機的內(nèi)存布局是沒有關系的
- 直接引用:指的是可以直接或間接指向目標內(nèi)存位置的指針或句柄,直接引用和虛擬機實現(xiàn)的內(nèi)存布局是有關系的
符號引用轉換為直接引用,可以理解成將某個符號與虛擬機中的內(nèi)存位置建立連接,通過指針或句柄來直接訪問目標。
與此同時,同一個符號引用在不同的虛擬機實現(xiàn)上翻譯出來的直接引用一般不會相同。
3.5、初始化
初始化是類加載的過程的最后一步,這個階段的主要工作是執(zhí)行類構造器 <clinit>()方法的過程。
簡單的說,初始化階段做的事就是給static變量賦予用戶指定的值,同時類中如果存在static代碼塊,也會執(zhí)行這個靜態(tài)代碼塊里面的代碼。
初始化階段,虛擬機大致依此會進行如下幾個步驟的操作:
- 1.檢查這個類是否被加載和連接,如果沒有,則程序先加載并連接該類
- 2.檢查該類的直接父類有沒有被初始化,如果沒有,則先初始化其直接父類
- 3.類中如果有多個初始化語句,比如多個static代碼塊,則依次執(zhí)行這些初始化語句
有個地方需要注意的是:虛擬機會保證類的初始化在多線程環(huán)境中被正確地加鎖、同步執(zhí)行,所以無需擔心是否會出現(xiàn)變量初始化時線程不安全的問題。
如果多個線程同時去初始化一個類,那么只會有一個線程去執(zhí)行這個類的<clinit>()方法,其他線程都會阻塞等待,直到<clinit>()方法執(zhí)行完畢。同時,同一個類加載器下,一個類只會初始化一次,如果檢查到當前類沒有初始化,執(zhí)行初始化;反之,不會執(zhí)行初始化。
與此同時,只有當對類的主動使用的時候才會觸發(fā)類的初始化,觸發(fā)時機主要有以下幾種場景:
- 1.創(chuàng)建類的實例對象,比如new一個對象操作
- 2.訪問某個類或接口的靜態(tài)變量,或者對該靜態(tài)變量賦值
- 3.調(diào)用類的靜態(tài)方法
- 4.反射操作,比如Class.forName("xxx")
- 5.初始化某個類的子類,則其父類也會被初始化,并且父類具有優(yōu)先被初始化的優(yōu)勢
- 6.Java 虛擬機啟動時被標明為啟動類的類,比如SpringBootApplication入口類
最后,<clinit>()方法和<init>()方法是不同的,一個是類構造器初始化,一個是實例構造器初始化,千萬別搞混淆了啊。
3.6、小結
當一個符合 Java 虛擬機規(guī)范的.class字節(jié)碼文件,經(jīng)歷加載、驗證、準備、解析、初始化這些 5 個階段相互協(xié)作執(zhí)行完成之后,虛擬機會將此文件的二進制數(shù)據(jù)導入運行時數(shù)據(jù)區(qū)的方法區(qū)內(nèi),然后在堆內(nèi)存中,創(chuàng)建一個java.lang.Class類的對象,這個對象描述了這個類所有的信息,同時提供了這個類在方法區(qū)的訪問入口。
可以用如下圖來簡要描述。
圖片
與此同時,在方法區(qū)中,使用同一加載器的情況下,每個類只會有一份Class字節(jié)流信息;在堆內(nèi)存中,使用同一加載器的情況下,每個類也只會有一份java.lang.Class類的對象。
四、類加載器
在上文類的加載過程中,我們有提到在加載階段,通過一個類的全限定名來獲取此類的二進制字節(jié)流操作,其實類加載器就是用來實現(xiàn)這個操作的。
在虛擬機中,任何一個類,都需要由加載它的類加載器和這個類本身一同確立其唯一性,每一個類加載器,都擁有一個獨立的類名稱空間,對于類也同樣如此。
簡單的說,在虛擬機中看兩個類是否相同,只有在這兩個類是由同一個類加載器加載的前提下才有意義,否則即使這兩個類來源于同一個.class文件,被同一個虛擬機加載,但是它們的類加載器不同,這兩個類必定不相等。
當年為了滿足瀏覽器上 Java Applet 的需求,Java 的開發(fā)團隊設計了類加載器,它獨立于 Java 虛擬機外部,同時也允許用戶按自身需要自行實現(xiàn)類加載器。通過類加載器,可以讓同一個類可以實現(xiàn)訪問隔離、OSGi、程序熱部署等等場景。發(fā)展至今,類加載器已經(jīng)是 Java 技術體系的一塊重要基石。
4.1、類加載器介紹
如果要查找類加載器,通過Thread.currentThread().getContextClassLoader()方法可以獲取。
簡單示例如下:
public class ClassLoaderTest {
public static void main(String[] args) {
ClassLoader loader = Thread.currentThread().getContextClassLoader();
System.out.println("current loader:" + loader);
System.out.println("parent loader:" + loader.getParent());
System.out.println("parent parent loader:" + loader.getParent().getParent());
}
}輸出結果如下:
current loader:sun.misc.Launcher$AppClassLoader@18b4aac2
parent loader:sun.misc.Launcher$ExtClassLoader@511d50c0
parent parent loader:null從運行結果可以看到,當前的類加載器是AppClassLoader,它的上一級是ExtClassLoader,再上一級是null。
其實ExtClassLoader的上一級是有類加載器的,它叫Bootstrap ClassLoader,是一個啟動類加載器,由 C++ 實現(xiàn),不是 ClassLoader 子類,因此以 null 作為結果返回。
這幾種類加載器的層次關系,可以用如下圖來描述。
圖片
它們之間的啟動流程,可以通過以下內(nèi)容來簡單描述:
- 1.在虛擬機啟動后,會優(yōu)先初始化Bootstrap Classloader
- 2.接著Bootstrap Classloader負責加載ExtClassLoader,并且將 ExtClassLoader的父加載器設置為Bootstrap Classloader
- 3Bootstrap Classloader加載完ExtClassLoader后,就會加載AppClassLoader,并且將AppClassLoader的父加載器指定為 ExtClassLoader
因此,在加載 Java 應用程序中的class文件時,這里的父類加載器并不是通過繼承關系來實現(xiàn)的,而是互相配合進行加載。
站在虛擬機的角度,只存在兩種不同的類加載器:
- 啟動類加載器:它由 C++ 實現(xiàn)(這里僅限于 Hotspot,不同的虛擬機可能實現(xiàn)不太一樣),是虛擬機自身的一部分
- 其它類加載器:這些類加載器都由 Java 實現(xiàn),獨立于虛擬機之外,并且全部繼承自抽象類java.lang.ClassLoader,比如ExtClassLoader、AppClassLoader等,這些類加載器需要由啟動類加載器加載到內(nèi)存中之后才能去加載其他的類
站在開發(fā)者的角度,類加載器大致可以劃分為三類:
- 啟動類加載器:比如Bootstrap ClassLoader,負責加載<JAVA_HOME>\lib目錄,或者被-Xbootclasspath參數(shù)制定的路徑,例如jre/lib/rt.jar里所有的class文件。同時,啟動類加載器是無法被 Java 程序直接引用的
- 拓展類加載器:比如Extension ClassLoader,負責加載 Java 平臺中擴展功能的一些 jar 包,包括<JAVA_HOME>\lib\ext目錄中或java.ext.dirs指定目錄下的 jar 包。同時,開發(fā)者可以直接使用擴展類加載器
- 應用程序類加載器:比如Application ClassLoader,負責加載ClassPath路徑下所有 jar 包,如果應用程序中沒有自定義過自己的類加載器,一般情況下它就是程序中默認的類加載器
當然,如果有必要,也可以自定義類加載器,因為 JVM 自帶的 ClassLoader 只懂得從本地文件系統(tǒng)中加載標準的class文件,如果要從特定的場所取得class文件,例如數(shù)據(jù)庫中和網(wǎng)絡中,此時可以自己編寫對應的 ClassLoader 類加載器。
4.2、雙親委派模型
在上文中我們提到,在虛擬機中,任何一個類由加載它的類加載器和這個類一同來確立其唯一性。
也就是說,JVM 對類的唯一標識,可以簡單的理解為由ClassLoader id + PackageName + ClassName組成,因此在一個運行程序中有可能存在兩個包名和類名完全一致的類,但是如果這兩個類不是由一個 ClassLoader 加載,會被視為兩個不同的類,此時就無法將一個類的實例強轉為另外一個類,這就是類加載器的隔離性。
為了解決類加載器的隔離問題,JVM 引入了雙親委派模型。
雙親委派模式,可以用一句話來說表達:任何一個類加載器在接到一個類的加載請求時,都會先讓其父類進行加載,只有父類無法加載(或者沒有父類)的情況下,才嘗試自己加載。
大致流程圖如下:
圖片
使用雙親委派模式,可以保證,每一個類只會有一個類加載器。例如 Java 最基礎的 Object 類,它存放在 rt.jar 之中,這是 Bootstrap 的職責范圍,當向上委派到 Bootstrap 時就會被加載。
但如果沒有使用雙親委派模式,可以任由自定義加載器進行加載的話,Java 這些核心類的 API 就會被隨意篡改,無法做到一致性加載效果。
JDK 中ClassLoader.loadClass()類加載器中的加載類的方法,部分核心源碼如下:
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// 1.首先要保證線程安全
synchronized (getClassLoadingLock(name)) {
// 2.先判斷這個類是否被加載過,如果加載過,直接跳過
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// 3.有父類,優(yōu)先交給父類嘗試加載;如果為空,使用BootstrapClassLoader類加載器
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 父類加載失敗,這里捕獲異常,但不需要做任何處理
}
// 4.沒有父類,或者父類無法加載,嘗試自己加載
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;
}
}4.3、自定義類加載器
在上文中我們提及過,針對某些特定場景,比如通過網(wǎng)絡來傳輸 Java 類的字節(jié)碼文件,為保證安全性,這些字節(jié)碼經(jīng)過了加密處理,這時系統(tǒng)提供的類加載器就無法對其進行加載,此時我們可以自定義一個類加載器來完成文件的加載。
自定義類加載器也需要繼承ClassLoader類,簡單示例如下:
public class CustomClassLoader extends ClassLoader {
private String classPath;
public CustomClassLoader(String classPath) {
this.classPath = classPath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class<?> c = findLoadedClass(name);
if (c == null) {
byte[] data = loadClassData(name);
if (data == null) {
throw new ClassNotFoundException();
}
return defineClass(name, data, 0, data.length);
}
return null;
}
protected byte[] loadClassData(String name) {
try {
// package -> file folder
name = name.replace(".", "http://");
FileInputStream fis = new FileInputStream(new File(classPath + "http://" + name + ".class"));
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int len = -1;
byte[] b = new byte[2048];
while ((len = fis.read(b)) != -1) {
baos.write(b, 0, len);
}
fis.close();
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}相關的測試類如下:
package com.example;
public class ClassLoaderTest {
public static void main(String[] args) {
ClassLoader loader = Thread.currentThread().getContextClassLoader();
System.out.println("current loader:" + loader);
}
}將ClassLoaderTest.java源文件放在指定目錄下,并通過javac命令編譯成ClassLoaderTest.class,最后進行測試。
public class CustomClassLoaderTest {
public static void main(String[] args) throws Exception {
String classPath = "/Downloads";
CustomClassLoader customClassLoader = new CustomClassLoader(classPath);
Class<?> testClass = customClassLoader.loadClass("com.example.ClassLoaderTest");
Object obj = testClass.newInstance();
System.out.println(obj.getClass().getClassLoader());
}
}輸出結果:
com.example.CustomClassLoader@60e53b93在實際使用過程中,最好不要重寫loadClass方法,避免破壞雙親委派模型。
4.4、加載類的幾種方式
在類加載器中,有三種方式可以實現(xiàn)類的加載。
- 1.通過命令行啟動應用時由 JVM 初始化加載,在上文已提及過
- 2.通過Class.forName()方法動態(tài)加載
- 3.通過ClassLoader.loadClass()方法動態(tài)加載
其中Class.forName()和ClassLoader.loadClass()加載方法,稍有區(qū)別:
- Class.forName():表示將類的.class文件加載到 JVM 中之后,還會對類進行解釋,執(zhí)行類中的static方法塊;
- Class.forName(name, initialize, loader):支持通過參數(shù)來控制是否執(zhí)行類中的static方法塊;
- ClassLoader.loadClass():它只將類的.class文件加載到 JVM,但是不執(zhí)行類中的static方法塊,只有在newInstance()才會去執(zhí)行static方法塊;
我們可以看一個簡單的例子!
public class ClassTest {
static {
System.out.println("初始化靜態(tài)代碼塊!");
}
}public class CustomClassLoaderTest {
public static void main(String[] args) throws Exception {
// 獲取當前系統(tǒng)類加載器
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
// 1.使用Class.forName()來加載類,默認會執(zhí)行初始化靜態(tài)代碼塊
Class.forName(ClassTest.class.getName());
// 2.使用Class.forName()來加載類,指定false,不會執(zhí)行初始化靜態(tài)代碼塊
// Class.forName(ClassTest.class.getName(), false, classLoader);
// 3.使用ClassLoader.loadClass()來加載類,不會執(zhí)行初始化靜態(tài)代碼塊
// classLoader.loadClass(ClassTest.class.getName());
}
}運行結果如下:
初始化靜態(tài)代碼塊!切換不同的加載方式,會有不同的輸出結果!
4.5、小結
從以上的介紹中,針對類加載器的機制,我們可以總結出以下幾點:
- 全盤負責:當一個類加載器負責加載某個Class文件時,該Class所依賴的和引用的其他Class也將由該類加載器負責載入,除非顯示使用另外一個類加載器來加載
- 雙親委派:在接受類加載請求時,會讓父類加載器試圖加載該類,只有在父類加載器無法加載該類或者沒有父類時,才嘗試從自己的類路徑中加載該類
- 按需加載:用戶創(chuàng)建的類,通常加載是按需進行的,只有使用了才會被類加載器加載
- 緩存機制:有被加載過的Class文件都會被緩存,當要使用某個Class時,會先去緩存查找,如果緩存中沒有才會讀取Class文件進行加載。這就是為什么修改了Class文件后,必須重啟 JVM,程序的修改才會生效的原因
五、小結
本文從類的加載過程到類加載器,做了一次知識內(nèi)容講解,內(nèi)容比較多,如果有描述不對的地方,歡迎大家留言指出,不勝感激!
六、參考
1.https://zhuanlan.zhihu.com/p/25228545
2.http://www.ityouknow.com/jvm/2017/08/19/class-loading-principle.html
3.https://www.cnblogs.com/xrq730/p/4844915.html
4.https://www.cnblogs.com/xrq730/p/4845144.html

































