我們?cè)趯W(xué)習(xí) java 基礎(chǔ)的時(shí)候,從宏觀上了解了一個(gè)類到運(yùn)行大致是:.java 文件通過 javac 編譯器編譯得到 .class 文件,在用到該類時(shí),jvm 會(huì)加載該 class 文件,并創(chuàng)建對(duì)應(yīng)的 class 對(duì)象,將 class 文件加載到 jvm 的內(nèi)存當(dāng)中,這個(gè)過程也被稱之為類加載過程。
下面我們將詳細(xì)了解這個(gè)過程,本篇過長(zhǎng)建議先收藏。
1、類加載過程
其實(shí)關(guān)于類加載過程是分為5個(gè)階段的:
加載,驗(yàn)證,準(zhǔn)備,解析,初始化

接下來我們看一下這五個(gè)階段:
1.1 加載
JVM 在該階段的主要目的是將字節(jié)碼從不同的數(shù)據(jù)源(可能是 class 文件、也可能是 jar 包,甚至網(wǎng)絡(luò))轉(zhuǎn)化為二進(jìn)制字節(jié)流加載到內(nèi)存中,并生成一個(gè)代表該類的 java.lang.Class 對(duì)象。
1.2 驗(yàn)證
這一階段的主要目的是為了確保 Class 文件的字節(jié)流中包含的信息是否符合當(dāng)前虛擬機(jī)的要求,并且不會(huì)危害虛擬機(jī)自身的安全,只有符合 JVM 字節(jié)碼規(guī)范的才能被 JVM 正確執(zhí)行。該階段是保證 JVM 安全的重要屏障,下面是一些主要的檢查。
確保二進(jìn)制字節(jié)流格式符合預(yù)期(比如說是否以 cafe bene 咖啡北鼻開頭)。
是否所有方法都遵守訪問控制關(guān)鍵字的限定。
方法調(diào)用的參數(shù)個(gè)數(shù)和類型是否正確。
確保變量在使用之前被正確初始化了。
檢查變量是否被賦予恰當(dāng)類型的值。
1.3 準(zhǔn)備
JVM 會(huì)在該階段對(duì)類變量(也稱為靜態(tài)變量,static 關(guān)鍵字修飾的)分配內(nèi)存并初始化(對(duì)應(yīng)數(shù)據(jù)類型的默認(rèn)初始值,如 0、0L、null、false 等)。
準(zhǔn)備階段是正式為類變量分配內(nèi)存并設(shè)置類變量的初始值階段,即在方法區(qū)中分配這些變量所使用的內(nèi)存空間。注意這里所說的初始值概念,比如一個(gè)類變量定義為:
public static int value1 = 123;
實(shí)際上變量 value1 在準(zhǔn)備階段過后的初始值為 0 而不是 123(如果是String類型,初始值為null),將 value1 賦值為 123 的 putstatic 指令是程序被編譯后,存放于類構(gòu)造器方法之中。
但是注意如果聲明為:
public static final int value2 = 123;
在編譯階段會(huì)為 value2 生成 ConstantValue 屬性,在準(zhǔn)備階段虛擬機(jī)會(huì)根據(jù) ConstantValue 屬性將 value2 賦值為 123。
也就是,static final 修飾的變量被稱作為常量,和類變量不同。常量一旦賦值就不會(huì)改變了,所以 value2 在準(zhǔn)備階段的值為 123 而不是 0。
1.4 解析
該階段將常量池中的符號(hào)引用轉(zhuǎn)化為直接引用。
what?符號(hào)引用,直接引用?
符號(hào)引用以一組符號(hào)(任何形式的字面量,只要在使用時(shí)能夠無歧義的定位到目標(biāo)即可)來描述所引用的目標(biāo)。
在編譯時(shí),Java 類并不知道所引用的類的實(shí)際地址,因此只能使用符號(hào)引用來代替。比如 club.sscai.Test1 類引用了 club.sscai.Test2 類,編譯時(shí) Test1 類并不知道 Test2 類的實(shí)際內(nèi)存地址,因此只能使用符號(hào) club.sscai.Test2。
直接引用通過對(duì)符號(hào)引用進(jìn)行解析,找到引用的實(shí)際內(nèi)存地址。
1.5 初始化
該階段是類加載過程的最后一步。在準(zhǔn)備階段,類變量已經(jīng)被賦過默認(rèn)初始值,而在初始化階段,類變量將被賦值為代碼期望賦的值。換句話說,初始化階段是執(zhí)行類構(gòu)造器方法的過程。
上面這段話說得比較抽象,不好理解,我來舉個(gè)例子。
String niceyoo = new String("感謝關(guān)注");
上面這段代碼使用了 new 關(guān)鍵字來實(shí)例化一個(gè)字符串對(duì)象,那么這時(shí)候,就會(huì)調(diào)用 String 類的構(gòu)造方法對(duì) niceyoo 進(jìn)行實(shí)例化,怎么個(gè)實(shí)例化?就是賦值唄。
本節(jié)點(diǎn)總結(jié)
其實(shí)看完類加載過程,由于大部分偏理論,乏味的同時(shí)又很難理解,也不容易記憶。所以將類加載過程結(jié)合面試題來進(jìn)一步擴(kuò)展,如下:
建議先思考后再看答案
題目一:如下代碼中,執(zhí)行 main 函數(shù)會(huì)通過編譯嗎?如果可以通過,打印結(jié)果是什么呢?
public class A {
public static void fun1(){
System.out.println("fun1");
}
public void fun2(){
System.out.println("fun2");
}
public static void main(String[] args){
((A) null).fun1();
((A) null).fun2();
}
}
答案: 首先代碼是可以通過編譯的,null 可以強(qiáng)制轉(zhuǎn)為任意類型,調(diào)用其類中的靜態(tài)方法 fun1 不報(bào)異常,調(diào)用其類中的非靜態(tài)方法 fun2 會(huì)報(bào)空指針異常。

