類加載器
類加載器的作用就是把磁盤中的類文件加載到內(nèi)存的方法區(qū)以供使用,分析類加載前,先看下jvm運(yùn)行時(shí)都需要加載什么樣的類
類和類庫
jvm運(yùn)行時(shí)的類主要分三種
-
核心類
比如String,Thread,Lock等,這種類都是jdk提供的,我們不需要自己寫,路徑在JRE的lib目錄下 -
擴(kuò)展類
一些jdk提供的擴(kuò)展類,比如DESKey等加密的類,也不需要自己寫,路徑是JRE的lib目錄下的ext擴(kuò)展目錄 -
應(yīng)用程序類
也就是程序員自己寫的類
三種不同的類存儲(chǔ)在磁盤不同的目錄下,jvm運(yùn)行時(shí)需要類加載器把類信息加載到內(nèi)存,具體說是工作區(qū)中,而針對(duì)三種類型的類分別是三種不同的類加載器負(fù)責(zé)加載的
引導(dǎo)類加載器/核心類加載器
jvm啟動(dòng)時(shí)首先會(huì)創(chuàng)建一個(gè)引導(dǎo)類加載器
實(shí)例,是由C++實(shí)現(xiàn)的,它的作用就是加載支撐JVM運(yùn)行時(shí)的核心類庫,這些類庫所在地就是我們jdk安裝路徑下的jre/lib中的一些jar包
其中最核心的就是rt.jar
,我們使用的大部分基礎(chǔ)類比如:String
,Thread
都定義在這個(gè)包下
擴(kuò)展類加載器
借助C++,有了引導(dǎo)類加載器,就可以實(shí)現(xiàn)加載一些基本的類,同時(shí)也可以定義一個(gè)自己的(java)的類加載器的基本類,由引導(dǎo)類加載器加載到工作區(qū),就可以創(chuàng)建它的實(shí)例,用這個(gè)實(shí)例就可自己去加載其他的類:額外類和應(yīng)用程序類,就不用再麻煩C++實(shí)現(xiàn)的引導(dǎo)類加載器,畢竟java就可以做了
這就好比雞生蛋問題,一個(gè)農(nóng)夫要吃雞蛋,先從外部買個(gè)雞,下了雞蛋又可生成雞繼續(xù)下蛋,就不需要再去買雞了
rt.jar
類庫中還有一個(gè)重量級(jí)的類:sun.misc.Launcher
,翻譯過來就是啟動(dòng)器,jvm在啟動(dòng)時(shí)通過C++實(shí)現(xiàn)的引導(dǎo)類加載器加載了這個(gè)類,然后生成一個(gè)該類的實(shí)例,實(shí)例初始化的構(gòu)造方法如下
其中Launcher.ExtClassLoader.getExtClassLoader()
就是初始化一個(gè)擴(kuò)展類加載器實(shí)例,它可以把JRE的lib目錄下的ext擴(kuò)展目錄下的類庫加載到工作區(qū)
應(yīng)用程序類加載器
而下面的Launcher.AppClassLoader.getAppClassLoader(var1)
就是初始化一個(gè)應(yīng)用程序類加載器實(shí)例,它可以把用戶代碼加載到工作區(qū)
解釋
這里可能有點(diǎn)蒙,可以用下面例子幫助理解一下
我們把jvm比作一個(gè)造萬物的工廠,工廠可以生產(chǎn)任何實(shí)物(對(duì)象實(shí)例),前提是必須有圖紙(class)才能創(chuàng)建出來
工廠工作時(shí)為了查找方便把生產(chǎn)出的實(shí)物和圖紙分開存儲(chǔ),其中圖紙存入工作方法區(qū)
,實(shí)物存入工作堆區(qū)
工廠向外部提供服務(wù),客戶給一張圖紙,工廠就可以生產(chǎn)出實(shí)物
同時(shí)工廠提供一些基本組件的圖紙(核心類),客戶可以在自己的圖紙中標(biāo)志使用這些基本組件
工廠提供一些額外組件的圖紙(擴(kuò)展類),滿足特殊需求,客戶也可以在自己的圖紙中標(biāo)志使用這些額外組件
但是客戶給的圖紙都放在一個(gè)圖紙收集欄中(java.class.path
),工廠需要收集欄圖紙加載到工作方法區(qū)
,并做一些特殊處理。與此同時(shí)工廠提供的基本組件圖紙和額外圖紙(放在倉庫里)也需要首先加載到工作方法區(qū)
,這樣才能在實(shí)際生產(chǎn)客戶實(shí)物用到時(shí)使用
為了實(shí)現(xiàn)加載這個(gè)加載過程,工廠首先從其它工廠借了個(gè)引導(dǎo)加載器(C++實(shí)現(xiàn)),它的工作就是把基本組件圖紙從倉庫加載到工作方法區(qū)
有了這個(gè)引導(dǎo)加載器,只能加載基本組件圖紙,客戶提交的圖紙和額外圖紙還是無法加載
于是工廠自己生成加載器,在基本組件圖紙倉庫中中制作了“額外圖紙加載器”和“客戶圖紙加載器”的圖紙
工廠開始工作后,有了引導(dǎo)類加載器的加載,就拿到了兩張自定義加載器的圖紙,然后創(chuàng)建這個(gè)“額外圖紙加載器”和“客戶圖紙加載器”的實(shí)物,
然后用“額外圖紙加載器”可以把額外的圖紙加載進(jìn)工作方法區(qū)
再用“客戶圖紙加載器”可以把客戶提交的圖紙加載到工作方法區(qū)
, 就這樣所有生產(chǎn)所需的圖紙都可以獲取到了,便可實(shí)現(xiàn)生產(chǎn)
查看類加載器
有了這三種類加載器,我們程序所需的類都可以加載到工作區(qū),進(jìn)而生成類的實(shí)例,我們可以使用代碼一探這些加載器
public class TestJDKClassLoader {
public static void main(String[] args) {
System.out.println(String.class.getClassLoader()); // String的類加載器
System.out.println(DESKeyFactory.class.getClassLoader().getClass().getName());
System.out.println(TestJDKClassLoader.class.getClassLoader().getClass().getName());
System.out.println(ClassLoader.getSystemClassLoader());
}
}
輸出
null
sun.misc.Launcher$ExtClassLoader
sun.misc.Launcher$AppClassLoader
sun.misc.Launcher$AppClassLoader
可以看到String.class的類加載器是null,因?yàn)橐龑?dǎo)類加載器是C++的對(duì)象,所以java打印不出來
DESKeyFactory.class在ext包下,所以它的類加載器是ExtClassLoader(擴(kuò)展類加載器)
TestJDKClassLoader是自己實(shí)現(xiàn)的類,對(duì)應(yīng)加載器AppClassLoader(應(yīng)用類加載器)
另外一種獲取類加載器的方式ClassLoader.getSystemClassLoader()
類加載時(shí)機(jī)
上文一直說類加載“可以”加載,那么實(shí)際上這些類什么時(shí)候被加載吶,可以做個(gè)測(cè)試
這里補(bǔ)充一下static代碼塊的代碼是類加載后執(zhí)行的
public class TestDynamicLoad {
static {
System.out.println("*************load TestDynamicLoad************");
}
public static void main(String[] args) {
new A();
B b = null; //B不會(huì)加載,除非這里執(zhí)行 new B()
}
}
class A {
static {
System.out.println("*************load A************");
}
public A() {
System.out.println("*************initial A************");
}
}
class B {
static {
System.out.println("*************load B************");
}
public B() {
System.out.println("*************initial B************");
}
}
運(yùn)行結(jié)果:
*************load TestDynamicLoad************
*************load A************
*************initial A************
可以看到Class B并沒有被實(shí)際加載,說明類加載也是一種懶加載,用到才使用類加載器加載
雙親委派機(jī)制
層級(jí)結(jié)構(gòu)
上文介紹了三種不同的類加載器分別加載不同類型的類,其實(shí)他們仨除了分工不同,還有上下級(jí)的關(guān)系,來看一下ClassLoader
(類加載器的抽象)源碼
一個(gè)類加載器中會(huì)包含一個(gè)parent屬性指向父類加載器,形成了一個(gè)單項(xiàng)鏈表,而以上三種類加載器的父子結(jié)構(gòu)是這樣的
類加載機(jī)制
有了這個(gè)層級(jí)有什么用吶,這就涉及到類加載機(jī)制,也就是雙親委派機(jī)制,機(jī)制其實(shí)很簡單:因?yàn)槊總€(gè)類加載器負(fù)責(zé)的類地址不一樣,所以當(dāng)一個(gè)類加載器想加載一個(gè)類時(shí)先讓父節(jié)點(diǎn)去父地盤找,找不到子節(jié)點(diǎn)再找,遞歸下去最終結(jié)果就是,要加載一個(gè)類,依次去引導(dǎo)類加載器
>擴(kuò)展類加載器
>應(yīng)用類加載器
尋找,找到后就加載,看一下ClassLoader.loadClass
代碼
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 0.檢查是否已加載,如果是就不用重新加載
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) { // 1.1 如果父節(jié)點(diǎn)不是null,讓父節(jié)點(diǎn)先查
c = parent.loadClass(name, false);
} else { //1.2 如果父節(jié)點(diǎn)是null,即引導(dǎo)類加載器,去查找基礎(chǔ)類
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) { //2. 父類查不到,自己找
// If still not found, then invoke findClass in order to find the class.
long t1 = System.nanoTime();
// 自己找的方法
c = findClass(name);
...
示意圖如下
雙親委派機(jī)制的作用
以上介紹了雙親委派機(jī)制,那為什么要這么設(shè)計(jì),主要是防止核心api被篡改,比如你自己寫一個(gè)String類且包名和原類一致,但由于雙親委派機(jī)制的存在,你寫的永遠(yuǎn)不會(huì)被加載。
比如說上例的工廠已經(jīng)提供了標(biāo)準(zhǔn)螺絲釘?shù)脑O(shè)計(jì)圖,如果客戶意圖使用自己的螺絲釘設(shè)計(jì)圖替換工廠的是不行的
自定義類加載器
以上三種類加載器,除了引導(dǎo)類加載器,都是java實(shí)現(xiàn)的,那作為java程序員可不可以自己寫一個(gè)類加載器?答案是肯定的,比如我們可以繼承上文提到的ClassLoader
抽象類,嘗試寫一個(gè)類加載器來把桌面上存放的的類加載進(jìn)來
ClassLoader
實(shí)際查找累的方法是findClass
,也就是上文loadClass
方法中當(dāng)父節(jié)點(diǎn)差找不到類時(shí)所執(zhí)行的方法,默認(rèn)如下
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
默認(rèn)拋出異常,也就是說要想實(shí)現(xiàn)類加載器,這個(gè)方法肯定要重寫的,最終代碼如下
public class MyClassLoader extends ClassLoader {
/**
* 桌面路徑
*/
private String classPath = "C:/Users/Administrator/Desktop";
private byte[] loadByte(String name) throws Exception {
name = name.replaceAll("\\.", "/");
FileInputStream fis = new FileInputStream(classPath + "/" + name
+ ".class");
int len = fis.available();
byte[] data = new byte[len];
fis.read(data);
fis.close();
return data;
}
/**
* 重寫findClass方法
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] data = loadByte(name);
//defineClass將一個(gè)字節(jié)數(shù)組轉(zhuǎn)為Class對(duì)象,這個(gè)字節(jié)數(shù)組是class文件讀取后最終的字節(jié)數(shù)組。
return defineClass(name, data, 0, data.length);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
}
測(cè)試一下,創(chuàng)建一個(gè)類放入桌面,包名是com,所以放一個(gè)com文件夾并放進(jìn)去,并通過javac轉(zhuǎn)換為class文件
package com;
public class MyClass {
public void say() {
System.out.println("hello");
}
}
測(cè)試代碼
public static void main(String[] args) throws Exception {
MyClassLoader classLoader = new MyClassLoader();
// 獲取MyClass類
Class clazz = classLoader.loadClass("com.MyClass");
Object obj = clazz.newInstance();
Method method = clazz.getDeclaredMethod("say", null);
method.invoke(obj, null);
System.out.println(clazz.getClassLoader().getClass().getName()); // 輸出hello
}
這樣我們就實(shí)現(xiàn)了一個(gè)自定義類加載器,可以把各個(gè)地方的類文件加載進(jìn)來并運(yùn)行
如果打印自定義加載器的父加載器就是AppClassLoader,是因?yàn)?code>ClassLoader抽象類中有個(gè)無參構(gòu)造函數(shù)(子類實(shí)例化會(huì)調(diào)用父類無參構(gòu)造)
protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader()); // getSystemClassLoader()的結(jié)果就是AppClassLoader
}
因此用自定義加載器去加載String.class也行的通,因?yàn)殡p親委派會(huì)去父級(jí)先找
打破雙親委派機(jī)制
“打破雙親委派機(jī)制”好像總被提起,聽起來很高端,其實(shí)看代碼雙親委派機(jī)制不過是對(duì)ClassLoader.loadClass
方法執(zhí)行過程起的一個(gè)名字,也就是說只是ClassLoader.loadClass
實(shí)現(xiàn)了雙親委派機(jī)制,那作為子類完全可以覆蓋重寫,所以所謂打破,也不過就是子類覆蓋重寫了父類的默認(rèn)代碼而已
比如說ClassLoader.loadClass
的邏輯是先去父級(jí)找,找不到再findClass
,我們可以改成先findClass
,找不到再調(diào)用父級(jí)的loadClass
不就打破了嗎
Tomcat
提到“打破雙親委派機(jī)制”,就不得不提Tomcat,因?yàn)樗褪且粋€(gè)打破雙親委派機(jī)制的典型案例
Tomcat為什么要打破這個(gè)機(jī)制,主要因?yàn)橐粋€(gè)tomcat容器可能同時(shí)運(yùn)行多個(gè)項(xiàng)目,多項(xiàng)目可能都有一樣報(bào)名和類名的類,但功能不一樣,比如引入不同的版本的三方類庫這種問題就太多了:
比如說項(xiàng)目一引用了1.0版本的某框架,項(xiàng)目二引用了同一框架的2.0版本,那么某類被加載一次就不會(huì)再被加載兩個(gè)項(xiàng)目其實(shí)用到的都是同一版本的某類,這樣肯定就錯(cuò)錯(cuò)了
所以Tomcat必須打破原機(jī)制,也就是重寫loadClass
實(shí)現(xiàn)自己的一套隔離版的類加載機(jī)制