作為一枚java猿,了解類加載器是有必要的,無論是針對面試還是自我學習。
本文從JDK提供的ClassLoader、委托模型以及如何編寫自定義的ClassLoader三方面對ClassLoader做一個簡要的總結。
JDK中提供的ClassLoader
1. Bootstrap ClassLoader
Bootstrap加載器是用C++語言寫的,它是在Java虛擬機啟動后初始化的,它主要負責加載%JAVA_HOME%/jre/lib以及%JAVA_HOME%/jre/classes中的類,是最頂級的ClassLoader。
2. Ext ClassLoader
Ext ClassLoader是用java寫的,且它的父加載器是Bootstrap,具體來說就是sun.misc.Launcher$ExtClassLoader,Ext ClassLoader主要加載%JAVA_HOME%/jre/lib/ext,此路徑下的所有classes目錄以及java.ext.dirs系統變量指定的路徑中的類庫。
3. App ClassLoader
系統類加載器,負責加載應用程序classpath目錄下的所有jar和class文件。它的父加載器為Ext ClassLoader。
具體關系如下圖:
委托模型
進入官方的Java doc里看到ClassLoader類的一段說明:
The ClassLoader class uses a delegation model to search for classes and resources. Each instance of ClassLoader has an associated parent class loader. When requested to find a class or resource, a ClassLoader instance will delegate the search for the class or resource to its parent class loader before attempting to find the class or resource itself. The virtual machine's built-in class loader, called the "bootstrap class loader", does not itself have a parent but may serve as the parent of a ClassLoader instance.
ClassLoader類使用一種委托模型來查找類和資源。每個ClassLoader實例都會關聯1個父ClassLoader。當需要查詢類和資源的時候,一個ClassLoader實例在查詢類或資源之前會先委托給它的父ClassLoader去查詢。Bootstrap ClassLoader是最頂層的加載器,並且可以作為其它ClassLoader實例的父ClassLoader。
由此看見,這個“委托模型”的安全性是很高的,Bootstrap是最頂層的加載器,這樣比如加載 java.lang.String 的時候,永遠都會被Bootstrap加載(Bootstrap ClassLoader會加載%JAVA_HOME%/jre/lib中rt.jar里的String類)。 這樣用戶自定義的java.lang.String永遠都不會被加載,這樣就避免了多個java.lang.String造成的混亂現象。
下面通過jdk里的ClassLoader源碼來驗證一下查找過程:
protected synchronized Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First, check if the class has already been loaded
Class c = findLoadedClass(name); //先查找這個類是否已經加載過,每個加載器都有自己的緩存
if (c == null) {
try {
if (parent != null) { //父加載器存在的話先使用父加載器加載
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name); //沒有父加載器的話使用bootstrap加載
}
} 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.
c = findClass(name); //如果父加載器沒有找到,那么自身查找
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
通過代碼看,這里的查找過程符合委托模型。
如何編寫自定義的ClassLoader
編寫自定義的ClassLoader注意2點即可:
1. 想遵循委托模型的話重寫findClass方法即可。
2. 不遵循委托模型的話重寫loadClass。
其他:defineClass方法把字節數組b中的內容轉換成Java 類,返回的結果是 java.lang.Class類的實例。這個方法被聲明為final的。該方法也是jvm預留給我們處理ClassLoader與類文件關系的入口。
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
throws ClassFormatError
下面就來寫一個基於文件系統的ClassLoader:
先來看下定義的一個類:
package org.format.classloader;
public class Obj {
@Override
public String toString() {
return "org.format.classloader.Obj";
}
}
自定義的ClassLoader:
public class FileSystemClassLoader extends ClassLoader{
private String directory;
public FileSystemClassLoader(String directory) {
this.directory = directory;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] clsBytes = getClassBytes(name);
ifclsBytes == null)
throw new ClassNotFoundException();
return defineClass(name, clsBytes, 0, clsBytes.length);
}
private byte[] getClassBytes(String name) {
String location = getClassLoc(name);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
FileInputStream fis = null;
try {
fis = new FileInputStream(location);
byte[] buffer = new byte[4096];
int readLen = 0;
while( (readLen = fis.read(buffer)) != -1 ) {
baos.write(buffer, 0, readLen);
}
baos.flush();
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
} finally {
if(fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return null;
}
private String getClassLoc(String name) {
return this.directory + File.separatorChar + name.replace('.', File.separatorChar) + ".class";
}
}
在測試之前,使用javac命令將Obj.java編譯成Obj.class,然后放與/tmp/java/classloader目錄下。
下面就用自定義的ClassLoader來進行幾個測試:
FileSystemClassLoader fscl = new FileSystemClassLoader("/tmp/java/classloader");
try {
System.out.println(fscl.loadClass("org.format.classloader.Obj").newInstance());
} catch (Exception e) {
e.printStackTrace();
}
很明顯,自定義的FileSystemClassLoader加載到了自定義的Obj類。
FileSystemClassLoader fscl1 = new FileSystemClassLoader("/tmp/java/classloader");
FileSystemClassLoader fscl2 = new FileSystemClassLoader("/tmp/java/classloader");
try {
Class cls1 = fscl1.loadClass("org.format.classloader.Obj");
Class cls2 = fscl2.loadClass("org.format.classloader.Obj");
System.out.println("class1: " + cls1);
System.out.println("class2: " + cls2);
System.out.println("class1 == class2? " + (cls1 == cls2));
} catch (Exception e) {
e.printStackTrace();
}
我們可以,使用不同的類加載器加載同一class文件得出的Class對象是不一樣的。
這是因為Java 虛擬機不僅要看類的全名是否相同,還要看加載此類的類加載器是否一樣。只有兩者都相同的情況,才認為兩個類是相同的。
參考資料
http://imtiger.net/blog/2009/11/09/java-classloader/