分析: 編譯是否正常通過最大的干擾項(xiàng)應(yīng)該是 null 強(qiáng)轉(zhuǎn)吧,估計(jì)有的小伙伴都不一定見過,<b>null 可以被強(qiáng)制類型轉(zhuǎn)換成任意類型的對(duì)象</b>,知識(shí)點(diǎn),下次要考。
關(guān)于打印結(jié)果則主要是類加載過程的考察:當(dāng)加載類對(duì)象時(shí),首先初始化靜態(tài)屬性,然后靜態(tài)代碼塊;當(dāng)實(shí)例化對(duì)象時(shí),首先執(zhí)行構(gòu)造塊(直接寫在類中的代碼塊{ xxx }),然后執(zhí)行構(gòu)造方法。至于各靜態(tài)塊和靜態(tài)屬性初始化哪個(gè)些執(zhí)行,是按代碼的先后順序。屬性、構(gòu)造塊、構(gòu)造方法之間的執(zhí)行順序(但構(gòu)造塊一定會(huì)在構(gòu)造方法前執(zhí)行),也是按代碼的先后順序。
綜上,對(duì)象即便被將轉(zhuǎn)為空時(shí),靜態(tài)方法也是可以被調(diào)用的,這也是我們平時(shí)在使用一些工具類時(shí),直接通過對(duì)象.來訪問其方法的原因。
題目二:請(qǐng)指出下面程序的運(yùn)行結(jié)果。
class A {
static {
System.out.print("1");
}
public A() {
System.out.print("2");
}
}
class B extends A {
static {
System.out.print("a");
}
public B() {
System.out.print("b");
}
}
public class Hello {
public static void main(String[] args) {
A ab = new B();
ab = new B();
}
}
分析: 通過上一題目的分析中,我們可能機(jī)智的得到了靜態(tài)代碼塊是優(yōu)于構(gòu)造方法的執(zhí)行的,但是這個(gè)題目中出現(xiàn)了A\B類的繼承關(guān)系,所以可能帶來困擾,但是沒關(guān)系,靜態(tài)代碼塊就是優(yōu)于構(gòu)造方法的,只是父類優(yōu)先級(jí)相對(duì)高一級(jí)罷了,比如 new B() 會(huì)先調(diào)用父類 A 的靜態(tài)代碼塊,其次是 B 的靜態(tài)代碼塊,然后是 A 的構(gòu)造方法,最后是 B 的構(gòu)造方法。
匯總: 執(zhí)行順序是先執(zhí)行父類的靜態(tài)代碼塊,然后執(zhí)行子類的靜態(tài)代碼塊;然后執(zhí)行父類的非靜態(tài)代碼塊,再執(zhí)行父類的構(gòu)造方法;之后再執(zhí)行子類的非靜態(tài)代碼塊,再執(zhí)行子類的構(gòu)造方法。靜態(tài)代碼塊>非靜態(tài)代碼塊>構(gòu)造方法。
再就是對(duì)象的創(chuàng)建只會(huì)調(diào)用一次靜態(tài)代碼塊,因?yàn)轭惓跏蓟畔⑹谴嬖诜椒▍^(qū)里,當(dāng)加載類的時(shí)候去檢查,第二次的時(shí)候它會(huì)發(fā)現(xiàn)已經(jīng)初始化過了,就不會(huì)再執(zhí)行,所以再去 new B() 的時(shí)候,是不會(huì)再去打印 1a 的。
如果覺得比較繞,再舉個(gè)例子,就好比你玩王者榮耀的時(shí)候,有個(gè)趙云的6元首充禮包,你第一次充錢,創(chuàng)建了這個(gè)首充禮包的對(duì)象,當(dāng)你第二次充錢時(shí)就不會(huì)再有首充禮包了。
答案: 1a2b2b
2、類加載器
聊完類加載過程的五個(gè)階段,我們?cè)賮砜纯醇虞d階段用到的類加載器。
系統(tǒng)運(yùn)行時(shí),是由類加載器將 .class 文件的二進(jìn)制數(shù)據(jù)從外部存儲(chǔ)器(如光盤,硬盤)調(diào)入內(nèi)存中,CPU再從內(nèi)存中讀取指令和數(shù)據(jù)進(jìn)行運(yùn)算,并將運(yùn)算結(jié)果存入內(nèi)存中的,顯然類加載器是很重要的第一步。

