這兩篇文章分别介紹了類的生命周期、ClassLoader類。今天,我們繼續介紹類的熱加載。
一、類的解除安裝
1、引用關系:
當某個類被類加載器加載到記憶體後,就會生成一個相應的Class對象。他們的關系如下:
1)類加載器和Class對象:
- 在類加載器的内部實作中,用一個Java集合來存放所加載類的Class執行個體的引用。
- 另一方面,一個Class對象總是會引用它的類加載器。調用Class對象的getClassLoader()方法,就能獲得它的類加載器。
由此可見,Class執行個體和加載它的加載器之間為雙向關聯關系。
2)類、類的執行個體對象、類的Class對象:
- 一個類的執行個體總是引用代表這個類的Class對象,在Object類中定義了getClass()方法,這個方法傳回代表對象所屬類的Class對象的引用。
- 此外,所有的Java類都有一個靜态屬性class,它引用代表這個類的Class對象。

2、解除安裝類:
由Java虛拟機自帶的類加載器(bootstrap、ext、app)所加載的類,在虛拟機的生命周期中,始終不會被解除安裝。因為,Java虛拟機本身會始終引用這些類加載器,而這些類加載器則會始終引用它們所加載的類的Class對象,是以這些Class對象始終是可觸及的。
由使用者自定義的類加載器加載的類是可以被解除安裝的。先看一下如何使用自定義加載器加載一個類,并建立類的執行個體。
代碼也許不準确,但是過程很清晰,整個過程涉及三個變量,而且他們之間的引用關系很明确,即執行個體對象引用着類對應的Class對象,Class對象引用着類加載器,調用類的getClassLoader()方法可以獲得該類的加載器。
1)類何時被解除安裝?
解除安裝類,其實就是解除安裝類對應的Class執行個體,以及方法區中關于該類對應的資料結構。是以要想解除安裝類必須:
- 先解除安裝該類的加載器,這很好了解,因為類引用着類加載器;
- 然後将類所建立的執行個體都解除安裝掉;
隻有當這個類的加載器和類的執行個體都結束了生命周期,這個類的生命周期才結束。即
有一種情況,就是類加載器被解除安裝掉了,但是類還沒有被解除安裝掉,這個時候如果有需要用這個類,就可以重新使用不需要加載,方法區的二進制資料結構也是原來的。如果類已經被解除安裝了,就需要重新加載這個類,當然方法區中的資料結構以及類資訊都是重新加載的。
總結:
- 被bootstrap、Ext、App加載的類運作期間不會被解除安裝;
- 被自定義類加載器加載的類運作期間可以被解除安裝,但是幾率也很小,因為涉及到垃圾回收等;
2)NoClassDefFoundError suddenly:
該錯誤線上上環境遇到過一次。
二、熱加載
1、自定義類加載器:
通過前面的分析可知,實作自定義類加載器需要繼承ClassLoader或者URLClassLoader,繼承ClassLoader則需要自己重寫findClass()方法并編寫加載邏輯,繼承URLClassLoader則可以省去編寫findClass()方法以及class檔案加載轉換成位元組碼流的代碼。那麼編寫自定義類加載器的意義何在呢?
- 當class檔案不在ClassPath路徑下,預設系統類加載器無法找到該class檔案,在這種情況下我們需要實作一個自定義的ClassLoader來加載特定路徑下的class檔案生成class對象。
- 當一個class檔案是通過網絡傳輸并且可能會進行相應的加密操作時,需要先對class檔案進行相應的解密後再加載到JVM記憶體中,這種情況下也需要編寫自定義的ClassLoader并實作相應的邏輯。
- 當需要實作熱部署功能時(一個class檔案通過不同的類加載器産生不同class對象進而實作熱部署功能),需要實作自定義ClassLoader的邏輯。
接下來我們看幾個例子。
1.1)繼承ClassLoader實作自定義File類加載器:
package cn.nuc.edu.LogTest.load;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
public class FileClassLoader extends ClassLoader{
private String rootPath;
public FileClassLoader(String rootPath) {
super();
this.rootPath = rootPath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = null;
InputStream ins = null;
ByteArrayOutputStream baos = null;
//1、讀取類檔案的位元組碼
try {
ins = new FileInputStream(classNameToPath(name));
baos = new ByteArrayOutputStream();
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int bytesNumRead = 0;
while ((bytesNumRead = ins.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
classData = baos.toByteArray();
} catch (IOException e) {
throw new ClassNotFoundException();
} finally {
try {
if (baos != null) baos.close();
if (ins != null) ins.close();
} catch (Exception e) {
}
}
//2、生成class對象
if (classData == null) {
throw new ClassNotFoundException();
} else {
return defineClass(name, classData, 0, classData.length);
}
}
private String classNameToPath(String className) {
return rootPath + File.separatorChar
+ className.replace('.', File.separatorChar) + ".class";
}
public static void main(String[] args) throws Exception {
//建立自定義檔案類加載器
FileClassLoader loader = new FileClassLoader("E:");
Class<?> class1 = loader.loadClass("cn.nuc.edu.LogTest.load.Demo");
System.out.println(class1.getClassLoader());
System.out.println(class1.newInstance().toString());
}
}
輸出:
cn.nuc.edu.LogTest.load.FileClassLoader@4614ac54
demo2...
說明:FileClassLoader繼承ClassLoader重寫findClass方法,保證了雙親委派模型。在main方法中調用了loadClass()方法加載指定路徑下的class檔案,由于bootstrap、Ext以及App類加載器都無法在其路徑下找到該類,是以最終将有自定義類加載器加載,即調用findClass()方法進行加載。
注:Demo.java不能放到IDE中。因為JVM加載類不關心rootPath,它隻關心包名組成的路徑和檔案名(類名),rootPath是由具體的類加載器定義死的(App查找classpath路徑),又因為FileClassLoader繼承自ClassLoader,調用loadClass方法會根據雙親委派會直接交給上級類加載器加載,最終會被AppClassloader在IED的classpath上加載(執行AppClassLoader中的findClass方法),進而不會執行FileClassLoader的findClass方法了。是以,即使上面的rootPath是E:,也不能将Demo.java放到IDE中。
注:如果将上面改成loader.findClass("cn.nuc.edu.LogTest.load.Demo"),無論Demo.java是否在IED中,最終都會是自定義類加載器加載Demo。(繞過了雙親委派)
1.2)繼承URLClassLoader實作自定義File類加載器:
package cn.nuc.edu.LogTest.load;
import java.io.File;
import java.net.URI;
import java.net.URL;
import java.net.URLClassLoader;
public class FileClassLoader2 extends URLClassLoader {
public FileClassLoader2(URL[] urls) {
super(urls);
}
public static void main(String[] args) throws Exception {
String rootDir = "E:\\";
// File to URI
URI uri = new File(rootDir).toURI();
URL[] urls = { uri.toURL() };
// 建立自定義檔案類加載器
FileClassLoader2 loader = new FileClassLoader2(urls);
Class<?> class1 = loader.loadClass("cn.nuc.edu.LogTest.load.Demo");
System.out.println(class1.getClassLoader());
System.out.println(class1.newInstance().toString());
loader.close();
}
}
輸出:
cn.nuc.edu.LogTest.load.FileClassLoader@4614ac54
demo2...
說明:繼承URLClassLoader,不需要自己再去實作findClass方法了,更加簡單。同樣,Demo.java不能放到IDE中。
注意:findClass()方法在URLClassLoader類中是protected的,而被protected修飾的成員對于本包和其子類可見,是以FileClassLoader2可以調用findClass()方法。
1.3)自定義網絡類加載器:
可以自己實作findClass也可以繼承URLClassLoader。
URL url = new URL(path);
InputStream ins = url.openStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int bytesNumRead = 0;
// 讀取類檔案的位元組
while ((bytesNumRead = ins.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
1.4)加載jar包:
http://icejoywoo.github.io/2017/02/22/jvm-classloader-issue.html
2、熱加載類:
想要知道熱部署的原理,必須要了解java類的加載過程:首先将java檔案編譯成class檔案,然後由類加載器将位元組碼檔案加載到記憶體生成類對應的Class對象,接下來連結、初始化。。。
一般在系統中,類的加載都是由系統自帶的類加載器完成,而且對于同一個全限定名的java類,隻能被加載一次,而且無法被解除安裝,而且上面也分析過類的解除安裝不可靠。那該如何做呢?
所謂的熱部署就是利用同一個class檔案不同的類加載器在記憶體建立出兩個不同的class對象,由于JVM在加載類之前會檢測請求的類是否已加載過(即在loadClass()方法中調用findLoadedClass()方法),如果被加載過,則直接從緩存擷取,不會重新加載。注意同一個類加載器的執行個體和同一個class檔案隻能被加載器一次,多次加載将報錯,是以我們實作的熱部署必須讓同一個class檔案可以根據不同的類加載器重複加載,以實作所謂的熱部署。
2.1)實作方式:
- 如果要被熱加載的類不會被bootstrap、Ext、App類加載器加載到(不在其路徑下),可以建立不同的自定義類加載器執行個體,然後調用loadClass()、findClass()方法,進而熱加載該類;(反正上面那些類加載器也加載不到,是以即使有雙親委派也沒事)
- 如果要被熱加載的類會被bootstrap、Ext、App類加載器加載到,建立不同的自定義類加載器執行個體,然後調用findClass()方法,進而熱加載該類;(繞過雙親委派,以及findLoadedClass()緩存的檢查)
- 重寫loadClass方法,破壞雙親委派;(強烈不建議)
說明:真正的類加載是調用了findClass()方法完成的(Ext、App也都是)。loadClass方法隻是雙親委派算法。
2.2)示例1:要熱替換的Demo類可以被AppClassLoader加載到
1)使用FileClassLoader自定義類測試:
實際上前面的FileClassLoader已具備這個功能,但前提是直接調用findClass()方法,而不是調用loadClass()方法,因為ClassLoader中loadClass()方法體中調用findLoadedClass()方法進行了檢測是否已被加載,是以我們直接調用findClass()方法就可以繞過這個問題。
A)使用loadClass方法,無法多次加載同一個類:
public static void main(String[] args) throws Exception {
//建立自定義檔案類加載器
FileClassLoader loader = new FileClassLoader("E:\\workspace_suike\\LogTest\\target\\classes");//"E:"
Class<?> class1 = loader.loadClass("cn.nuc.edu.LogTest.load.Demo");
System.out.println(class1.getClassLoader());
System.out.println(class1.hashCode());
FileClassLoader loader2 = new FileClassLoader("E:\\workspace_suike\\LogTest\\target\\classes");
Class<?> class2 = loader2.loadClass("cn.nuc.edu.LogTest.load.Demo");
System.out.println(class2.getClassLoader());
System.out.println(class2.hashCode());
}
//輸出
sun.misc.Launcher$AppClassLoader@7d05e560
607547411
sun.misc.Launcher$AppClassLoader@7d05e560
607547411
說明:由于Demo類可以被AppClassLoader加載到,是以雖然建立了兩個自定義類加載器執行個體,由于雙親委派,也無法使用自定義類加載器來加載該類。
B)使用findClass方法,多次加載同一個類:
public static void main(String[] args) throws Exception {
//建立自定義檔案類加載器
FileClassLoader loader = new FileClassLoader("E:\\workspace_suike\\LogTest\\target\\classes");//"E:"
Class<?> class1 = loader.findClass("cn.nuc.edu.LogTest.load.Demo");
System.out.println(class1.getClassLoader());
System.out.println(class1.hashCode());
FileClassLoader loader2 = new FileClassLoader("E:\\workspace_suike\\LogTest\\target\\classes");
Class<?> class2 = loader2.findClass("cn.nuc.edu.LogTest.load.Demo");
System.out.println(class2.getClassLoader());
System.out.println(class2.hashCode());
}
//輸出
cn.nuc.edu.LogTest.load.FileClassLoader@1a3a2a52
1649907150
cn.nuc.edu.LogTest.load.FileClassLoader@642c39d2
1433743869
說明:這裡調用了findClass方法,繞過了雙親委派檢測,是以雖然AppClassLoader可以加載到Demo,但仍然使用的是自定義類加載器來加載的Demo類。此外如果用同一個類加載,調用兩次findClass方法加載同一個類,會報LinkError錯誤。
2)使用FileClassLoader2類做測試:
A)調用loadClass方法,無法多次加載同一個類:
public class FileClassLoader2 extends URLClassLoader {
public FileClassLoader2(URL[] urls) {
super(urls);
}
public static void main(String[] args) throws Exception {
String rootDir = "E:\\";
URI uri = new File(rootDir).toURI();// File to URI
URL[] urls = { uri.toURL() };
FileClassLoader2 loader1 = new FileClassLoader2(urls);
Class<?> class1 = loader1.loadClass("cn.nuc.edu.LogTest.load.Demo");
System.out.println(class1.getClassLoader());
System.out.println(class1.hashCode());
FileClassLoader2 loader2 = new FileClassLoader2(urls);
Class<?> class2 = loader2.loadClass("cn.nuc.edu.LogTest.load.Demo");
System.out.println(class2.getClassLoader());
System.out.println(class2.hashCode());
}
}
//輸出
sun.misc.Launcher$AppClassLoader@5736ab79
2053014315
sun.misc.Launcher$AppClassLoader@5736ab79
2053014315
B)使用findClass方法,多次加載同一個類:
public class FileClassLoader2 extends URLClassLoader {
public FileClassLoader2(URL[] urls) {
super(urls);
}
public static void main(String[] args) throws Exception {
String rootDir = "E:\\";
URI uri = new File(rootDir).toURI();// File to URI
URL[] urls = { uri.toURL() };
FileClassLoader2 loader1 = new FileClassLoader2(urls);
Class<?> class1 = loader1.findClass("cn.nuc.edu.LogTest.load.Demo");
System.out.println(class1.getClassLoader());
System.out.println(class1.hashCode());
FileClassLoader2 loader2 = new FileClassLoader2(urls);
Class<?> class2 = loader2.findClass("cn.nuc.edu.LogTest.load.Demo");
System.out.println(class2.getClassLoader());
System.out.println(class2.hashCode());
}
}
//輸出:
cn.nuc.edu.LogTest.load.FileClassLoader2@1e0196f8
1391835856
cn.nuc.edu.LogTest.load.FileClassLoader2@2bbd83d
892236228
注:雖然URLClassLoader有close方法,但例子中沒有調用該方法也可以加載新資源。
2.3)示例2:要熱替換的Demo類無法被AppClassLoader加載到
這裡我們就用FileClassLoader2做個測試:
1)使用loadClass方法:
public class FileClassLoader2 extends URLClassLoader {
public FileClassLoader2(URL[] urls) {
super(urls);
}
public static void main(String[] args) throws Exception {
String rootDir = "E:\\";
URI uri = new File(rootDir).toURI();// File to URI
URL[] urls = { uri.toURL() };
FileClassLoader2 loader1 = new FileClassLoader2(urls);
Class<?> class1 = loader1.loadClass("cn.nuc.edu.LogTest.load.Demo");
System.out.println(class1.getClassLoader());
System.out.println(class1.hashCode());
FileClassLoader2 loader2 = new FileClassLoader2(urls);
Class<?> class2 = loader2.loadClass("cn.nuc.edu.LogTest.load.Demo");
System.out.println(class2.getClassLoader());
System.out.println(class2.hashCode());
}
}
//輸出
cn.nuc.edu.LogTest.load.FileClassLoader2@5fcbc39b
1931573327
cn.nuc.edu.LogTest.load.FileClassLoader2@1a61c596
195780229
2)使用findClass方法:
public class FileClassLoader2 extends URLClassLoader {
public FileClassLoader2(URL[] urls) {
super(urls);
}
public static void main(String[] args) throws Exception {
String rootDir = "E:\\";
URI uri = new File(rootDir).toURI();// File to URI
URL[] urls = { uri.toURL() };
FileClassLoader2 loader1 = new FileClassLoader2(urls);
Class<?> class1 = loader1.findClass("cn.nuc.edu.LogTest.load.Demo");
System.out.println(class1.getClassLoader());
System.out.println(class1.hashCode());
FileClassLoader2 loader2 = new FileClassLoader2(urls);
Class<?> class2 = loader2.findClass("cn.nuc.edu.LogTest.load.Demo");
System.out.println(class2.getClassLoader());
System.out.println(class2.hashCode());
}
}
//輸出
cn.nuc.edu.LogTest.load.FileClassLoader2@5b202f4d
2053014315
cn.nuc.edu.LogTest.load.FileClassLoader2@52f5bad0
2054262321
說明:無論使用findClass和loadClass都可以通過多個classLoader示例加載多次同一個類。
3、Tomcat類加載器:
3.1)思考一個問題
我們思考一下:Tomcat是個web容器, 那麼它要解決什麼問題:
- 一個web容器可能需要部署兩個應用程式,不同的應用程式可能會依賴同一個第三方類庫的不同版本,不能要求同一個類庫在同一個伺服器隻有一份,是以要保證每個應用程式的類庫都是獨立的,保證互相隔離。
- 部署在同一個web容器中相同的類庫相同的版本可以共享。否則,如果伺服器有10個應用程式,那麼要有10份相同的類庫加載進虛拟機。
- web容器也有自己依賴的類庫,不能于應用程式的類庫混淆。基于安全考慮,應該讓容器的類庫和程式的類庫隔離開來。
- web容器要支援jsp的修改,我們知道,jsp 檔案最終也是要編譯成class檔案才能在虛拟機中運作,但程式運作後修改jsp已經是司空見慣的事情,否則要你何用? 是以,web容器需要支援 jsp 修改後不用重新開機。
Tomcat 如果使用預設的類加載機制行不行?
答案是不行的。為什麼?我們看,第一個問題,如果使用預設的類加載器機制,那麼是無法加載兩個相同類庫的不同版本的,預設的累加器是不管你是什麼版本的,隻在乎你的全限定類名,并且隻有一份。第二個問題,預設的類加載器是能夠實作的,因為他的職責就是保證唯一性。第三個問題和第一個問題一樣。我們再看第四個問題,我們想我們要怎麼實作jsp檔案的熱修改(樓主起的名字),jsp 檔案其實也就是class檔案,那麼如果修改了,但類名還是一樣,類加載器會直接取方法區中已經存在的,修改後的jsp是不會重新加載的。那麼怎麼辦呢?我們可以直接解除安裝掉這jsp檔案的類加載器,是以你應該想到了,每個jsp檔案對應一個唯一的類加載器,當一個jsp檔案修改了,就直接解除安裝這個jsp類加載器。重新建立類加載器,重新加載jsp檔案。
3.2)Tomcat如何實作自己獨特的類加載機制?
我們看到,前面3個類加載和預設的一緻,CommonClassLoader、CatalinaClassLoader、SharedClassLoader和WebappClassLoader則是Tomcat自己定義的類加載器。它們分别加載/common/*、/server/*、/shared/*(在tomcat 6之後已經合并到根目錄下的lib目錄下)和/WebApp/WEB-INF/*中的Java類庫。其中WebApp類加載器和Jsp類加載器通常會存在多個執行個體,每一個Web應用程式對應一個WebApp類加載器,每一個JSP檔案對應一個Jsp類加載器。
- CommonClassLoader能加載的類都可以被Catalina ClassLoader和SharedClassLoader使用,進而實作了公有類庫的共用,而CatalinaClassLoader和Shared ClassLoader自己能加載的類則與對方互相隔離。
- WebAppClassLoader可以使用SharedClassLoader加載到的類,但各個WebAppClassLoader執行個體之間互相隔離。
- JasperLoader的加載範圍僅僅是這個JSP檔案所編譯出來的那一個.Class檔案,它出現的目的就是為了被丢棄:當Web容器檢測到JSP檔案被修改時,會替換掉目前的JasperLoader的執行個體,并通過再建立一個新的Jsp類加載器來實作JSP檔案的HotSwap功能。
3.3)擴充:
如果tomcat 的 Common ClassLoader 想加載 WebApp ClassLoader 中的類,該怎麼辦?