一、Java的類加載簡介
先了解一個基本概念,類的初始化。
當程序使用某個類時,如果該類還沒被初始化,加載到內存中,則系統會通過加載、鏈接、初始化三個過程來對該類進行初始化,該過程稱爲類的初始化。
那麼類加載又是啥呢?說到類加載就必須要知道java.lang.ClassLoader類,它的基本職責就是根據一個指定的類的名稱,找到或者生成其對應的字節代碼,然後從這些字節代碼中定義出一個Java 類,即 java.lang.Class類的一個實例。它有以下幾個重要的方法:
- getParent():返回該類加載器的父類加載器;
- loadClass(String className):加載名稱爲className的類,返回的結果是java.lang.Class類的實例;
- findClass(String className):查找名稱爲className的類,返回的結果是java.lang.Class類的實例;
- findLoadedClass(String className):查找名稱爲className的已經被加載過的類,返回的結果是java.lang.Class類的實例;
- defineClass(String className, byte[] b, int off, int len):把字節數組b中的內容轉換成Java類,返回的結果是java.lang.Class的實例,這個方法被聲明爲final的;
- resolveClass(Class
二、JVM中的類加載器
類加載器就是負責加載所有的類,並將其載入內存中,生成一個java.lang.Class的實例。一旦一個類被加載到JVM中之後,就不會被再次載入了。
類加載器通常無須等到“首次使用”該類時才加載該類,JVM允許系統預先加載某些類
Java 中的類加載器大致可以分成兩類,一類是系統提供的,另外一類則是由 Java 應用開發人員編寫的。
- 根類加載器(Bootstrap ClassLoader):其負責加載Java的核心類,比如String、System這些類,是用原生C++代碼來實現的,並不繼承自java.lang.ClassLoader;
- 拓展類加載器(Extension ClassLoader):其負責加載JRE的拓展類庫(jre/ext/*.jar),Java 虛擬機的實現會提供一個擴展庫目錄。該類加載器在此目錄裏面查找並加載Java類;
- 系統類加載器(System ClassLoader):其負責加載CLASSPATH環境變量所指定的JAR包和類路徑,一般來說,Java應用的類都是由它來完成加載的。可以通過 ClassLoader.getSystemClassLoader()來獲取它;
- 自定義類加載器(Custom ClassLoader):用戶自定義的加載器,以java.lang.ClassLoader類爲父類。
測試一下類加載器的層次關係:
public static void main(String[] args) {
//application class loader
System.out.println(ClassLoader.getSystemClassLoader());
//extensions class loader
System.out.println(ClassLoader.getSystemClassLoader().getParent());
//bootstrap class loader
System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent());
}
打印結果如下:
sun.misc.Launcher$AppClassLoader@3e55a58f
sun.misc.Launcher$ExtClassLoader@68e86f41
null
可以看出ClassLoader類是由AppClassLoader加載的,它的父加載器是ExtClassLoader,而ExtClassLoader的父加載器無法獲取,是因爲它是用C++實現的。
注意,這裏類加載器之間的父子關係並不是繼承關係,而是類加載器實例之間的關係。
三、JVM中的類加載機制
- 全盤負責機制,當一個類加載器負責加載某個Class時,該Class所依賴的和引用的其他Class也將由該類加載器負責載入,除非顯示使用另外一個類加載器來載入;
- 雙親委託機制,首先將加載任務委託交給父類加載器,父類加載器又將加載任務向上委託,直到最父類加載器,如果最父類加載器可以完成類加載任務,就成功返回,如果不行就向下傳遞委託任務,由其子類加載器進行加載;
- 代理機制:與雙親委派機制相反,代理模式是先自己嘗試加載,如果無法加載則向上傳遞,tomcat就是代理模式;
- 緩存機制,緩存機制將會保證所有加載過的Class都會被緩存,當程序中需要使用某個Class時,類加載器先從緩存區尋找該Class,只有緩存區不存在,系統纔會讀取該類對應的二進制數據,並將其轉換成Class對象,存入緩存區。這就是爲什麼修改了Class後,必須重啓JVM,程序的修改纔會生效。
四、自定義類加載器實例
public class MyClassLoader extends ClassLoader{
private String rootPath;
public MyClassLoader(String rootPath){
this.rootPath = rootPath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
//check if the class have been loaded
Class<?> c = findLoadedClass(name);
if(c!=null){
return c;
}
//load the class
byte[] classData = getClassData(name);
if(classData==null){
throw new ClassNotFoundException();
}
else{
c = defineClass(name,classData, 0, classData.length);
return c;
}
}
private byte[] getClassData(String className){
String path = rootPath+"/"+className.replace('.', '/')+".class";
InputStream is = null;
ByteArrayOutputStream bos = null;
try {
is = new FileInputStream(path);
bos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int temp = 0;
while((temp = is.read(buffer))!=-1){
bos.write(buffer,0,temp);
}
return bos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
}finally{
try {
is.close();
bos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}
}
創建一個測試類HelloWorld:
package testOthers;
public class HelloWorld {
}
在D盤根目錄創建一個testOthers文件夾,編譯HelloWorld.java,將得到的class文件放到testOthers文件夾下。
利用如下代碼進行測試:
public class testMyClassLoader {
@Test
public void test() throws Exception{
MyClassLoader loader = new MyClassLoader("D:");
Class<?> c = loader.loadClass("testOthers.HelloWorld");
System.out.println(c.getClassLoader());
}
}
最終輸出:
test.ClassLoader.MyClassLoader@3e55a58f
說明HelloWorld類是被我們的自定義類加載器MyClassLoader加載的。
五、Java類加載器的過程
JVM將類加載的過程分爲三個步驟:裝載(Load),鏈接(Link)和初始化(Initialize)。
1.裝載:
查找並加載類的二進制數據;
2.鏈接:
當類被加載後,系統會爲之生成一個Class對象,接着將會進入鏈接階段,鏈接階段負責把類的二進制數據合併到JRE中,它有三個階段:
- 驗證:確保被加載類信息符合JVM規範、沒有安全方面的問題。
- 準備:爲類的靜態變量分配內存,並將其初始化爲默認值。
- 解析:把虛擬機常量池中的符號引用轉換爲直接引用。
3.初始化:
當一個Java類第一次被真正使用到的時候,JVM會進行該類的初始化操作。初始化過程的主要操作是執行靜態代碼塊和初始化靜態域。在一個類被初始化之前,它的直接父類也需要被初始化。
Java類和接口的初始化只有在特定的時機纔會發生,這些時機包括:
- 創建一個Java類的實例。如
MyClass obj = new MyClass()
- 調用一個Java類中的靜態方法。如
MyClass.sayHello()
- 給Java類或接口中聲明的靜態域賦值。如
MyClass.value = 10
- 訪問Java類或接口中聲明的靜態域,並且該域不是常值變量。如
int value = MyClass.value
- 在頂層Java類中執行assert語句。
- 使用反射方式強制創建某個類或接口對應的java.lang.Class對象。
- 初始化某個類的子類,則其父類也會被初始化。
JVM初始化類的步驟:
- 如果這個類還沒有被加載和鏈接,那先進行加載和鏈接;
- 假如這個類存在直接父類,並且這個類還沒有被初始化(注意:在一個類加載器中,類只能初始化一次),那就初始化直接的父類(不適用於接口)
- 如果類中存在static標識的塊,那就依次執行這些初始化語句。
Java 中,虛擬機會爲每個加載的類維護一個常量池【不同於字符串常量池,這個常量池只是該類的字面值(例如類名、方法名)和符號引用的有序集合。 而字符串常量池,是整個JVM共享的】這些符號(如int a = 5;中的a)就是符號引用,而解析過程就是把它轉換成指向堆中的對象地址的相對地址。