一般來說,Java 程序員并不需要直接同類加載器進(jìn)行交互。JVM 默認(rèn)的行為就已經(jīng)足夠滿足大多數(shù)情況的需求了。不過,如果遇到了需要和類加載器進(jìn)行交互的情況,而對(duì)類加載器的機(jī)制又不是很了解的話,就不得不花大量的時(shí)間去調(diào)試 ClassNotFoundException 和 NoClassDefFoundError 等異常。
對(duì)于任意一個(gè)類,都需要由它的類加載器和這個(gè)類本身一同確定其在 JVM 中的唯一性。也就是說,如果兩個(gè)類的加載器不同,即使兩個(gè)類來源于同一個(gè)字節(jié)碼文件,那這兩個(gè)類就必定不相等(比如兩個(gè)類的 Class 對(duì)象不 equals)。
Java 類加載器可以分為三種:
1)啟動(dòng)類加載器(Bootstrap Class-Loader),加載 jre/lib 包下面的 jar 文件,比如說常見的 rt.jar。
啟動(dòng)類加載器主要加載的是JVM自身需要的類,這個(gè)類加載使用 C++ 語言實(shí)現(xiàn)的,是虛擬機(jī)自身的一部分,它負(fù)責(zé)將 <JAVA_HOME>/lib路徑下的核心類庫或-Xbootclasspath參數(shù)指定的路徑下的jar包加載到內(nèi)存中,注意必由于虛擬機(jī)是按照文件名識(shí)別加載jar包的,如rt.jar,如果文件名不被虛擬機(jī)識(shí)別,即使把jar包丟到lib目錄下也是沒有作用的(出于安全考慮,Bootstrap啟動(dòng)類加載器只加載包名為java、javax、sun等開頭的類)。
2)擴(kuò)展類加載器(Extension or Ext Class-Loader),加載 jre/lib/ext 包下面的 jar 文件。
擴(kuò)展類加載器是指Sun公司(已被Oracle收購)實(shí)現(xiàn)的sun.misc.Launcher$ExtClassLoader類,由Java語言實(shí)現(xiàn)的,是Launcher的靜態(tài)內(nèi)部類,它負(fù)責(zé)加載<JAVA_HOME>/lib/ext目錄下或者由系統(tǒng)變量-Djava.ext.dir指定位路徑中的類庫,開發(fā)者可以直接使用標(biāo)準(zhǔn)擴(kuò)展類加載器。
3)應(yīng)用類加載器(Application or App Clas-Loader),根據(jù)程序的類路徑(classpath)來加載 Java 類。
也稱應(yīng)用程序加載器是指 Sun公司實(shí)現(xiàn)的sun.misc.Launcher$AppClassLoader。它負(fù)責(zé)加載系統(tǒng)類路徑j(luò)ava -classpath或-D java.class.path 指定路徑下的類庫,也就是我們經(jīng)常用到的classpath路徑,開發(fā)者可以直接使用系統(tǒng)類加載器,一般情況下該類加載是程序中默認(rèn)的類加載器,通過ClassLoader#getSystemClassLoader()方法可以獲取到該類加載器。
來來來,通過一段簡(jiǎn)單的代碼了解下。
public class Test1 {
public static void main(String[] args){
ClassLoader currentLoader = Test.class.getClassLoader();
System.out.println(currentLoader.toString());
ClassLoader parentLoader = currentLoader.getParent();
System.out.println(parentLoader.toString());
ClassLoader parentParentLoader = parentLoader.getParent();
System.out.println(parentParentLoader);
}
}
每個(gè) Java 類都維護(hù)著一個(gè)指向定義它的類加載器的引用,通過 類名.class.getClassLoader() 可以獲取到此引用;然后通過 .getParent() 可以獲取類加載器的上層類加載器。
這段代碼的輸出結(jié)果如下:
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@4554617c
null
第一行輸出為 Test 的類加載器,即應(yīng)用類加載器,它是 sun.misc.Launcher$AppClassLoader 類的實(shí)例;第二行輸出為擴(kuò)展類加載器,是 sun.misc.Launcher$ExtClassLoader 類的實(shí)例。那啟動(dòng)類加載器呢?按理說,擴(kuò)展類加載器的上層類加載器是啟動(dòng)類加載器,但在我這個(gè)版本的 JDK 中, 擴(kuò)展類加載器的 getParent() 返回 null。
在 Java 的日常應(yīng)用程序開發(fā)中,類的加載幾乎是由上述3種類加載器相互配合執(zhí)行的,在必要時(shí),我們還可以自定義類加載器,需要注意的是,Java 虛擬機(jī)對(duì) class 文件采用的是按需加載的方式,也就是說當(dāng)需要使用該類時(shí)才會(huì)將它的 class 文件加載到內(nèi)存生成 class 對(duì)象,而且加載某個(gè)類的 class 文件時(shí), Java 虛擬機(jī)采用的是雙親委派模式即把請(qǐng)求交由父類處理,它一種任務(wù)委派模式,下面我們進(jìn)一步了解它。
別放棄,加油!
3、雙親委派模型

