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/
<完>
個人網站:https://www.codingme.net
如果你喜歡這篇文章,可以關注公眾號,文章第一時間直達 。
關注公眾號回復資源可以沒有套路的獲取全網最火的的 Java 核心知識整理&面試資料。