獲得ClassLoader的途徑
- 獲得當前類的ClassLoader
clazz.getClassLoader()
- 獲得當前線程上下文的ClassLoader
Thread.currentThread().getContextClassLoader();
- 獲得系統的ClassLoader
ClassLoader.getSystemClassLoader()
- 獲得調用者的ClassLoader
DriverManager.getCallerClassLoader
ClassLoader源碼解析
概述
類加載器是用於加載類的對象,ClassLoader是一個抽象類。如果我們給定了一個類的二進制名稱,類加載器應嘗試去定位或生成構成定義類的數據。一種典型的策略是將給定的二進制名稱轉換為文件名,然后去文件系統中讀取這個文件名所對應的class文件。
每個Class對象都會包含一個定義它的ClassLoader的一個引用。
數組類的Class對象,不是由類加載器去創建的,而是在Java運行期JVM根據需要自動創建的。對於數組類的類加載器來說,是通過Class.getClassLoader()返回的,與數組當中元素類型的類加載器是一樣的;如果數組當中的元素類型是一個原生類型,數組類是沒有類加載器的【代碼一】。
應用實現了ClassLoader的子類是為了擴展JVM動態加載類的方式。
類加載器典型情況下時可以被安全管理器所使用去標識安全域問題。
ClassLoader類使用了委托模型來尋找類和資源,ClassLoader的每一個實例都會有一個與之關聯的父ClassLoader,當ClassLoader被要求尋找一個類或者資源的時候,ClassLoader實例在自身嘗試尋找類或者資源之前會委托它的父類加載器去完成。虛擬機內建的類加載器,稱之為啟動類加載器,是沒有父加載器的,但是可以作為一個類加載器的父類加載器【雙親委托機制】。
支持並發類加載的類加載器叫做並行類加載器,要求在初始化期間通過**ClassLoader.registerAsParallelCapable **方法注冊自身,ClassLoader類默認被注冊為可以並行,但是如果它的子類也是並行加載的話需要單獨去注冊子類。
在委托模型不是嚴格的層次化的環境下,類加載器需要並行,否則類加載會導致死鎖,因為加載器的鎖在類加載過程中是一直被持有的。
通常情況下,Java虛擬機以平台相關的形式從本地的文件系統中加載類,比如在UNIX系統,虛擬機從CLASSPATH環境所定義的目錄加載類。
然而,有些類並不是來自於文件;它們是從其它來源得到的,比如網絡,或者是由應用本身構建【動態代理】。定義類(defineClass )方法會將字節數組轉換為Class的實例,這個新定義類的實例可以由Class.newInstance創建。由類加載器創建的對象的方法和構造方法可能引用其它的類,為了確定被引用的類,Java虛擬機會調用最初創建類的類加載器的loadClass方法。
二進制名稱:以字符串參數的形式向CalssLoader提供的任意一個類名,必須是一個二進制的名稱,包含以下四種情況
- "java.lang.String" 正常類
- "javax.swing.JSpinner$DefaultEditor" 內部類
- "java.security.KeyStore\(Builder\)FileBuilder$1" KeyStore的內部類Builder的內部類FileBuilder的第一個匿名內部類
- "java.net.URLClassLoader$3$1" URLClassLoader類的第三個匿名內部類的第一個匿名內部類
代碼一:
public class Test12 {
public static void main(String[] args) {
String[] strings = new String[6];
System.out.println(strings.getClass().getClassLoader());
// 運行結果:null
Test12[] test12s = new Test12[1];
System.out.println(test12s.getClass().getClassLoader());
// 運行結果:sun.misc.Launcher$AppClassLoader@18b4aac2
int[] ints = new int[2];
System.out.println(ints.getClass().getClassLoader());
// 運行結果:null
}
}
loadClass方法
loadClass的源碼如下, loadClass方法加載擁有指定的二進制名稱的Class,默認按照如下順序尋找類:
- 調用findLoadedClass(String)檢查這個類是否被加載
- 調用父類加載器的loadClass方法,如果父類加載器為null,就會調用啟動類加載器
- 調用findClass(String)方法尋找
使用上述步驟如果類被找到且resolve為true,就會去調用resolveClass(Class)方法
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;
}
}
findClass方法
findClass的源碼如下,findClass尋找擁有指定二進制名稱的類,JVM鼓勵我們重寫此方法,需要自定義加載器遵循雙親委托機制,該方法會在檢查完父類加載器之后被loadClass方法調用,默認返回ClassNotFoundException異常。
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
defineClass方法
defineClass的源碼如下,defineClass方法將一個字節數組轉換為Class的實例。
protected final Class<?> defineClass(String name, byte[] b, int off, int len,
ProtectionDomain protectionDomain)
throws ClassFormatError
{
protectionDomain = preDefineClass(name, protectionDomain);
String source = defineClassSourceLocation(protectionDomain);
Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
postDefineClass(c, protectionDomain);
return c;
}
自定義類加載器
/**
* 繼承了ClassLoader,這是一個自定義的類加載器
* @author 夜的那種黑丶
*/
public class ClassLoaderTest extends ClassLoader {
public static void main(String[] args) throws Exception {
ClassLoaderTest loader = new ClassLoaderTest("loader");
Class<?> clazz = loader.loadClass("classloader.Test01");
Object object = clazz.newInstance();
System.out.println(object);
System.out.println(object.getClass().getClassLoader());
}
//------------------------------以上為測試代碼---------------------------------
/**
* 類加載器名稱,標識作用
*/
private String classLoaderName;
/**
* 從磁盤讀物字節碼文件的擴展名
*/
private String fileExtension = ".class";
/**
* 創建一個類加載器對象,將系統類加載器當做該類加載器的父加載器
* @param classLoaderName 類加載器名稱
*/
private ClassLoaderTest(String classLoaderName) {
// 將系統類加載器當做該類加載器的父加載器
super();
this.classLoaderName = classLoaderName;
}
/**
* 創建一個類加載器對象,顯示指定該類加載器的父加載器
* 前提是需要有一個類加載器作為父加載器
* @param parent 父加載器
* @param classLoaderName 類加載器名稱
*/
private ClassLoaderTest(ClassLoader parent, String classLoaderName) {
// 顯示指定該類加載器的父加載器
super(parent);
this.classLoaderName = classLoaderName;
}
/**
* 尋找擁有指定二進制名稱的類,重寫ClassLoader類的同名方法,需要自定義加載器遵循雙親委托機制
* 該方法會在檢查完父類加載器之后被loadClass方法調用
* 默認返回ClassNotFoundException異常
* @param className 類名
* @return Class的實例
* @throws ClassNotFoundException 如果類不能被找到,拋出此異常
*/
@Override
protected Class<?> findClass(String className) throws ClassNotFoundException {
byte[] data = this.loadClassData(className);
/*
* 通過defineClass方法將字節數組轉換為Class
* defineClass:將一個字節數組轉換為Class的實例,在使用這個Class之前必須要被解析
*/
return this.defineClass(className, data, 0 , data.length);
}
/**
* io操作,根據類名找到對應文件,返回class文件的二進制信息
* @param className 類名
* @return class文件的二進制信息
* @throws ClassNotFoundException 如果類不能被找到,拋出此異常
*/
private byte[] loadClassData(String className) throws ClassNotFoundException {
InputStream inputStream = null;
byte[] data;
ByteArrayOutputStream byteArrayOutputStream = null;
try {
this.classLoaderName = this.classLoaderName.replace(".", "/");
inputStream = new FileInputStream(new File(className + this.fileExtension));
byteArrayOutputStream = new ByteArrayOutputStream();
int ch;
while (-1 != (ch = inputStream.read())) {
byteArrayOutputStream.write(ch);
}
data = byteArrayOutputStream.toByteArray();
} catch (Exception e) {
throw new ClassNotFoundException();
} finally {
try {
if (inputStream != null) {
inputStream.close();
}
if (byteArrayOutputStream != null) {
byteArrayOutputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return data;
}
}
以上是一段自定義類加載器的代碼,我們執行這段代碼
classloader.Test01@7f31245a
sun.misc.Launcher$AppClassLoader@18b4aac2
可以看見,這段代碼中進行類加載的類加載器還是系統類加載器(AppClassLoader)。這是因為jvm的雙親委托機制造成的,private ClassLoaderTest(String classLoaderName)
將系統類加載器當做我們自定義類加載器的父加載器,jvm的雙親委托機制使自定義類加載器委托系統類加載器完成加載。
改造以下代碼,添加一個path屬性用來指定類加載位置:
public class ClassLoaderTest extends ClassLoader {
public static void main(String[] args) throws Exception {
ClassLoaderTest loader = new ClassLoaderTest("loader");
loader.setPath("/home/fanxuan/Study/java/jvmStudy/out/production/jvmStudy/");
Class<?> clazz = loader.loadClass("classloader.Test01");
System.out.println("class:" + clazz);
Object object = clazz.newInstance();
System.out.println(object);
System.out.println(object.getClass().getClassLoader());
}
//------------------------------以上為測試代碼---------------------------------
/**
* 從指定路徑加載
*/
private String path;
......
/**
* io操作,根據類名找到對應文件,返回class文件的二進制信息
* @param className 類名
* @return class文件的二進制信息
* @throws ClassNotFoundException 如果類不能被找到,拋出此異常
*/
private byte[] loadClassData(String className) throws ClassNotFoundException {
InputStream inputStream = null;
byte[] data;
ByteArrayOutputStream byteArrayOutputStream = null;
className = className.replace(".", "/");
try {
this.classLoaderName = this.classLoaderName.replace(".", "/");
inputStream = new FileInputStream(new File(this.path + className + this.fileExtension));
byteArrayOutputStream = new ByteArrayOutputStream();
int ch;
while (-1 != (ch = inputStream.read())) {
byteArrayOutputStream.write(ch);
}
data = byteArrayOutputStream.toByteArray();
} catch (Exception e) {
throw new ClassNotFoundException();
} finally {
try {
if (inputStream != null) {
inputStream.close();
}
if (byteArrayOutputStream != null) {
byteArrayOutputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return data;
}
public void setPath(String path) {
this.path = path;
}
}
運行一下
class:class classloader.Test01
classloader.Test01@7f31245a
sun.misc.Launcher$AppClassLoader@18b4aac2
修改一下測試代碼,並刪除工程下的Test01.class文件
public static void main(String[] args) throws Exception {
ClassLoaderTest loader = new ClassLoaderTest("loader");
loader.setPath("/home/fanxuan/桌面/");
Class<?> clazz = loader.loadClass("classloader.Test01");
System.out.println("class:" + clazz);
Object object = clazz.newInstance();
System.out.println(object);
System.out.println(object.getClass().getClassLoader());
}
運行一下
class:class classloader.Test01
classloader.Test01@135fbaa4
classloader.ClassLoaderTest@7f31245a
分析
改造后的兩塊代碼,第一塊代碼中加載類的是系統類加載器AppClassLoader,第二塊代碼中加載類的是自定義類加載器ClassLoaderTest。是因為ClassLoaderTest會委托他的父加載器AppClassLoader加載class,第一塊代碼的path直接是工程下,AppClassLoader可以加載到,而第二塊代碼的path在桌面目錄下,所以AppClassLoader無法加載到,然后ClassLoaderTest自身嘗試加載並成功加載到。如果第二塊代碼工程目錄下的Test01.class文件沒有被刪除,那么依然是AppClassLoader加載。
再來測試一塊代碼
public static void main(String[] args) throws Exception {
ClassLoaderTest loader = new ClassLoaderTest("loader");
loader.setPath("/home/fanxuan/Study/java/jvmStudy/out/production/jvmStudy/");
Class<?> clazz = loader.loadClass("classloader.Test01");
System.out.println("class:" + clazz.hashCode());
Object object = clazz.newInstance();
System.out.println(object.getClass().getClassLoader());
ClassLoaderTest loader2 = new ClassLoaderTest("loader");
loader2.setPath("/home/fanxuan/Study/java/jvmStudy/out/production/jvmStudy/");
Class<?> clazz2 = loader2.loadClass("classloader.Test01");
System.out.println("class:" + clazz2.hashCode());
Object object2 = clazz2.newInstance();
System.out.println(object2.getClass().getClassLoader());
}
結果顯而易見,類由系統類加載器加載,並且clazz和clazz2是相同的。
class:2133927002
sun.misc.Launcher$AppClassLoader@18b4aac2
class:2133927002
sun.misc.Launcher$AppClassLoader@18b4aac2
在改造一下
public static void main(String[] args) throws Exception {
ClassLoaderTest loader = new ClassLoaderTest("loader");
loader.setPath("/home/fanxuan/桌面/");
Class<?> clazz = loader.loadClass("classloader.Test01");
System.out.println("class:" + clazz.hashCode());
Object object = clazz.newInstance();
System.out.println(object.getClass().getClassLoader());
ClassLoaderTest loader2 = new ClassLoaderTest("loader2");
loader2.setPath("/home/fanxuan/桌面/");
Class<?> clazz2 = loader2.loadClass("classloader.Test01");
System.out.println("class:" + clazz2.hashCode());
Object object2 = clazz2.newInstance();
System.out.println(object2.getClass().getClassLoader());
}
運行結果
class:325040804
classloader.ClassLoaderTest@7f31245a
class:621009875
classloader.ClassLoaderTest@45ee12a7
ClassLoaderTest是顯而易見,但是clazz和clazz2是不同的,這是因為類加載器的命名空間的原因。
我們可以通過設置父類加載器來讓loader和loader2處於同一命名空間
public static void main(String[] args) throws Exception {
ClassLoaderTest loader = new ClassLoaderTest("loader");
loader.setPath("/home/fanxuan/桌面/");
Class<?> clazz = loader.loadClass("classloader.Test01");
System.out.println("class:" + clazz.hashCode());
Object object = clazz.newInstance();
System.out.println(object.getClass().getClassLoader());
ClassLoaderTest loader2 = new ClassLoaderTest(loader, "loader2");
loader2.setPath("/home/fanxuan/桌面/");
Class<?> clazz2 = loader2.loadClass("classloader.Test01");
System.out.println("class:" + clazz2.hashCode());
Object object2 = clazz2.newInstance();
System.out.println(object2.getClass().getClassLoader());
}
運行結果
class:325040804
classloader.ClassLoaderTest@7f31245a
class:325040804
classloader.ClassLoaderTest@7f31245a
擴展:命名空間
- 每個類加載器都有自己的命名空間,命名空間由該加載器及所有的父加載器所加載的類組成
- 在同一命名空間中,不會出現類的完整名字(包括類的包名)相同的兩個類
- 在不同的命名空間中,有可能會出現類的完整名字(包括類的包名)相同的兩個類