雙親委派模式是在 Java 1.2 后引入的,其工作原理的是,如果一個(gè)類加載器收到了類加載請(qǐng)求,它并不會(huì)自己先去加載,而是把這個(gè)請(qǐng)求委托給父類的加載器去執(zhí)行,如果父類加載器還存在其父類加載器,則進(jìn)一步向上委托,依次遞歸,請(qǐng)求最終將到達(dá)頂層的啟動(dòng)類加載器,如果父類加載器可以完成類加載任務(wù),就成功返回,倘若父類加載器無法完成此加載任務(wù),子加載器才會(huì)嘗試自己去加載,這就是雙親委派模式,即每個(gè)兒子都很懶,每次有活就丟給父親去干,直到父親說這件事我也干不了時(shí),兒子自己想辦法去完成,這不就是傳說中的實(shí)力坑爹啊?那么采用這種模式有啥用呢?
雙親委派模式優(yōu)勢(shì)
采用雙親委派模式的是好處是 Java 類隨著它的類加載器一起具備了一種帶有優(yōu)先級(jí)的層次關(guān)系,通過這種層級(jí)關(guān)可以避免類的重復(fù)加載,當(dāng)父親已經(jīng)加載了該類時(shí),就沒有必要子 ClassLoader 再加載一次。
其次是考慮到安全因素,java 核心 api 中定義類型不會(huì)被隨意替換,假設(shè)通過網(wǎng)絡(luò)傳遞一個(gè)名為 java.lang.Integer 的類,通過雙親委托模式傳遞到啟動(dòng)類加載器,而啟動(dòng)類加載器在核心 Java API 發(fā)現(xiàn)這個(gè)名字的類,發(fā)現(xiàn)該類已被加載,并不會(huì)重新加載網(wǎng)絡(luò)傳遞的過來的 java.lang.Integer,而直接返回已加載過的 Integer.class,這樣便可以防止核心API庫被隨意篡改。
可能你會(huì)想,如果我們?cè)?classpath 路徑下自定義一個(gè)名為 java.lang.SingleInterge 類(該類是胡編的)呢?該類并不存在 java.lang 中,經(jīng)過雙親委托模式,傳遞到啟動(dòng)類加載器中,由于父類加載器路徑下并沒有該類,所以不會(huì)加載,將反向委托給子類加載器加載,最終會(huì)通過系統(tǒng)類加載器加載該類。但是這樣做是不允許,因?yàn)?java.lang 是核心 API 包,需要訪問權(quán)限,強(qiáng)制加載將會(huì)報(bào)出如下異常:
java.lang.SecurityException: Prohibited package name: java.lang
文字內(nèi)容太乏味,上個(gè)例子吧,我們通過自定義類加載器去證實(shí)雙親委派模式。
先簡(jiǎn)單了解一下這個(gè)類加載器的主要方法:
loadClass:該方法中的邏輯就是雙親委派模式的實(shí)現(xiàn),當(dāng)類加載請(qǐng)求到來時(shí),先從緩存中查找該類對(duì)象,如果存在直接返回,如果不存在則交給該類加載去的父加載器去加載,倘若沒有父加載則交給頂級(jí)啟動(dòng)類加載器去加載,最后倘若仍沒有找到,則使用findClass()方法去加載。
findClass:findClass()方法是在loadClass()方法中被調(diào)用的,當(dāng)loadClass()方法中父加載器加載失敗后,則會(huì)調(diào)用自己的findClass()方法來完成類加載,這樣就可以保證自定義的類加載器也符合雙親委托模式。
defineClass:通過這個(gè)方法不僅能夠通過class文件實(shí)例化class對(duì)象,也可以通過其他方式實(shí)例化class對(duì)象,defineClass()方法通常與findClass()方法一起使用,一般情況下,在自定義類加載器時(shí),會(huì)直接覆蓋ClassLoader的findClass()方法并編寫加載規(guī)則,取得要加載類的字節(jié)碼后轉(zhuǎn)換成流,然后調(diào)用defineClass()方法生成類的Class對(duì)象。
resolveClass:使用該方法可以使用類的Class對(duì)象創(chuàng)建完成也同時(shí)被解析。前面我們說鏈接階段主要是對(duì)字節(jié)碼進(jìn)行驗(yàn)證,為類變量分配內(nèi)存并設(shè)置初始值同時(shí)將字節(jié)碼文件中的符號(hào)引用轉(zhuǎn)換為直接引用。
自定義類加載器 MyClassLoader :
package club.sscai.test7;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* author: niceyoo
* blog: https://cnblogs.com/niceyoo
* desc: 自定義類加載器
*/
public class MyClassLoader extends ClassLoader {
private String path;/* 加載器的路徑 */
private String name;/* 類加載器名稱 */
public MyClassLoader(String path,String name){
super();/* 讓起同類加載器成為該類的父加載器 */
this.name = name;
this.path = path;
}
/**
* 父類加載器構(gòu)造方法
* @param parent
* @param path
* @param name
*/
public MyClassLoader(ClassLoader parent,String path,String name){
super(parent);/* 顯示指定父類加載器 */
this.name = name;
this.path = path;
}
/**
* 加載我們自己定義的類,通過我們自定義的這個(gè) ClassLoader
* 例如:club.sscai.test7.Demo
* @param name
* @return
* @throws ClassNotFoundException
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
/* 讀取 class 文件,轉(zhuǎn)換成二進(jìn)制數(shù)組 */
byte[] data = readClassFileToByteArray(name);
return this.defineClass(name,data,0,data.length);
}
@Override
public String toString() {
return this.name;
}
/**
* 獲取 .class 字節(jié)數(shù)組
* 【讀取 class 文件,將類轉(zhuǎn)換成二進(jìn)制數(shù)組】
* club.sscai.test7.Demo >
* F:/idea_workspace/test/Demo.class
* @param name
* @return
*/
private byte[] readClassFileToByteArray(String name) {
InputStream is = null;
byte[] returnData = null;
name = name.replace("\\.","/");
String filePath = this.path + name + ".class";
File file = new File(filePath);
ByteArrayOutputStream os = new ByteArrayOutputStream();
try {
is = new FileInputStream(file);
int tep = 0;
while ((tep = is.read()) != -1){
os.write(tep);
}
returnData = os.toByteArray();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
is.close();
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return returnData;
}
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
return super.loadClass(name, resolve);
}
}
當(dāng)前項(xiàng)目位于 F:\idea_workspace\Demo

