1. 什麼是熱加載
熱加載是指可以在不重新開機服務的情況下讓更改的代碼生效,熱加載可以顯著的提升開發以及調試的效率,它是基于 Java 的類加載器實作的,但是由于熱加載的不安全性,一般不會用于正式的生産環境。
2. 熱加載與熱部署的差別
首先,不管是熱加載還是熱部署,都可以在不重新開機服務的情況下編譯/部署項目,都是基于 Java 的類加載器實作的。
那麼兩者到底有什麼差別呢?
在部署方式上:
- 熱部署是在伺服器運作時重新部署項目。
- 熱加載是在運作時重新加載 class。
在實作原理上:
- 熱部署是直接重新加載整個應用,耗時相對較高。
- 熱加載是在運作時重新加載 class,背景會啟動一個線程不斷檢測你的類是否改變。
在使用場景上:
- 熱部署更多的是在生産環境使用。
- 熱加載則更多的是在開發環境上使用。線上由于安全性問題不會使用,難以監控。
3. 類加載五個階段

可能你已經發現了,圖中一共是7個階段,而不是5個。是因為圖是類的完整生命周期,如果要說隻是類加載階段的話,圖裡最後的使用(Using)和解除安裝(Unloading)并不算在内。
簡單描述一下類加載的五個階段:
- 加載階段:找到類的靜态存儲結構,加載到虛拟機,定義資料結構。使用者可以自定義類加載器。
- 驗證階段:確定位元組碼是安全的,確定不會對虛拟機的安全造成危害。
- 準備階段:确定記憶體布局,确定記憶體周遊,賦初始值(注意:是初始值,也有特殊情況)。
- 解析階段: 将符号變成直接引用。
- 初始化階段:調用程式自定義的代碼。規定有且僅有5種情況必須進行初始化。
- new(執行個體化對象)、getstatic(擷取類變量的值,被final修飾的除外,他的值在編譯器時放到了常量池)、putstatic(給類變量指派)、invokestatic(調用靜态方法) 時會初始化
- 調用子類的時候,發現父類還沒有初始化,則父類需要立即初始化。
- 虛拟機啟動,使用者要執行的主類,主類需要立即初始化,如 main 方法。
- 使用 java.lang.reflect包的方法對類進行反射調用方法 是會初始化。
-
當使用JDK 1.7的動态語言支援時, 如果一個java.lang.invoke.MethodHandle執行個體最後
的解析結果REF_getStatic、 REF_putStatic、 REF_invokeStatic的方法句柄, 并且這個方法句柄
所對應的類沒有進行過初始化, 則需要先觸發其初始化。
要說明的是,類加載的 5 個階段中,隻有加載階段是使用者可以自定義處理的,而驗證階段、準備階段、解析階段、初始化階段都是用 JVM 來處理的。
4. 實作類的熱加載
4.1 實作思路
我們怎麼才能手動寫一個類的熱加載呢?根據上面的分析,Java 程式在運作的時候,首先會把 class 類檔案加載到 JVM 中,而類的加載過程又有五個階段,五個階段中隻有加載階段使用者可以進行自定義處理,是以我們如果能在程式代碼更改且重新編譯後,讓運作的程序可以實時擷取到新編譯後的 class 檔案,然後重新進行加載的話,那麼理論上就可以實作一個簡單的 Java 熱加載。
是以我們可以得出實作思路:
- 實作自己的類加載器。
- 從自己的類加載器中加載要熱加載的類。
- 不斷輪訓要熱加載的類 class 檔案是否有更新。
- 如果有更新,重新加載。
4.2 自定義類加載器
設計 Java 虛拟機的團隊把類的加載階段放到的 JVM 的外部實作( 通過一個類的全限定名來擷取描述此類的二進制位元組流 )。這樣就可以讓程式自己決定如果擷取到類資訊。而實作這個加載動作的代碼子產品,我們就稱之為 “類加載器”。
在 Java 中,類加載器也就是
java.lang.ClassLoader
. 是以如果我們想要自己實作一個類加載器,就需要繼承
ClassLoader
然後重寫裡面
findClass
的方法,同時因為類加載器是
雙親委派模型
實作(也就說。除了一個最頂層的類加載器之外,每個類加載器都要有父加載器,而加載時,會先詢問父加載器能否加載,如果父加載器不能加載,則會自己嘗試加載)是以我們還需要指定父加載器。
最後根據傳入的類路徑,加載類的代碼看下面。
package net.codingme.box.classloader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
/**
* <p>
* 自定義 Java類加載器來實作Java 類的熱加載
*
* @Author niujinpeng
* @Date 2019/10/24 23:22
*/
public class MyClasslLoader extends ClassLoader {
/** 要加載的 Java 類的 classpath 路徑 */
private String classpath;
public MyClasslLoader(String classpath) {
// 指定父加載器
super(ClassLoader.getSystemClassLoader());
this.classpath = classpath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] data = this.loadClassData(name);
return this.defineClass(name, data, 0, data.length);
}
/**
* 加載 class 檔案中的内容
*
* @param name
* @return
*/
private byte[] loadClassData(String name) {
try {
// 傳進來是帶包名的
name = name.replace(".", "//");
FileInputStream inputStream = new FileInputStream(new File(classpath + name + ".class"));
// 定義位元組數組輸出流
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int b = 0;
while ((b = inputStream.read()) != -1) {
baos.write(b);
}
inputStream.close();
return baos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
4.3 定義要類型熱加載的類
我們假設某個接口(BaseManager.java)下的某個方法(logic)要進行熱加載處理。
首先定義接口資訊。
package net.codingme.box.classloader;
/**
* <p>
* 實作這個接口的子類,需要動态更新。也就是熱加載
*
* @Author niujinpeng
* @Date 2019/10/24 23:29
*/
public interface BaseManager {
public void logic();
}
寫一個這個接口的實作類。
package net.codingme.box.classloader;
import java.time.LocalTime;
/**
* <p>
* BaseManager 這個接口的子類要實作類的熱加載功能。
*
* @Author niujinpeng
* @Date 2019/10/24 23:30
*/
public class MyManager implements BaseManager {
@Override
public void logic() {
System.out.println(LocalTime.now() + ": Java類的熱加載");
}
}
後面我們要做的就是讓這個類可以通過我們的 MyClassLoader 進行自定義加載。類的熱加載應當隻有在類的資訊被更改然後重新編譯之後進行重新加載。是以為了不意義的重複加載,我們需要判斷 class 是否進行了更新,是以我們需要記錄 class 類的修改時間,以及對應的類資訊。
是以編譯一個類用來記錄某個類對應的某個類加載器以及上次加載的 class 的修改時間。
package net.codingme.box.classloader;
/**
* <p>
* 封裝加載類的資訊
*
* @Author niujinpeng
* @Date 2019/10/24 23:32
*/
public class LoadInfo {
/** 自定義的類加載器 */
private MyClasslLoader myClasslLoader;
/** 記錄要加載的類的時間戳-->加載的時間 */
private long loadTime;
/** 需要被熱加載的類 */
private BaseManager manager;
public LoadInfo(MyClasslLoader myClasslLoader, long loadTime) {
this.myClasslLoader = myClasslLoader;
this.loadTime = loadTime;
}
public MyClasslLoader getMyClasslLoader() {
return myClasslLoader;
}
public void setMyClasslLoader(MyClasslLoader myClasslLoader) {
this.myClasslLoader = myClasslLoader;
}
public long getLoadTime() {
return loadTime;
}
public void setLoadTime(long loadTime) {
this.loadTime = loadTime;
}
public BaseManager getManager() {
return manager;
}
public void setManager(BaseManager manager) {
this.manager = manager;
}
}
4.4 熱加載擷取類資訊
在實作思路裡,我們知道輪訓檢查 class 檔案是不是被更新過,是以每次調用要熱加載的類時,我們都要進行檢查類是否被更新然後決定要不要重新加載。為了友善這步的擷取操作,可以使用一個簡單的工廠模式進行封裝。
要注意是加載 class 檔案需要指定完整的路徑,是以類中定義了 CLASS_PATH 常量。
package net.codingme.box.classloader;
import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;
/**
* <p>
* 加載 manager 的工廠
*
* @Author niujinpeng
* @Date 2019/10/24 23:38
*/
public class ManagerFactory {
/** 記錄熱加載類的加載資訊 */
private static final Map<String, LoadInfo> loadTimeMap = new HashMap<>();
/** 要加載的類的 classpath */
public static final String CLASS_PATH = "D:\\IdeaProjectMy\\lab-notes\\target\\classes\\";
/** 實作熱加載的類的全名稱(包名+類名 ) */
public static final String MY_MANAGER = "net.codingme.box.classloader.MyManager";
public static BaseManager getManager(String className) {
File loadFile = new File(CLASS_PATH + className.replaceAll("\\.", "/") + ".class");
// 擷取最後一次修改時間
long lastModified = loadFile.lastModified();
System.out.println("目前的類時間:" + lastModified);
// loadTimeMap 不包含 ClassName 為 key 的資訊,證明這個類沒有被加載,要加載到 JVM
if (loadTimeMap.get(className) == null) {
load(className, lastModified);
} // 加載類的時間戳變化了,我們同樣要重新加載這個類到 JVM。
else if (loadTimeMap.get(className).getLoadTime() != lastModified) {
load(className, lastModified);
}
return loadTimeMap.get(className).getManager();
}
/**
* 加載 class ,緩存到 loadTimeMap
*
* @param className
* @param lastModified
*/
private static void load(String className, long lastModified) {
MyClasslLoader myClasslLoader = new MyClasslLoader(className);
Class loadClass = null;
// 加載
try {
loadClass = myClasslLoader.loadClass(className);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
BaseManager manager = newInstance(loadClass);
LoadInfo loadInfo = new LoadInfo(myClasslLoader, lastModified);
loadInfo.setManager(manager);
loadTimeMap.put(className, loadInfo);
}
/**
* 以反射的方式建立 BaseManager 的子類對象
*
* @param loadClass
* @return
*/
private static BaseManager newInstance(Class loadClass) {
try {
return (BaseManager)loadClass.getConstructor(new Class[] {}).newInstance(new Object[] {});
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
return null;
}
}
4.5 熱加載測試
直接寫一個線程不斷的檢測要熱加載的類是不是已經更改需要重新加載,然後運作測試即可。
package net.codingme.box.classloader;
/**
* <p>
*
* 背景啟動一條線程,不斷檢測是否要重新整理重新加載,實作了熱加載的類
*
* @Author niujinpeng
* @Date 2019/10/24 23:53
*/
public class MsgHandle implements Runnable {
@Override
public void run() {
while (true) {
BaseManager manager = ManagerFactory.getManager(ManagerFactory.MY_MANAGER);
manager.logic();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
主線程:
package net.codingme.box.classloader;
public class ClassLoadTest {
public static void main(String[] args) {
new Thread(new MsgHandle()).start();
}
}
代碼已經全部準備好了,最後一步,可以啟動測試了。如果你是用的是 Eclipse ,直接啟動就行了;如果是 IDEA ,那麼你需要 DEBUG 模式啟動(IDEA 對熱加載有一定的限制)。
啟動後看到控制台不斷的輸出:
00:08:13.018: Java類的熱加載
00:08:15.018: Java類的熱加載
這時候我們随便更改下 MyManager 類的 logic 方法的輸出内容然後儲存。
@Override
public void logic() {
System.out.println(LocalTime.now() + ": Java類的熱加載 Oh~~~~");
}
可以看到控制台的輸出已經自動更改了(IDEA 在更改後需要按 CTRL + F9)。
代碼已經放到Github: https://github.com/niumoo/lab-notes/
<完>

java.lang.ClassLoader
ClassLoader
findClass
雙親委派模型
package net.codingme.box.classloader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
/**
* <p>
* 自定義 Java類加載器來實作Java 類的熱加載
*
* @Author niujinpeng
* @Date 2019/10/24 23:22
*/
public class MyClasslLoader extends ClassLoader {
/** 要加載的 Java 類的 classpath 路徑 */
private String classpath;
public MyClasslLoader(String classpath) {
// 指定父加載器
super(ClassLoader.getSystemClassLoader());
this.classpath = classpath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] data = this.loadClassData(name);
return this.defineClass(name, data, 0, data.length);
}
/**
* 加載 class 檔案中的内容
*
* @param name
* @return
*/
private byte[] loadClassData(String name) {
try {
// 傳進來是帶包名的
name = name.replace(".", "//");
FileInputStream inputStream = new FileInputStream(new File(classpath + name + ".class"));
// 定義位元組數組輸出流
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int b = 0;
while ((b = inputStream.read()) != -1) {
baos.write(b);
}
inputStream.close();
return baos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
package net.codingme.box.classloader;
/**
* <p>
* 實作這個接口的子類,需要動态更新。也就是熱加載
*
* @Author niujinpeng
* @Date 2019/10/24 23:29
*/
public interface BaseManager {
public void logic();
}
package net.codingme.box.classloader;
import java.time.LocalTime;
/**
* <p>
* BaseManager 這個接口的子類要實作類的熱加載功能。
*
* @Author niujinpeng
* @Date 2019/10/24 23:30
*/
public class MyManager implements BaseManager {
@Override
public void logic() {
System.out.println(LocalTime.now() + ": Java類的熱加載");
}
}
package net.codingme.box.classloader;
/**
* <p>
* 封裝加載類的資訊
*
* @Author niujinpeng
* @Date 2019/10/24 23:32
*/
public class LoadInfo {
/** 自定義的類加載器 */
private MyClasslLoader myClasslLoader;
/** 記錄要加載的類的時間戳-->加載的時間 */
private long loadTime;
/** 需要被熱加載的類 */
private BaseManager manager;
public LoadInfo(MyClasslLoader myClasslLoader, long loadTime) {
this.myClasslLoader = myClasslLoader;
this.loadTime = loadTime;
}
public MyClasslLoader getMyClasslLoader() {
return myClasslLoader;
}
public void setMyClasslLoader(MyClasslLoader myClasslLoader) {
this.myClasslLoader = myClasslLoader;
}
public long getLoadTime() {
return loadTime;
}
public void setLoadTime(long loadTime) {
this.loadTime = loadTime;
}
public BaseManager getManager() {
return manager;
}
public void setManager(BaseManager manager) {
this.manager = manager;
}
}
package net.codingme.box.classloader;
import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;
/**
* <p>
* 加載 manager 的工廠
*
* @Author niujinpeng
* @Date 2019/10/24 23:38
*/
public class ManagerFactory {
/** 記錄熱加載類的加載資訊 */
private static final Map<String, LoadInfo> loadTimeMap = new HashMap<>();
/** 要加載的類的 classpath */
public static final String CLASS_PATH = "D:\\IdeaProjectMy\\lab-notes\\target\\classes\\";
/** 實作熱加載的類的全名稱(包名+類名 ) */
public static final String MY_MANAGER = "net.codingme.box.classloader.MyManager";
public static BaseManager getManager(String className) {
File loadFile = new File(CLASS_PATH + className.replaceAll("\\.", "/") + ".class");
// 擷取最後一次修改時間
long lastModified = loadFile.lastModified();
System.out.println("目前的類時間:" + lastModified);
// loadTimeMap 不包含 ClassName 為 key 的資訊,證明這個類沒有被加載,要加載到 JVM
if (loadTimeMap.get(className) == null) {
load(className, lastModified);
} // 加載類的時間戳變化了,我們同樣要重新加載這個類到 JVM。
else if (loadTimeMap.get(className).getLoadTime() != lastModified) {
load(className, lastModified);
}
return loadTimeMap.get(className).getManager();
}
/**
* 加載 class ,緩存到 loadTimeMap
*
* @param className
* @param lastModified
*/
private static void load(String className, long lastModified) {
MyClasslLoader myClasslLoader = new MyClasslLoader(className);
Class loadClass = null;
// 加載
try {
loadClass = myClasslLoader.loadClass(className);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
BaseManager manager = newInstance(loadClass);
LoadInfo loadInfo = new LoadInfo(myClasslLoader, lastModified);
loadInfo.setManager(manager);
loadTimeMap.put(className, loadInfo);
}
/**
* 以反射的方式建立 BaseManager 的子類對象
*
* @param loadClass
* @return
*/
private static BaseManager newInstance(Class loadClass) {
try {
return (BaseManager)loadClass.getConstructor(new Class[] {}).newInstance(new Object[] {});
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
return null;
}
}
package net.codingme.box.classloader;
/**
* <p>
*
* 背景啟動一條線程,不斷檢測是否要重新整理重新加載,實作了熱加載的類
*
* @Author niujinpeng
* @Date 2019/10/24 23:53
*/
public class MsgHandle implements Runnable {
@Override
public void run() {
while (true) {
BaseManager manager = ManagerFactory.getManager(ManagerFactory.MY_MANAGER);
manager.logic();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
package net.codingme.box.classloader;
public class ClassLoadTest {
public static void main(String[] args) {
new Thread(new MsgHandle()).start();
}
}
00:08:13.018: Java類的熱加載
00:08:15.018: Java類的熱加載
@Override
public void logic() {
System.out.println(LocalTime.now() + ": Java類的熱加載 Oh~~~~");
}