1). 位元組碼和.class檔案差別
建立Java對象時,JVM将這個對象對應的位元組碼加載到記憶體中,這個位元組碼的原始資訊存放在classpath目錄下(及Java工程的bin目錄下)的.class檔案中,類加載需要将.class檔案導入硬碟,經過處理變成位元組碼加載到記憶體。
示例:
public class ClassLoaderTest {
public static void main(String[] args) {
// 輸出ClassLoaderTest的類加載器名稱
System.out.println("ClassLoaderTest類的類加載器名稱:" + ClassLoaderTest.class.getClassLoader().getClass().getName());
System.out.println("System類的類加載器名稱:" + System.class.getClassLoader());
System.out.println("List類的類加載器名稱:" + List.class.getClassLoader());
// 擷取ClassLoaderTest類的類加載器
ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
while (null != classLoader) {
System.out.println(classLoader.getClass().getName() + "->");
// 擷取類加載器的父類
classLoader = classLoader.getParent();
}
System.out.println(classLoader);
}
}
列印結果:
圖1.png
從列印結果可以看出ClassLoaderTest類是有AppClassLoader加載的。
因為System類,List,Map等這樣的系統提供jar類都在rt.jar中,是以由BootStrap類加載器加載,因為BootStrap是祖先類,不是Java編寫的,是以列印出class為null。詳情請看第三點:類加載器的委托機制
2). Java虛拟中的類加載器
Java虛拟機中可以安裝多個類加載器,系統預設的有三個,每個類負責加載特定位置的類:BootStrap,ExtClassLoader,AppClassLoader
由于類加載器本身是Java類,也需要被類加載器加載,是以必須有一個為類加載器不是Java類,是以Bootstrap是由C/C++代碼編寫,被封裝到JVM核心中,ExtClassLoader和AppClassLoader是Java類。
類加載器屬性結構圖:
圖2.png
3). 類加載器的委托機制
問題:Java虛拟機要加載第一個類的時候,使用哪一個類加載器加載?
(1). 目前線程的類加載器加載線程中的第一個類(目前線程的類加載器:Thread類中有一個get/setContextClassLoader(ClassLoader);方法,可以擷取/指定本線程中的類加載器)
(2). 如果類A引用了類B,Java虛拟機将使用加載類A的加載器來加載類B
(3). 可以直接調用ClassLoader.loadClass(String className)方法來指定某個類加載器去加載某個類
每個類加載器加載類時,先委托其上級類加載器來加載,當所有父加載器沒有加載到類時,回到發起者類加載器,如果還是加載不了,則會抛出ClassNotFoundException。
好處:
能夠很好的統一管理類加載,首先交給上級,如果上級有,就加載,這樣如果之前已經加載過的類,這時候在來加載它的時候隻要拿過來用就可以了,無需二次加載
4). 測試ExtClassLoader
- 将ClassLoaderTest類打包為Jar檔案,拷貝至
路徑 圖3.pngjdk\jre\lib\ext\下
- 接下來在eclipse中運作ClassLoaderTest檔案,其列印結果為
圖4.png
列印結果驗證了類加載器的委托機制.
測試完成後記得将test.jar删除
5). 類加載器主要方法
- 預設的類加載器
System.out.println("預設的類加載器:"+ClassLoader.getSystemClassLoader());
圖5.png
- loadClass(String name) 源碼
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
// 加鎖,進行同步處理,可能多個線程加載類
synchronized (getClassLoadingLock(name)) {
// 檢查是否已經加載過
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// 如果自定義的類加載器的parent不為null,就調用parent的loadClass進行類加載
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 如果自定義的類加載器的parent為null,就調用findBootstrapClassOrNull方法查找類,就是BootStrap類加載器
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 類未發現異常
}
if (c == null) {
// 如果仍然沒有發現,就調用自己的findClass方法進行進行類加載
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;
}
}
- defineClass,将class檔案的位元組數組編譯成一個class對象,不能重寫,内部實作為C/C++代碼 圖6.png
6). 自定義ClassLoader
- 定義一個被加載的類
/**
* 加載類
* @author mazaiting
*/
public class ClassLoaderAttachment extends Date{
private static final long serialVersionUID = -1892974244318329380L;
@Override
public String toString() {
return "Hello ClassLoader";
}
}
- 自定義ClassLoader
/**
* 自定義類加載器
* 步驟:
* 1. 繼承ClasssLoader
* 2. 重寫findClass(String name) 方法
* @author mazaiting
*/
public class MyClassLoader extends ClassLoader{
/**需要加載類.class檔案的目錄*/
private String classDir;
/**無參構造*/
public MyClassLoader() {}
public MyClassLoader(String classDir) {
this.classDir = classDir;
}
// 測試,先将ClassLoaderAttachment.class檔案加密到工程的class_temp目錄下
public static void main(String[] args) throws Exception {
// 配置運作參數
// ClassLoaderAttachment.class原路徑--E:\test\java_advanced\ClassLoader\bin\com\mazaiting\ClassLoaderAttachment.class
String srcPath = args[0];
// ClassLoaderAttachment.class輸出路徑--E:\test\java_advanced\ClassLoader\class_temp\
String desPath = args[1];
// 擷取檔案名
String desFileName = srcPath.substring(srcPath.lastIndexOf("\\") +1);
// 配置目标檔案路徑
String desPathFile = desPath + "/" + desFileName;
FileInputStream fis = new FileInputStream(srcPath);
FileOutputStream fos = new FileOutputStream(desPathFile);
// 将class進行加密
encodeAndDecode(fis, fos);
fis.close();
fos.close();
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// class檔案的路徑
String classPathFile = classDir + "/" + name + ".class";
try {
// 将class檔案進行解密
FileInputStream fis = new FileInputStream(classPathFile);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
encodeAndDecode(fis, baos);
byte[] classByte = baos.toByteArray();
// 将位元組流變成一個class
return defineClass(null, classByte, 0, classByte.length);
} catch (Exception e) {
e.printStackTrace();
}
return super.findClass(name);
}
/**
* 加密解密
* @param is 輸入流
* @param os 輸出流
* @throws IOException
*/
private static void encodeAndDecode(InputStream is, OutputStream os) throws IOException {
int len;
while ((len = is.read()) != -1) {
len ^= 0xFF;
os.write(len);
}
}
}
- 運作:在MyClassLoader的main方法右鍵 -> Run As -> Run Configurations...,進行如下配置(配置前請確定ClassLoader工程目錄下存在class_temp檔案夾,其中第一個參數是ClassLoaderAttachment.class檔案的源路徑,第二個參數是加密後存放的目錄),點選Run運作,此時在
将會生成加密後的E:\test\java_advanced\ClassLoader\class_temp
檔案 圖7.pngClassLoaderAttachment.class
- 使用,在
的main函數中應用ClassLoaderTest
public class ClassLoaderTest {
public static void main(String[] args) {
try {
// 加載類
Class classDate = new MyClassLoader("class_temp").loadClass("ClassLoaderAttachment");
// 建立執行個體
Date date = (Date) classDate.newInstance();
// 輸出ClassLoaderAttachment類的加載器名稱
System.out.println("ClassLoader: " + date.getClass().getClassLoader().getClass().getName());
// 調用方法
System.out.println(date.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
}
圖8.png
- 如果loadClass方法的參數為類的全路徑名
// 加載類
Class classDate = new MyClassLoader("class_temp").loadClass("com.mazaiting.ClassLoaderAttachment");
圖9.png
- 将
路徑下的ClassLoaderAttachment.class複制到\class_temp\
路徑下,然後運作\bin\com\mazaiting\
Exception in thread "main" java.lang.ClassFormatError: Incompatible magic value 889275713 in class file com/mazaiting/ClassLoaderAttachment
圖10.png
不合适的魔數錯誤(class檔案的前六個位元組是個魔數用來辨別class檔案的),因為加密後的資料AppClasssLoader無法解析。