類的加載
類的加載是指將類的.class文件中二進制數(shù)據(jù)讀入到內(nèi)存中,然后將其放在運行時數(shù)據(jù)區(qū)的方法區(qū)
內(nèi),然后在內(nèi)存中創(chuàng)建愛你一個java.lang.Class
對象
規(guī)范并沒有說明Class對象應該存放在哪,HotSpot虛擬機將其放在方法區(qū)中,用來封裝類在方法區(qū)內(nèi)的數(shù)據(jù)結構
加載.class文件的方式
- 從本地系統(tǒng)中直接加載
- 從網(wǎng)絡下載.calss文件
- 從zip,jar等歸檔文件中加載
- 從專有數(shù)據(jù)庫中提取
- 將Java源文件動態(tài)編譯為.class文件
servlet技術
類加載器
類加載器用來把類加載到Java虛擬機中,從JDK1.2版本開始,類的加載過程采用雙親委托機制
,這種機制能保證Java平臺的安全性.
從源碼文檔中翻譯應該稱為父類委托模式
類加載器并不需要等到某個類被首次主動使用
時再加載它
- JVM規(guī)范允許類加載器在預料某個類將要被使用時就預先加載它,如果在預先加載的過程中遇到了.class文件缺失或者存在錯誤,類加載器必須在
程序首次主動
使用該類時才報告錯誤(LinkageError) - 如果一個類一直沒有被程序主動使用,那么累加載器就不會報告錯誤
JVM中的類加載器
根加載器(Bootstrap),
根加載器沒有父加載器
,主要負責虛擬機的核心類庫,如java.lang.*
等,java.lang.Object
是由根類加載器加載的,根類加載器的實現(xiàn)依賴于底層操作系統(tǒng)
,屬于虛擬機實現(xiàn)第一部分,它并沒有繼承java.lang.ClassLoader類.
啟動類加載器是特定于平臺的機器指令,它負責開啟整個加載過程
啟動類加載器還會負責加載JRE正常運行所需的基本組件.其中包括java.util
,java.lang
包中的類
擴展類加載器(Extension)
擴展類加載器的父加載器是根加載器
,從java.ext.dirs
系統(tǒng)屬性指定的目錄中加載類庫,或者再jre\lib\ext
子目錄下加載類庫,如果把用戶創(chuàng)建的JAR文件放在這個目錄下,會自動由擴展類加載器
加載,擴展類加載器是純Java類,是ClassLoader的子類
注意一點的是,拓展類加載器加載的是jar包內(nèi)的class文件
系統(tǒng)(應用)類加載器(System/Application)
系統(tǒng)類加載器
的父加載器為擴展類加載器
,從環(huán)境變量classpath或者系統(tǒng)屬性java.class.path
所制定的目錄加載類,它是用戶自定義的類加載器的默認父加載器,系統(tǒng)類加載器是純Java類,是ClassLoader的子類
用戶自定義的類加載器
除了虛擬機自帶的加載器外,用戶可以定制自己的類加載器.Java提供了抽象類ClassLoader.所有用戶自定義的加載器都應該繼承ClassLoader
AppClassLoader和ExtClassLoader都是Java類,所以需要類加載器進行加載,而這兩個類的類加載器就是bootstrapClassLoader
可以通過修改
System.getProperty(java.system.class.loader)對默認的SystemClassLoader進行修改
父親委托機制
在父親委托機制中,各個加載器按照父子關系形成樹形結構,除了根加載器之外,其余的類加載器有且只有一個父加載器.
簡單描述,就是一個類加載器要加載一個類,并不是由自身進行直接加載,而是通過向上尋找父加載器,直到?jīng)]有父加載器的類加載器,然后再從上至下嘗試加載,直至找到一個可以正確加載的類加載器,一般情況下,系統(tǒng)類加載器就能加載普通的類.
并不是所有的類加載器都必須遵守雙親委托的機制,具體實現(xiàn)可以根據(jù)需要進行改造
代碼示例,查看類的加載器
public class Test08 {
public static void main(String[] args) {
try {
Class<?> clzz = Class.forName("java.lang.String");
//如果返回null,證明是由BootStrap加載器進行加載的
System.out.println(clzz.getClassLoader());
Class<?> customClass = Class.forName("com.r09er.jvm.classloader.Custom");
System.out.println(customClass.getClassLoader());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
class Custom{
}
輸出
null
sun.misc.Launcher$AppClassLoader@18b4aac2
String的類加載器為null,證明String是由Bootstrap類加載器加載,因為根加載器是由C++實現(xiàn).所以會返回null
.
Custom的類加載器是Launcher$AppClassLoader,這個類是不開源的.但是是默認的系統(tǒng)(應用)類加載器
.
classLoader和初始化的時機
通過ClassLoader手動加載類,觀察是否會觸發(fā)類的初始化
public class Test12 {
public static void main(String[] args) throws Exception {
ClassLoader loader = ClassLoader.getSystemClassLoader();
Class<?> aClass = loader.loadClass("com.r09er.jvm.classloader.TestClassLoader");
System.out.println(aClass);
System.out.println("-------");
aClass = Class.forName("com.r09er.jvm.classloader.TestClassLoader");
System.out.println(aClass);
}
}
class TestClassLoader{
static {
System.out.println("Test classloader");
}
}
輸出
class com.r09er.jvm.classloader.TestClassLoader
-------
Test classloader
class com.r09er.jvm.classloader.TestClassLoader
結論
明顯可以看出,classLoader.load方法加載類,類并不會初始化,說明不是對類的主動使用,調(diào)用了Class.ForName
才進行初始化
不同的類加載器與加載動作分析
打印類加載器,由于根加載器由C++編寫,所以就會返回null
public static void main(String[] args) {
ClassLoader loader = ClassLoader.getSystemClassLoader();
System.out.println(loader);
//向上遍歷父classLoader
while (null != loader) {
loader = loader.getParent();
System.out.println(loader);
}
}
輸出結果
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@610455d6
null
獲取ClassLoader的途徑
- 通過類對象獲取ClassLoader,
clazz.getClassLoader()
- 通過線程獲取上限文ClassLoader,
Thread.currentThread().getContextLoader()
- 獲得系統(tǒng)(應用)ClassLoader,
ClassLoader.getSystemClassLoader()
- 獲得調(diào)用者的ClassLoader,
DriverManager.getClassLoader()
ClassLoader源碼分析
JavaDoc描述
類加載器是負責加載類
的對象,classLoader是抽象類.賦予類一個二進制名稱,一個類加載器應當嘗試定位
或生成
數(shù)據(jù),這些數(shù)據(jù)構成類的定義.一種典型的策略是將二進制名稱轉(zhuǎn)換為文件名忆矛,然后從文件系統(tǒng)中讀取該名稱的字節(jié)碼文件
。
每一個類
對象都包含定義該類
的classLoader引用(reference)
數(shù)組
對應的class對象并不是由類加載器創(chuàng)建的,而是由java虛擬機在需要時自動創(chuàng)建的.對于一個數(shù)組的類加載器,與這個數(shù)組元素的類加載器一致.如果數(shù)組是原生類型
,那這個數(shù)組將沒有classLoader
String[],則這個數(shù)組的類加載器是String的類加載器,使用的是Bootstrap類加載器
int[] ,這種基本類型的數(shù)組,是沒有類加載器的.
應用
實現(xiàn)classLoader的目的是為了拓展JVM動態(tài)加載類
ClassLoader使用了委托模型去尋找類的資源.ClassLoader的每一個實例都有會一個關聯(lián)的父ClassLoader,當需要尋找一個類的資源時,ClassLoader實例就會委托給父ClassLoader.虛擬機內(nèi)建的ClassLoader稱為BootstrapClassLoader
,BootstrapClassLoader
本身是沒有父ClassLoader的,但是可以作為其他ClassLoader的父加載器
支持并發(fā)加載的類加載器稱為并行類加載器,這種類加載器要求在類初始化期間通過ClassLoader.registerAsParallelCapable
將自身注冊上.默認情況下就是并行的,而子類需要需要并行,則需要調(diào)用該方法
在委托機制并不是嚴格層次化的環(huán)境下,classLoader需要并行處理,否則類在加載過程中會導致死鎖,因為類加載過程中是持有鎖的
通常情況下,JVM會從本地的文件系統(tǒng)中加載類,這種加載與平臺相關.例如在UNIX系統(tǒng)中,jvm會從環(huán)境變量中CLASSPATH定義的目錄中加載類.
然而有些類并不是文件,例如網(wǎng)絡,或者由應用構建出來(動態(tài)代理),這種情況下,defineClass
方法會將字節(jié)數(shù)組轉(zhuǎn)換為Class實例,可以通過Class.newInstance
創(chuàng)建類真正的對象
由類加載器創(chuàng)建的對象的構造方法和方法,可能會引用其他的類,所以JVM會調(diào)用loadClass
方法加載其他引用的類
二進制名稱BinaryNames
,作為ClassLoader中方法的String參數(shù)提供的任何類名稱铸抑,都必須是Java語言規(guī)范所定義的二進制名稱遏暴。
例如
- "java.lang.String",全限定類名
- "javax.swing.JSpinner$DefaultEditor",內(nèi)部類
- "java.security.KeyStore
FileBuilder$1",匿名內(nèi)部類
- "java.net.URLClassLoader
1"
自定義類加載器
步驟
- 1.繼承CLassLoader
- 2.重寫
loadClass
方法 - 3.在
loadClass
方法中實現(xiàn)加載class字節(jié)碼的方法,返回byte[] - 4.調(diào)用
super.defineClass(byte[])
方法將Class對象返回給loadClass方法
源碼示例
public class Test16 extends ClassLoader {
private String classLoaderName;
private String path;
private final String fileExtension = ".class";
public Test16(String classLoaderName) {
//將systemClassLoader作為當前加載器的父加載器
super();
this.classLoaderName = classLoaderName;
}
public Test16(ClassLoader parent, String classLoaderName) {
//將自定義的ClassLoader作為當前加載器的父加載器
super(parent);
this.classLoaderName = classLoaderName;
}
public void setPath(String path) {
this.path = path;
}
public static void main(String[] args) throws Exception {
Test16 loader1 = new Test16("loader1");
//設置絕對路徑,加載工程根目錄下的com.r09er.jvm.classloader.Test01.class
loader1.setPath("/Users/cicinnus/Documents/sources/jvm-learning/");
Class<?> aClass = loader1.loadClass("com.r09er.jvm.classloader.Test01");
//打印加載的類
System.out.println("loader1 load class" + aClass.hashCode());
Object instance = aClass.newInstance();
System.out.println("instance1: " + instance);
Test16 loader2 = new Test16("loader2");
//設置絕對路徑,加載工程根目錄下的Test01.class
loader2.setPath("/Users/cicinnus/Documents/sources/jvm-learning/");
Class<?> aClass2 = loader2.loadClass("com.r09er.jvm.classloader.Test01");
System.out.println("loader2 load class" + aClass2.hashCode());
Object instance2 = aClass2.newInstance();
System.out.println("instance2 : " + instance2);
//todo ****
//1.重新編譯工程,確保默認的classPath目錄下有Test01.class的字節(jié)碼文件,然后運行main方法,觀察輸出
//2.刪除默認classpath目錄下的Test01.class,運行main方法,觀察輸出
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
System.out.println("invoke findClass");
System.out.println("class loader name : " + this.classLoaderName);
byte[] bytes = this.loadClassData(name);
return super.defineClass(name, bytes, 0, bytes.length);
}
private byte[] loadClassData(String binaryName) {
byte[] data = null;
binaryName = binaryName.replace(".", "/");
try (
InputStream ins = new FileInputStream(new File(this.path + binaryName + this.fileExtension));
ByteArrayOutputStream baos = new ByteArrayOutputStream();
) {
int ch;
while (-1 != (ch = ins.read())) {
baos.write(ch);
}
data = baos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
}
return data;
}
}
執(zhí)行兩次main方法后,會發(fā)現(xiàn)類加載器真正生效的邏輯,因為默認的父加載器其實是系統(tǒng)加載器(AppClassLoader
),所以如果默認的classPath存在字節(jié)碼文件,則會由AppClassLoader
正確加載類,如果classPath中沒有,則會向下使用自定義的類加載器加載類
如果構造函數(shù)傳入兩個不一樣的ClassLoaderName,會發(fā)現(xiàn)兩個class對象并不一致,是由于命名空間NameSpace
的原因,因為兩個類加載器定義的名稱是不一樣的,如果改成相同的名稱,則兩個class對象一致
重寫的是findClass方法,在調(diào)用時候,使用的是classLoader的
loadClass
方法,這個方法內(nèi)部會調(diào)用findClass
還有一個重點,如果將class字節(jié)碼文件放在根目錄,則會拋出
NoClassDefFoundError
異常,因為binaryName
不符合規(guī)范.
類加載器重要方法詳解
findClass
實現(xiàn)自己的類加載器,最重要就是實現(xiàn)findClass,通過傳入binaryName
,將二進制名稱加載成一個Class對象
defineClass
在實現(xiàn)findClass
后,需要通過defineClass方法,將二進制數(shù)據(jù)交給defineClass
方法轉(zhuǎn)換成一個Class實例,
在defineClass
內(nèi)部會做一些保護和檢驗工作.
雙親委派機制解析
通過loadClass
方法加載類,會有如下默認加載順序
- 1.調(diào)用
findLoadedClass
方法檢查class是否被加載 - 2.調(diào)用父加載器的
loadClass
方法,如果父加載器為null,則會調(diào)用JVM內(nèi)建的類加載器. - 3.調(diào)用
findClass
方法找到類
在默認的loadClass方法中,類加載是同步
的
雙親委派機制優(yōu)點
- 1.可以確保Java核心類庫的類型安全,如果這個加載過程由Java應用自己的類加載器完成,很可能會在JVM中存在多個版本的
同一個類(包名,類名一致)
,
命名空間發(fā)揮的作用
- 2.可以確保Java核心類庫提供的類不會被自定義的類替代
因為優(yōu)先加載的是類庫中的class,會忽略掉自定義的類
- 3.不同的類加載器可以為相同名稱(binaryName)的類創(chuàng)建額外的命名空間,相同名稱的類可以并存在JVM中.
類的卸載
當類被加載,連接,初始化之后,它的生命周期就開始了.當代表類的Class對象不再被引用,即不可觸及時,Class對象就會結束生命周期,類在元空間內(nèi)的數(shù)據(jù)也會被卸載,從而結束類的生命周期.
一個類何時結束生命周期,取決于代表它的Class對象何時結束生命周期
由Java虛擬機自帶的類加載器所加載的類,在虛擬機的生命周期中,始終不會被卸載.
用戶自定義的類加載器所加載的類是可以被卸載的
類加載器加載的類路徑
BootstrapClassLoader加載的路徑
- System.getProperty("sun.boot.class.path")
ExtClassLoader
- System.getProperty("java.ext.dirs")
AppClassLoader
- System.getProperty("java.class.path")
三個路徑和JDK版本,操作系統(tǒng)都有關系
如果將編譯好的class字節(jié)碼文件放到根加載器的加載路徑上,可以成功由BootstrapClassLoader加載
類加載器命名空間
- 每個類加載器都有自己的命名空間,命名空間由該加載器及所有父加載器所加載的類組成
即子加載器能訪問父加載器加載的類,而父加載器不能訪問子加載器加載的類.(類似于繼承的概念)
- 在同一個命名空間中,不會出現(xiàn)類的完整名字相同的兩個類
一個Java類是由該類的全限定名稱+用于加載該類的定義類加載器(defining loader)共同決定.
ClassLoader.getSystemClassLoader
源碼
返回用于委托的系統(tǒng)類加載器.是自定義類加載器的父加載器,通常情況下類會被系統(tǒng)類加載器加載.
該方法在程序運很早的時間就會被創(chuàng)建,并且會將系統(tǒng)類加載器設為調(diào)用線程的上下文類加載器(context class loader
)
Launcher構造主要邏輯
1.初始化ExtClassLoader
2.初始化AppClassLoader,將初始化好的ExtClassLoader設置為AppClassLoader的父加載器
3.將AppClassLoader設置為當前線程的上下文類加載器
SystemClassLoaderAction
邏輯
1.判斷System.getProperty("java.system.class.loader")
是否有設置系統(tǒng)類加載器
2.如果為空,直接返回AppClassLoader
3.如果不為空,通過反射創(chuàng)建classLoader,其中必須提供一個函數(shù)簽名為ClassLoader
的構造
4.將反射創(chuàng)建的自定義類加載器設置為上限為加載器.
5.返回創(chuàng)建好的類加載器
Class.ForName(name,initialize,classloader)
解析
-
name
,需要構造的類全限定名稱(binaryName)
不能用于原生類型或者void類型
如果表示的是數(shù)組,則會加載數(shù)組中的元素class對象,但是不進行初始化
-
initialize
,類是否需要初始化 -
classloader
,加載此類的類加載器
線程上下文加載器(ContextClassLoader
)實現(xiàn)與分析
CurrentClassLoader(當前類加載器)
- 每一個類都會嘗試使用自己的
ClassLoader
去加載當前類引用的其他類
如果ClassA引用了ClassY,那么ClassA的類加載器會去加載ClassY,前提是ClassY未被加載
線程類加載器從JDK1.2開始引入,Thread類中的getContextClassLoader
和setContextClassLoader
分別用來獲取和設置上下文加載器.如果沒有手動進行設置,那么線程會繼承其父線程的上下文加載器.
java應用運行時的初始線程的上下文類加載器是系統(tǒng)類加載器(AppClassLoader),在線程中運行的類可以通過這個類加載器加載類與資源
由JDBC引出的問題
回顧一下JDBC操作
Class.forName("com.mysql.driver.Driver");
Connection conn = Driver.connect();
Statement stae = conn.getStatement();
Driver
,Connection
,Statement
都是由JDK提供的標準,而實現(xiàn)是由具體的DB廠商提供.
根據(jù)類加載的機制,JDK的rt包會被BootstrapClassLoader
加載,而自定義的類會被AppClassLoader
加載,同時因為命名空間
的原因,父加載器是無法訪問子加載器加載的類的.所以父加載器會導致這個問題.
上下文加載器就是為了解決這種問題所存在的
父ClassLaoder可以使用當前線程Thread.currentThread().getContextClassLoader()
加載的類,
這就改變了父ClassLoader不能使用子ClassLoader或是其他沒有直接父子關系的ClassLoader無法訪問對方加載的class問題.
即改變了父親委托模型
線程上下文加載器一般使用
使用步驟(獲取 - 使用 - 還原)
- Thread.currentThread().getContextClassLoader()
- Thread.currentThread().setContextClassLoader(targetClassLoader)
doSomentthing();
3.Thread.currentThread().setContextClassLoader(originClassLoader);
ContextClassLoader的作用就是破壞Java的類加載委托機制
ServiceLoader
ServiceLoader
是一個簡單的服務提供者加載設施
加載基于JDK規(guī)范接口實現(xiàn)的具體實現(xiàn)類
實現(xiàn)類需要提供無參構造,用于反射構造出示例對象
服務提供者將配置文件放到資源目錄的META-INF/services
目錄下,告訴JDK在此目錄的文件內(nèi)配置了需要加載的類,其中文件名稱是需要加載的接口全限定名稱,文件內(nèi)容是一個或多個實現(xiàn)的類全限定名稱.
總結
在雙親委托模型下,類加載時由下至上的.但是對于SPI
機制來說,有些接口是由Java核心庫提供的,根據(jù)類加載的機制,JDK的rt包會被BootstrapClassLoader
加載,而自定義的類會被AppClassLoader
加載.這樣傳統(tǒng)的雙親委托模型就不能滿足SPI
的情況,就可以通過線程上下文加載器來實現(xiàn)對于接口實現(xiàn)類的加載.