類載入器ClassLoader
一直想搞清楚類載入器是什麼東西,終於有機會好好研究一下。
類載入器是什麼?
定義:將“通過一個類的全限定名來獲取描述此類的二進位制位元組流”這個動作放到JVM外部去實現,以便讓應用程式自己決定如何去獲取所需要的類。實現這個程式碼模組的類就是類載入器——ClassLoader。其實說的通俗一點就是將Class載入到JVM中去。
ClassLoader結構
ClassLoader的三個作用:
將Class載入到JVM中
審查每個類該由誰載入(父優先的等級載入機制)
將Class位元組碼重新解析成JVM統一要求的物件格式
ClassLoader是我們經常用到或擴充套件的抽象類,下面是它的幾個主要方法:
//將byte位元組流解析成JVM能夠識別的Class物件
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
throws ClassFormatError
{
return defineClass(name, b, off, len, null);
}
//實現類的載入規則
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
//載入類
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
...
}
defineClass方法用來將byte位元組流解析成JVM能夠識別的Class物件。這個方法不僅可以使用class檔案例項化物件,還可以通過其他方式:從網路接收到一個類的位元組碼流來建立物件
findClass方法主要用來實現類的載入規則
loadClass用來載入類
類與類載入器
對於任意一個類,都需要由載入它的類載入器和這個類本身一同確立其在JVM中的唯一性,每一個類載入器都有一個獨立的類名稱空間。通俗來說就是:比較兩個類是否相等,只有在這兩個類都是被同一個類載入器載入的前提下進行比較才有意義,否則,即使這兩個類來源於同一個 Class 檔案,被同一個虛擬機器載入,只要載入它們的類載入器不同,那麼這兩個類就必定不相等。
這其實很好理解,就如同你有兩個證,一個是教師資格證,一個是工程師資格證。但是你去應聘教師崗位的時候需要的是教師的身份,不需要工程師的身份,由於證的不同,身份也就發生了變化,即使人是同一個。
下面看一下不同類載入器對instanceof關鍵字運算結果的影響:
public class ClassLoaderTest {
public static void main(String[] args) throws Exception {
//自定義ClassLoader
ClassLoader myLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
InputStream is = getClass().getResourceAsStream(fileName);
if (is == null) {
return super.loadClass(name);
}
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
} catch (IOException e) {
throw new ClassNotFoundException(name);
}
}
};
Object obj = myLoader.loadClass("com.yuangh.classloader.demo.ClassLoaderTest").newInstance();
System.out.println(obj.getClass());
System.out.println(obj instanceof com.yuangh.classloader.demo.ClassLoaderTest);
}
}
//結果
class com.yuangh.classloader.demo.ClassLoaderTest
false
分析: 這裡我們是自己定義了一個類載入器來載入“com.yuangh.classloader.demo.ClassLoaderTest”這個類,並例項化了物件。關於上述的結果,可能會有所迷惑,obj.getClass()的結果都打印出是“com.yuangh.classloader.demo.ClassLoaderTest”了嗎?為什麼用instanceof判斷得到結果是false呢?其實,這是因為虛擬機器中存在了兩個ClassLoaderTest類,一個是由系統應用程式類載入器載入的,另外一個是由我們自定義的類載入器載入的,雖然來自同一個Class檔案,但依然是兩個獨立的類,做物件所屬型別檢查時自然為false。
雙親委派模型
從JVM層面來說,存在兩種不同的類載入器:
一種是啟動類載入器(Bootstrap ClassLoader),該類載入器由C++語言實現,是虛擬機器自身的一部分
另一種就是其他的所有類載入器,這些累加器器由Java語言實現,獨立於JVM外部,並且全部繼承自抽象類java.lang.ClassLoader
從Java開發人員層面來說,類載入器可以分為以下三個層面:
- 啟動類載入器(Bootstrap Classloader)
該類載入器負責將存放在 \lib 目錄中的,或者被 -Xbootstrappath 引數所指定路徑的,並且是被虛擬機器識別的類庫載入到虛擬機器記憶體中來。啟動類載入器無法被Java程式直接引用。
- 擴充套件類載入器(Extension ClassLoader)
該類載入器由 sun.misc.Launcher$ExtClassLoader實現,它負責將 \lib\ext 目錄中的,或者被 java.ext.dirs 系統變數所指定路徑中的所有類庫載入到虛擬機器中,開發者可以直接使用擴充套件類載入器。
- 應用程式類載入器(Application ClassLoader)
該類載入器由sun.misc.Launcher$AppClassLoader 實現。該類是 ClassLoader.getSystemClassLoader() 的返回值,所以一般被稱為系統累加器,它負責載入使用者類路徑(ClassPath)下的所有類庫。開發者可以直接使用這個類載入器,如果應用程式中沒有自定義過自己的類載入器,一般情況下這個就是程式中預設的類載入器。
下面看一下ExtClassLoader和AppClassLoader的載入路徑:
public class Test {
public static void main(String[] args) throws Exception {
System.out.println(ClassLoader.getSystemClassLoader());
System.out.println("============Extensiion ClassLoader===========");
System.out.println(System.getProperty("java.ext.dirs"));
System.out.println("============Application ClassLoader===========");
String[] str = System.getProperty("java.class.path").split(";");
for (String str1 : str) {
System.out.println(str1);
}
}
}
//結果如下:
sun.misc.Launcher[email protected]
============Extensiion ClassLoader===========
C:\Program Files\Java\jdk1.8.0_111\jre\lib\ext;C:\WINDOWS\Sun\Java\lib\ext
============Application ClassLoader===========
C:\Program Files\Java\jdk1.8.0_111\jre\lib\charsets.jar
C:\Program Files\Java\jdk1.8.0_111\jre\lib\deploy.jar
C:\Program Files\Java\jdk1.8.0_111\jre\lib\ext\access-bridge-64.jar
C:\Program Files\Java\jdk1.8.0_111\jre\lib\ext\cldrdata.jar
C:\Program Files\Java\jdk1.8.0_111\jre\lib\ext\dnsns.jar
C:\Program Files\Java\jdk1.8.0_111\jre\lib\ext\jaccess.jar
C:\Program Files\Java\jdk1.8.0_111\jre\lib\ext\jfxrt.jar
C:\Program Files\Java\jdk1.8.0_111\jre\lib\ext\localedata.jar
C:\Program Files\Java\jdk1.8.0_111\jre\lib\ext\nashorn.jar
C:\Program Files\Java\jdk1.8.0_111\jre\lib\ext\sunec.jar
C:\Program Files\Java\jdk1.8.0_111\jre\lib\ext\sunjce_provider.jar
C:\Program Files\Java\jdk1.8.0_111\jre\lib\ext\sunmscapi.jar
C:\Program Files\Java\jdk1.8.0_111\jre\lib\ext\sunpkcs11.jar
C:\Program Files\Java\jdk1.8.0_111\jre\lib\ext\zipfs.jar
C:\Program Files\Java\jdk1.8.0_111\jre\lib\javaws.jar
C:\Program Files\Java\jdk1.8.0_111\jre\lib\jce.jar
C:\Program Files\Java\jdk1.8.0_111\jre\lib\jfr.jar
C:\Program Files\Java\jdk1.8.0_111\jre\lib\jfxswt.jar
C:\Program Files\Java\jdk1.8.0_111\jre\lib\jsse.jar
C:\Program Files\Java\jdk1.8.0_111\jre\lib\management-agent.jar
C:\Program Files\Java\jdk1.8.0_111\jre\lib\plugin.jar
C:\Program Files\Java\jdk1.8.0_111\jre\lib\resources.jar
C:\Program Files\Java\jdk1.8.0_111\jre\lib\rt.jar
C:\Program Files\Java\jdk1.8.0_111\jre\lib\ext\algs4.jar
//專案路徑
D:\ideaIU-2018.1.6.win\project\Java\ClassLoader\target\classes
D:\ideaIU-2018.1.6.win\lib\idea_rt.jar
三個ClassLoader型別的引數展示:
ClassLoader型別 | 引數選項 | 說明 |
---|---|---|
Bootstrap ClassLoader | -Xbootclasspath:-xbootclasspath/a:-Xbootclasspath/p: | 設定Bootstrap ClassLoader的搜尋路徑把路徑新增到已存在的Bootstrap ClassLoader搜尋路徑的後面把路徑新增到已存在的Bootstrap ClassLoader搜尋路徑的前面 |
ExtClassLoader | -Djava.ext.dirs | 設定 ExtClassLoader的搜尋路徑 |
AppClassLoader | -Djava.class.path=-cp 或 -classpath | 設定 AppClassLoader 的搜尋路徑 |
雙親委派模型展示
在上面介紹了三種主要的類載入器,我們的應用程式就是由這三個累加器互相配合進行載入的,如有必要我們也可以加入自定義的類載入器,下面是這些類載入器之間的關係:
上面這種層次關係稱為雙親委派模型,該模型要求除了頂層的啟動類載入器外,其餘的類載入器都應當有自己的父類載入器。類載入器之間的父子關係不會以繼承的關係來實現,而是使用組合關係來複用父載入器的程式碼。
雙親委派模型的工作過程:如果一個類載入器收到了類載入的請求,它不會首先嚐試去載入這個類,而是把這個請求委派給父類載入器去完成,每一個層次的類載入器都是如此,因此所有的類載入請求最終都應該傳送到頂層的啟動類載入器中,只有當父類載入器無法完成這個載入請求時(在它的搜尋範圍內沒有搜尋到所需要的類),子載入器才會嘗試自己去載入。
雙親委派模型的好處:Java類隨著它的類載入器一起具備了一種帶有優先順序的層級關係。例如:java.lang.Object 這個類存放在 rt.jar 之中,無論哪一個類載入器載入這個類,最終都會委派給處於模型頂端的啟動類載入器載入,因此Object類在程式的各種類載入器環境下都是同一個類。
雙親委派模型的實現在java.lang.ClassLoader的loadClass()方法中:
//載入類
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 先檢查請求的類是否已經被載入
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
// 說明父類載入器無法完成載入請求
}
if (c == null) {
// 父類載入器無法載入時
// 使用自己的類載入器載入
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;
}
}
自定義ClassLoader
通過前面的分析介紹,我們已經瞭解了什麼類載入器,以及雙親委派模型的原理,現在我們就自己實現ClassLoader。下面是三種場景:
自定義ClassLoader找到那些不在ClassPath路徑下的class檔案
對於我們要載入的類做特殊處理。例如:保證通過網路傳輸的類的安全性,可以將類經過加密之後再傳輸,在載入到JVM之前先對類的位元組碼再解密。
定義類的實現機制,檢查已經載入的class檔案是否被修改,如果被修改了,可以重新載入這個類,從而實現類的熱部署。
載入自定義路徑下的class檔案
1. 繼承ClassLoader
//測試類
public class ClassLoaderCase {
public void test() {
System.out.println("我是一個測試方法。。。");
}
}
public class PathCLassLoader extends ClassLoader {
private String classPath; //類路徑
private String packageName = "com.yuangh.classloader.demo";
public PathCLassLoader(String classPath) {
this.classPath = classPath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
//如果該類是指定包名下的類,就使用自定義的類載入器載入
if (packageName.startsWith(name)) {
//得到要載入的類的位元組陣列
byte[] classData = getData(name);
//如果為空,代表該類不存在
if (classData == null) {
throw new ClassNotFoundException();
} else {
//將byte位元組流解析為JVM能夠識別的Class物件
return defineClass(name, classData, 0, classData.length);
}
//如果不是,就交給父類載入
} else {
return super.loadClass(name);
}
}
/**
* 得到類的位元組陣列
*/
private byte[] getData(String className) {
String path = classPath + File.separatorChar + className.replace('.', File.separatorChar) + ".class";
try {
InputStream is = new FileInputStream(path);
ByteArrayOutputStream stream = new ByteArrayOutputStream();
byte[] buffer = new byte[2048];
int num = 0;
while((num = is.read(buffer)) != -1) {
stream.write(buffer, 0, num);
}
return stream.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
public static void main(String[] args) throws Exception {
PathCLassLoader pathCLassLoader = new PathCLassLoader("D:\\ideaIU-2018.1.6.win\\project\\Java\\ClassLoader\\src\\main\\java\\com\\yuangh\\classloader\\demo\\");
Class<?> classLoadercase1 = pathCLassLoader.findClass("com.yuangh.classloader.demo.ClassLoaderCase");
ClassLoaderCase classLoadercase2 = (ClassLoaderCase) classLoadercase1.newInstance();
classLoadercase2.test();
}
}
//結果:
我是一個測試方法。。。
2. 繼承URLClassLoader
public class URLPathClassLoader extends URLClassLoader {
private String packageName = "com.yuangh.classloader.demo";
public URLPathClassLoader(URL[] classPath, ClassLoader parent) {
super(classPath, parent);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class<?> aClass = findLoadedClass(name);
if (aClass != null) {
return aClass;
}
if (!packageName.startsWith(name)) {
return super.loadClass(name);
} else {
return findClass(name);
}
}
}
載入自定義格式的class檔案
public class NetCLassLoader extends ClassLoader {
private String classPath; //類路徑
private String packageName = "com.yuangh.classloader.demo";
public NetCLassLoader(String classPath) {
this.classPath = classPath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class<?> aClass = findLoadedClass(name);
if (aClass != null) {
return aClass;
}
if (packageName.startsWith(name)) {
//得到要載入的類的位元組陣列
byte[] classData = getData(name);
//如果為空,代表該類不存在
if (classData == null) {
throw new ClassNotFoundException();
} else {
//將byte位元組流解析為JVM能夠識別的Class物件
return defineClass(name, classData, 0, classData.length);
}
//如果不是,就交給父類載入
} else {
return super.loadClass(name);
}
}
/**
* 得到類的位元組陣列
*/
private byte[] getData(String className) {
String path = classPath + File.separatorChar + className.replace('.', File.separatorChar) + ".class";
try {
URL url = new URL(path);
InputStream is = url.openStream();
ByteArrayOutputStream stream = new ByteArrayOutputStream();
byte[] buffer = new byte[2048];
int num = 0;
while((num = is.read(buffer)) != -1) {
stream.write(buffer, 0, num);
}
return stream.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
public static void main(String[] args) throws Exception {
NetCLassLoader netCLassLoader = new NetCLassLoader("D:\\ideaIU-2018.1.6.win\\project\\Java\\ClassLoader\\src\\main\\java\\com\\yuangh\\classloader\\demo\\");
Class<?> classLoadercase1 = netCLassLoader.findClass("com.yuangh.classloader.demo.ClassLoaderCase");
ClassLoaderCase classLoadercase2 = (ClassLoaderCase) classLoadercase1.newInstance();
classLoadercase2.test();
}
}
//結果
我是一個測試方法。。。
實現類的熱部署
前面已經說過,JVM表示兩個類是否是同一個類會有兩個條件:一是看完整類名是否一樣,包括包名。二是看載入這個類的ClassLoader是否是同一個類載入器類例項。即使是同一個累加器類的兩個例項,載入同一個類也會不一樣。所以要實現熱部署可以建立不同的ClassLoader例項物件,然後通過這個不同的例項物件來載入同名的類。
public class ClassReloader extends ClassLoader {
private String classPath; //類路徑
String classname = "com.yuangh.classloader.demo.ClassLoaderCase";
public ClassReloader(String classPath) {
this.classPath = classPath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = getData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
return defineClass(classname, classData, 0, classData.length);
}
}
/**
* 得到類的位元組陣列
*/
private byte[] getData(String className) {
String path = classPath + className;
try {
InputStream is = new FileInputStream(path);
ByteArrayOutputStream stream = new ByteArrayOutputStream();
byte[] buffer = new byte[2048];
int num = 0;
while ((num = is.read(buffer)) != -1) {
stream.write(buffer, 0, num);
}
return stream.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
public static void main(String[] args) {
try {
String path = "D:\\ideaIU-2018.1.6.win\\project\\Java\\ClassLoader\\target\\classes\\com\\yuangh\\classloader\\demo\\";
ClassReloader reloader = new ClassReloader(path);
Class r = reloader.findClass("ClassLoaderCase.class");
System.out.println(r.newInstance());
ClassReloader reloader2 = new ClassReloader(path);
Class r2 = reloader2.findClass("ClassLoaderCase.class");
System.out.println(r2.newInstance());
System.out.println(r.newInstance() == r2.newInstance());
} catch (Exception e) {
e.printStackTrace();
}
}
}
//結果:
[email protected]45ee12a7
[email protected]4b67cf4d
false
參考
《深入理解Java虛擬機器》 《深入分析Java Web技術內幕》