why?
ClassLoader,即java類加載器,主要作用是將class加載到JVM內,同時它還要考慮class由誰來加載。在說java的類加載機制之前,還是像前面的博客一樣,先說說爲什麼要知道java的類加載機制。個人認爲主要有以下幾個原因:
- 按需加載。JVM啓動時不能確定我要加載哪些東西,或者有些類非常大,我只希望用到它時再加載,並非一次性加載所有的class,所以這時候瞭解了加載機制就可以按需加載了。
- 類隔離。比如web容器中部署多個應用,應用之間互相可能會有衝突,所以希望儘量隔離,這裏可能就要分析各個應用加載的資源和加載順序之間的衝突,針對這些衝突再自己定些規則,讓它們能夠愉快地玩耍。
- 資源回收。如果你不瞭解java是如何加載資源的,又怎麼理解java是如何回收資源的?
what?
一般說到java的類加載機制,都要說到“雙親委派模型”(其實個人很不理解爲什麼叫“雙親”,其實英文叫“parent”)。使用這種機制,可以避免重複加載,當父親已經加載了該類的時候,就沒有必要子ClassLoader再加載一次。JVM根據 類名+包名+ClassLoader實例ID
來判定兩個類是否相同,是否已經加載過(所以這裏可以略微擴展下,可以通過創建不同的classloader實例來實現類的熱部署)。
有個圖很形象(來源見參考資料)。
注意:如果想形象地看到java的類加載順序,可以在運行java的時候加個啓動參數,即java –verbose
下面結合上圖來詳細介紹下java的類加載過程。
- BootStrapClassLoader。它是最頂層的類加載器,是由C++編寫而成, 已經內嵌到JVM中了。在JVM啓動時會初始化該ClassLoader,它主要用來讀取Java的核心類庫JRE/lib/rt.jar中所有的class文件,這個jar文件中包含了java規範定義的所有接口及實現。
- ExtensionClassLoader。它是用來讀取Java的一些擴展類庫,如讀取JRE/lib/ext/*.jar中的包等(這裏要注意,有些版本的是沒有ext這個目錄的)。
- AppClassLoader。它是用來讀取CLASSPATH下指定的所有jar包或目錄的類文件,一般情況下這個就是程序中默認的類加載器。
- CustomClassLoader。它是用戶自定義編寫的,它用來讀取指定類文件 。基於自定義的ClassLoader可用於加載非Classpath中(如從網絡上下載的jar或二進制)的jar及目錄、還可以在加載前對class文件優一些動作,如解密、編碼等。
很多資料和文章裏說,ExtClassLoader
的父類加載器是BootStrapClassLoader
,其實這裏省掉了一句話,容易造成很多新手(比如我)的迷惑。嚴格來說,ExtClassLoader
的父類加載器是null
,只不過在默認的ClassLoader 的 loadClass
方法中,當parent爲null
時,是交給BootStrapClassLoader
來處理的,而且ExtClassLoader
沒有重寫默認的loadClass
方法,所以,ExtClassLoader
也會調用BootStrapLoader
類加載器來加載,這就導致“BootStrapClassLoader具備了ExtClassLoader父類加載器的功能”。看一下下面的代碼就很容易理解上面這一大段話了。
/**
* 查看父類加載器
*/
private static void test1() {
ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
System.out.println("系統類裝載器:" + appClassLoader);
ClassLoader extensionClassLoader = appClassLoader.getParent();
System.out.println("系統類裝載器的父類加載器——擴展類加載器:" + extensionClassLoader);
ClassLoader bootstrapClassLoader = extensionClassLoader.getParent();
System.out.println("擴展類加載器的父類加載器——引導類加載器:" + bootstrapClassLoader);
}
可以看出ExtensionClassLoader
的parent
爲null
。
三個重要的方法
查看classloader的源碼可以發現三個重要的方法:
- loadClass。classloader加載類的入口,此方法負責加載指定名字的類,ClassLoader的實現方法爲先從已經加載的類中尋找,如沒有則繼續從父ClassLoader中尋找,如仍然沒找到,則從BootstrapClassLoader中尋找,最後再調用findClass方法來尋找,如要改變類的加載順序,則可覆蓋此方法,如加載順序相同,則可通過覆蓋findClass來做特殊的處理,例如解密、固定路徑尋找等,當通過整個尋找類的過程仍然未獲取到Class對象時,則拋出ClassNotFoundException。如類需要resolve,則調用resolveClass進行鏈接。
- findClass。此方法直接拋出ClassNotFoundException,因此需要通過覆蓋loadClass或此方法來以自定義的方式加載相應的類。
- defineClass。此方法負責將二進制的字節碼轉換爲Class對象,這個方法對於自定義加載類而言非常重要,如二進制的字節碼的格式不符合JVM Class文件的格式,拋出ClassFormatError;如需要生成的類名和二進制字節碼中的不同,則拋出NoClassDefFoundError;如需要加載的class是受保護的、採用不同簽名的或類名是以java.開頭的,則拋出SecurityException;如需加載的class在此ClassLoader中已加載,則拋出LinkageError。
好,可能有人看了上面會疑惑,爲什麼上面說自定義classloader是需要重寫findClass而不是loadClass或者其他方法。這裏建議查看源碼。
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 {
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();
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;
}
}
可以看到,JDK已經在loadClass方法中幫我們實現了ClassLoader搜索類的判斷方法,當在loadClass方法中搜索不到類時,loadClass方法就會調用findClass方法來搜索類,所以我們只需重寫該方法即可。
看完這一大串可能有人還是不理解,ok,那我們現在就動手寫一個自己的ClassLoader,儘量包含上面三個方法。
自定義ClassLoader
先定義一個Person接口。
public interface Person {
public void say();
}
再定一個高富帥類實現這個接口
public class HighRichHandsome implements Person {
@Override
public void say() {
System.out.println("I don't care whether you are rich or not");
}
}
好的,開胃菜結束,主角來了,MyClassLoader。
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
public class MyClassLoader extends ClassLoader{
/*
* 覆蓋了父類的findClass,實現自定義的classloader
*/
@Override
public Class<?> findClass(String name) {
byte[] bt = loadClassData(name);
return defineClass(name, bt, 0, bt.length);
}
private byte[] loadClassData(String className) {
InputStream is = getClass().getClassLoader().getResourceAsStream(
className.replace(".", "/") + ".class");
ByteArrayOutputStream byteSt = new ByteArrayOutputStream();
int len = 0;
try {
while ((len = is.read()) != -1) {
byteSt.write(len);
}
} catch (IOException e) {
e.printStackTrace();
}
return byteSt.toByteArray();
}
}
代碼很簡單,不解釋了,最後在測試類LoaderTest裏寫個測試方法。
/**
* 父類classloader
* @throws Exception
*/
private static void test2() throws Exception{
MyClassLoader loader = new MyClassLoader();
Class<?> c = loader.loadClass("com.alibaba.classload.HighRichHandsome");
System.out.println("Loaded by :" + c.getClassLoader());
Person p = (Person) c.newInstance();
p.say();
HighRichHandsome man = (HighRichHandsome) c.newInstance();
man.say();
}
main方法中調用這個方法即可。LoaderTest默認構造函數會設置AppClassLoader
爲parent
, 測試時執行test2()
方法會發現HighRichHandsome
類是委託AppClassLoader
加載的,所以AppClassLoader可以訪問到,不會出錯。
但是我們再想一下,如果我們直接加載,不委託給父類加載,會出現什麼情況?
/**
* 自己的classloader加載
* @throws Exception
*/
private static void test3() throws Exception{
MyClassLoader loader = new MyClassLoader();
Class<?> c = loader.findClass("com.alibaba.classload.HighRichHandsome");
System.out.println("Loaded by :" + c.getClassLoader());
Person p = (Person) c.newInstance();
p.say();
//註釋下面兩行則不報錯
HighRichHandsome man = (HighRichHandsome) c.newInstance();
man.say();
}
可以看到,悲劇的報錯了。根據class loader命名空間規則,每個class loader都有自己唯一的命名空間,每個class loader 只能訪問自己命名空間中的類,如果一個類是委託parent加載的,那麼加載後,這個類就類似共享的,parent和child都可以訪問到這個類,因爲parent是不會委託child加載類的,所以child加載的類parent訪問不到。簡單來說,即子加載器的命名空間包含了parent加載的所有類,反過來則不成立。 本例中LoaderTest
類是AppClassLoader
加載的,所以其看不見由MyClassLoader
加載的HighRichHandsome
類,但Person接口是可以訪問的,所以賦給Person類型不會出錯。
一些小的知識點
1.Class.forName()
相信大家都寫過連接數據庫的例子,基本上就是加載驅動,建立連接,創建請求,寫prepareStatement,關閉連接之類的。在這裏,有一段代碼:
public DbTest() {
try {
Class.forName("com.mysql.jdbc.Driver");// 加載驅動
conn = DriverManager.getConnection(url, "root", "");// 建立連接
stm = conn.createStatement(); // 創建請求
} catch (Exception e) {
e.printStackTrace();
}
}
我相信大家一開始的時候肯定都有些疑惑,就是Class.forName(“com.mysql.jdbc.Driver”),爲什麼加載驅動是Class.forName,而不是ClassLoader的loadClass?爲什麼這麼寫就可以加載驅動了呢?
其實Class.forName()
是顯示加載類,作用是要求JVM查找並加載指定的類,也就是說JVM會執行該類的靜態代碼段。查看com.mysql.jdbc.Driver
源碼可以發現裏面有個靜態代碼塊,在加載後,類裏面的靜態代碼塊就執行(主要目的是註冊驅動,把自己註冊進去),所以主要目的就是爲了觸發這個靜態方法。
2.Web容器加載機制
其實web容器的加載機制這一點我說不好,因爲沒看過諸如tomcat之類的源碼,但是根據自己使用感覺以及查了相關資料,可以簡單介紹一下。一般服務器端都要求類加載器能夠反轉委派原則,也就是先加載本地的類,如果加載不到,再到parent中加載。這個得細看,我這裏只知道這個概念,所以就不說了。JavaEE規範推薦每個模塊的類加載器先加載本類加載的內容,如果加載不到纔回到parent類加載器中嘗試加載。
3.重複加載與回收
一個class可以被不同的class loader重複加載,但同一個class只能被同一個class loader加載一次。見下列代碼:
/**
* 對象只加載一次,返回true
*/
private static void test4() {
ClassLoader c1 = LoaderTest.class.getClassLoader();
LoaderTest loadtest = new LoaderTest();
ClassLoader c2 = loadtest.getClass().getClassLoader();
System.out.println("c1.equals(c2):"+c1.equals(c2));
}
類的回收。說到最開始的why,其實java的回收機制我前面有篇博客說的比較詳細,這裏就插一句廢話吧,當某個classloader加載的所有類實例化的所有對象都被回收了,則該classloader會被回收。
參考:http://blog.csdn.net/xyang81/article/details/7292380
ps:話說csdn的markdown寫出來的效果實在不敢恭維,小小的吐槽一下。