類載入器 - ClassLoader詳解
獲得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是不同的,這是因為類載入器的名稱空間的原因。
擴充套件:名稱空間
- 每個類載入器都有自己的名稱空間,名稱空間由該載入器及所有的父載入器所載入的類組成
- 在同一名稱空間中,不會出現類的完整名字(包括類的包名)相同的兩個類
- 在不同的名稱空間中,有可能會出現類的完整名字(包括類的包名)相同的兩個類