當(dāng)前項(xiàng)目中的干擾項(xiàng) Demo.java:
package club.sscai.test7;
/**
* author: niceyoo
* blog: https://cnblogs.com/niceyoo
* desc: 干擾項(xiàng)Demo
*/
public class Demo {
public Demo() {
System.out.println("我是父加載器加載的Demo:"+Demo.class.getClassLoader());
}
}
存放在 F:/idea_workspace/test/ 目錄下的 Demo.java,注意如下代碼需要通過 javac 編譯成 Demo.class

public class Demo {
public Demo(){
System.out.println("Demo:" + this.getClass().getClassLoader());
}
}
測(cè)試代碼 TestDemo.java 如下:
public class TestDemo {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
/* 參數(shù)一為讀取class路徑,參數(shù)二為自定義類加載器名稱 */
MyClassLoader xwLoader = new MyClassLoader("F:/idea_workspace/test/","xiaowang");
Class<?> demo = xwLoader.loadClass("Demo");
demo.newInstance();
}
}
執(zhí)行 main 方法后會(huì)打印什么呢?
Demo:xiaowang
我們改一下測(cè)試代碼:
public class TestDemo {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
MyClassLoader xwLoader = new MyClassLoader("F:/idea_workspace/test/","xiaowang");
Class<?> demo = xwLoader.loadClass("club.sscai.test7.Demo");
demo.newInstance();
}
}
再次執(zhí)行會(huì)打印什么呢?
我是父加載器加載的Demo:sun.misc.Launcher$AppClassLoader@18b4aac2
顯然第二次并沒有加載 F:/idea_workspace/test/ 目錄下的 Demo,而是執(zhí)行了當(dāng)前項(xiàng)目中的 Demo,為什么?
這就是雙親委派模式,由于當(dāng)前啟動(dòng)類 TestDemo 的父級(jí)是 AppClassLoader,顯然該包下已經(jīng)加載過 Demo 類了,所以不會(huì)再去加載目標(biāo) Demo
4、熱部署與熱加載(擴(kuò)展)
上邊算是說了一堆理論吧,熱部署、熱加載則算是實(shí)際應(yīng)用了,相信這兩者應(yīng)該并不陌生,或多或少的應(yīng)該也有所了解吧。
熱加載的實(shí)現(xiàn)原理主要依賴java的類加載機(jī)制,在實(shí)現(xiàn)方式可以概括為在容器啟動(dòng)的時(shí)候起一條后臺(tái)線程,定時(shí)的檢測(cè)類文件的時(shí)間戳變化,如果類的時(shí)間戳變掉了,則將類重新載入。
熱部署原理類似,但它是直接重新加載整個(gè)應(yīng)用,這種方式會(huì)釋放內(nèi)存,比熱加載更加干凈徹底,但同時(shí)也更費(fèi)時(shí)間。
簡(jiǎn)單總結(jié)一下兩者的區(qū)別與聯(lián)系:
Java熱部署與熱加載的聯(lián)系
- 不重啟服務(wù)器編譯/部署項(xiàng)目
- 基于Java的類加載器實(shí)現(xiàn)
Java熱部署與熱加載的區(qū)別:
- 部署方式
-- 熱部署在服務(wù)器運(yùn)行時(shí)重新部署項(xiàng)目
-- 熱加載在運(yùn)行時(shí)重新加載class - 實(shí)現(xiàn)原理
-- 熱部署直接重新加載整個(gè)應(yīng)用
-- 熱加載在運(yùn)行時(shí)重新加載class - 使用場(chǎng)景
-- 熱部署更多的是在生產(chǎn)環(huán)境使用
-- 熱加載則更多的實(shí)在開發(fā)環(huán)境使用
想要實(shí)現(xiàn)熱部署可以分以下三個(gè)步驟:
- 銷毀該自定義ClassLoader
- 更新class類文件
- 創(chuàng)建新的ClassLoader去加載更新后的class類文件。
相關(guān)代碼:
User沒有被修改類:
public class User {
public void add() {
System.out.println("addV1,沒有修改過...");
}
}
User更新類
public class User {
public void add() {
System.out.println("我把之前的user add方法修改啦!");
}
}
自定義類加載器:
public class MyClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
/* 文件名稱 */
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
/* 獲取文件輸入流 */
InputStream is = this.getClass().getResourceAsStream(fileName);
/* 讀取字節(jié) */
byte[] b = new byte[is.available()];
is.read(b);
/* 將byte字節(jié)流解析成jvm能夠識(shí)別的Class對(duì)象 */
return defineClass(name, b, 0, b.length);
} catch (Exception e) {
throw new ClassNotFoundException();
}
}
}
更新代碼:
public class Hotswap {
public static void main(String[] args)
throws ClassNotFoundException, InstantiationException, IllegalAccessException,
NoSuchMethodException,
SecurityException, IllegalArgumentException, InvocationTargetException,
InterruptedException {
loadUser();
System.gc();
Thread.sleep(1000);/* 等待資源回收 */
/* 需要被熱部署的class文件 */
File file1 = new File("F:\\test\\User.class");
/* 之前編譯好的class文件 */
File file2 = new File(
"F:\\idea_workspace\\target\\classes\\club\\sscai\\User.class");
/* 刪除舊版本的class文件 */
boolean isDelete = file2.delete();
if (!isDelete) {
System.out.println("熱部署失敗.");
return;
}
file1.renameTo(file2);
System.out.println("update success!");
loadUser();
}
public static void loadUser() throws ClassNotFoundException, InstantiationException,
IllegalAccessException,
NoSuchMethodException, SecurityException, IllegalArgumentException,
InvocationTargetException {
MyClassLoader myLoader = new MyClassLoader();
Class<?> class1 = myLoader.findClass("club.sscai.User");
Object obj1 = class1.newInstance();
Method method = class1.getMethod("add");
method.invoke(obj1);
System.out.println(obj1.getClass());
System.out.println(obj1.getClass().getClassLoader());
}
}
5、最后
本篇有點(diǎn)過長(zhǎng)了,其實(shí)大致看下來,類加載無非也就那么回事。
類加載機(jī)制:JVM 將類的信息動(dòng)態(tài)添加到內(nèi)存并使用的一種機(jī)制。
18年??飘厴I(yè)后,期間一度迷茫,最近我創(chuàng)建了一個(gè)公眾號(hào)用來記錄自己的成長(zhǎng)。
