在個人深刻理解JVM類加載機制中,類加載器的部分我只談了一點點內容,這篇文章將深刻了解Java中的類加載器是如何工做的。java
類加載的第一個階段就須要經過一個類的全限定名來獲取描述此類的二進制字節流,實現這個動做的模塊就是類加載器。面試
類加載器雖然只是實現類的加載動做,可是在Java程序中的做用遠不止於此。在Java中一個類的惟一性不只僅是看類自己,還要看它的加載器。通俗地說:比較兩個類是否相等,只有在兩個類時由同一個類加載器加載的前提下才有意義,不然,即便兩個類來源於同一個Class文件,被同一個虛擬機加載,只要類加載器不一樣,兩個類也不相等。bootstrap
下面看周志明老師那本書上例舉的一段代碼:bash
import java.io.IOException;
import java.io.InputStream;
public class ClassLoaderTest {
public static void main(String[] args) throws Exception{
ClassLoader myLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
String fileName = name.substring(name.lastIndexOf(".") + 1)+".class";
InputStream is = getClass().getResourceAsStream(fileName);
if (is==null) {
return super.loadClass(name);
}
byte[] b =new byte[is.available()];
is.read(b);
return defineClass(name,b,0,b.length);
} catch (IOException e) {
throw new ClassNotFoundException(name);
}
}
};
Object obj = myLoader.loadClass("ClassLoaderTest").newInstance();
System.out.println(obj.getClass()); //class ClassLoaderTest
System.out.println(obj instanceof ClassLoaderTest); //false
}
}複製代碼
註釋後打印了輸出內容,咱們使用本身定義的類加載器去加載自己產生的class文件,產生Class類,再實例化產生了obj對象。從打印的第一句看出,obj確實是ClassLoaderTest實例化的對象,但第二句返回了false,由於系統內存在兩個ClassLoaderTest類,一個是系統應用程序類加載器加載的,一個是咱們自定義的類加載器加載的,雖然來自於同一個class文件,可是倒是兩個類。服務器
上面咱們提到了應用程序加載器,自定義類加載器什麼的,別急,繼續往下看。網絡
什麼是雙親委派模型想必也是面試中的常問考點。以前咱們也提到過在JVM中,只存在兩種類加載器:ide
java.lang.ClassLoader
通常來講,在討論類加載器時,咱們會劃分的更細,咱們能夠看下圖:函數
下面的討論都將基於這幅圖。post
絕大部分的Java程序都會使用到如下3種系統提供的類加載器。ui
<JAVA_HOME>\lib\
目錄中的,或者-Xbootclasspath
參數指定的目錄所指定的路徑中的,而且是虛擬機識別的的類庫加載到虛擬機內存中,如rt.jar,識別僅按照文件名識別,若是名字不符合,即便在這個目錄下,也不會被加載。啓動類加載器沒法被java程序直接引,用戶若是在編寫自定義的類加載器時,若是須要把加載請求委託給引導類加載器,那麼直接用null代替便可。sun.misc.Launcher $ExtClassLoader
實現,它負責加載<JAVA_HOME>\lib\ext
目錄中的,或者被java.ext.dirs
系統變量所指定的路徑中的全部類庫。sun.misc.Launcher $App-ClassLoader
實現。該加載器是由ClassLoader的getSystemClassLoader()
方法返回,因此通常稱它爲系統類加載器。通常它加載用戶類路徑(ClassPath)所指定的類庫,開發者通常直接使用這個類加載器,若是沒有定義本身的類加載器,那麼這個應用程序加載類就是程序中默認的類加載器。咱們來解釋下什麼是雙親委派模型?
雙親委派模型要求除了頂層的Bootstrap ClassLoader
外,其它全部類加載器都要有本身的父類加載器。這裏的父子關係通常不會議繼承實現,而是經過組合實現。它的基本工做流程是:若是一個類加載器收到了類加載的請求,它首先不會本身去嘗試加載這個類,而是把這個請求委派給父類加載器完成,每一個層次的類加載器都是如此,所以最後全部的請求都會傳遞到頂層的啓動類加載器中,只有當父加載器返回本身沒法完成這個加載請求(即它的搜索範圍內沒有找到所須要的類),子加載器纔會嘗試去本身加載。
使用雙親委派模型的好處呢?
使用雙親委派模型最直接的好處就是Java類隨着它的類加載器一塊兒具有了一種帶有優先級的層次關係,例如jaba.lang.Object
類,它存放在rt.jar
中,不管哪個類加載器要加載這個類,最終都要委派給最上層的boostrap ClassLoader
,因此Object類在程序的各類類加載器環境中都是同一個類。假如沒有使用雙親委派模型,由各個類各自加載Object,那麼系統裏將會出現各類版本的Object類,致使整個系統的混亂。
下面咱們從ClassLoader的源碼來看看雙親委派模式:
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) {
//若是自定義的類加載器的parent不爲null,就調用parent的loadClass進行加載類
c = parent.loadClass(name, false);
} else {
//不然就去找bootstrap ClassLoader
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;
}
}複製代碼
從上面的代碼來看,使用指定名稱加載類分爲如下3步:
findLoadedClass(String)
來檢查類是否已經被加載;loadClass
方法,若是父類爲空,就調用虛擬機內置的引導類加載器加載;findClass(String)
來查找該類。由於loadClass
封裝了雙親委派模型,因此在開發本身的類加載器時,Java標準提覆寫findClass()
方法。
一般狀況下,Java虛擬機都是從文件系統裏load一個class,可是有一些類不必定來自一個文件,它們也可能來自別的源,好比網絡,加密文件等等,假設咱們寫一個本身的類加載器,加載服務器下載的class文件。
示例使用代碼:
ClassLoader loader = new NetworkClassLoader(host, port);
Object main = loader.loadClass("Main", true).newInstance();複製代碼
在定義時,咱們覆寫findClass方法:
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
. . .
}
}複製代碼
這裏重要的還有個defineClass
函數,用來把一組二進制字節轉換爲Class的實例,轉換爲Class後再交給後續的類加載過程解析。後續步驟就又回到深刻理解JVM類加載機制中所描述的了。