1.問題 1、如何理解類文件結構佈局? 2、如何應用類載入器的工作原理進行將應用輾轉騰挪? 3、熱部署與熱替換有何區別,如何隔離類衝突? 4、JVM如何管理記憶體,有何記憶體淘汰機制? 5、JVM執行引擎的工作機制是什麼? 6、JVM調優應該遵循什麼原則,使用什麼工具? 7、JPDA架構是什麼,如何 ...
1.問題
-
1、如何理解類文件結構佈局?
-
2、如何應用類載入器的工作原理進行將應用輾轉騰挪?
-
3、熱部署與熱替換有何區別,如何隔離類衝突?
-
4、JVM如何管理記憶體,有何記憶體淘汰機制?
-
5、JVM執行引擎的工作機制是什麼?
-
6、JVM調優應該遵循什麼原則,使用什麼工具?
-
7、JPDA架構是什麼,如何應用代碼熱替換?
-
8、JVM位元組碼增強技術有哪些?
2.關鍵詞
類結構,類載入器,載入,鏈接,初始化,雙親委派,熱部署,隔離,堆,棧,方法區,計數器,記憶體回收,執行引擎,調優工具,JVMTI,JDWP,JDI,熱替換,位元組碼,ASM,CGLIB,DCEVM
3.全文概要
作為三大工業級別語言之一的JAVA如此受企業青睞有加,離不開她背後JVM的默默復出。只是由於JAVA過於成功以至於我們常常忘了JVM平臺上還運行著像Clojure/Groovy/Kotlin/Scala/JRuby/Jython這樣的語言。我們享受著JVM帶來跨平臺“一次編譯到處執行”台的便利和自動記憶體回收的安逸。本文從JVM的最小元素類的結構出發,介紹類載入器的工作原理和應用場景,思考類載入器存在的意義。進而描述JVM邏輯記憶體的分佈和管理方式,同時列舉常用的JVM調優工具和使用方法,最後介紹高級特性JDPA框架和位元組碼增強技術,實現熱替換。從微觀到巨集觀,從靜態到動態,從基礎到高階介紹JVM的知識體系。
4.類的裝載
1. 類的結構
我們知道不只JAVA文本文件,像Clojure/Groovy/Kotlin/Scala這些文本文件也同樣會經過JDK的編譯器編程成class文件。進入到JVM領域後,其實就跟JAVA沒什麼關係了,JVM只認得class文件,那麼我們需要先瞭解class這個黑箱裡面包含的是什麼東西。
JVM規範嚴格定義了CLASS文件的格式,有嚴格的數據結構,下麵我們可以觀察一個簡單CLASS文件包含的欄位和數據類型。
詳細的描述我們可以從JVM規範說明書裡面查閱類文件格式(https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html),類的整體佈局如下圖展示的。
在我的理解,我想把每個CLASS文件類別成一個一個的資料庫,裡面包含的常量池/類索引/屬性表集合就像資料庫的表,而且表之間也有關聯,常量池則存放著其他表所需要的所有字面量。瞭解完類的數據結構後,我們需要來觀察JVM是如何使用這些從硬碟上或者網路傳輸過來的CLASS文件。
2. 載入機制
類的入口
在我們探究JVM如何使用CLASS文件之前,我們快速回憶一下編寫好的C語言文件是如何執行的?我們從C的HelloWorld入手看看先。
#include <stdio.h> int main() { /* my first program in C */ printf("Hello, World! \n"); return 0; }
編輯完保存為hello.c文本文件,然後安裝gcc編譯器(GNU C/C++)
$ gcc hello.c $ ./a.out Hello, World!
這個過程就是gcc編譯器將hello.c文本文件編譯成機器指令集,然後讀取到記憶體直接在電腦的CPU運行。從操作系統層面看的話,就是一個進程的啟動到結束的生命周期。
下麵我們看JAVA是怎麼運行的。學習JAVA開發的第一件事就是先下載JDK安裝包,安裝完配置好環境變數,然後寫一個名字為helloWorld的類,然後編譯執行,我們來觀察一下發生了什麼事情?
先看源碼,有夠簡單了吧。
package com.zooncool.example.theory.jvm; public class HelloWorld { public static void main(String[] args) { System.out.println("my classLoader is " + HelloWorld.class.getClassLoader()); } }
編譯執行
$ javac src/main/java/com/zooncool/example/theory/jvm/HelloWorld.java $ java -cp src/main/java/ com.zooncool.example.theory.jvm.HelloWorld my classLoader is sun.misc.Launcher$AppClassLoader@2a139a55
對比C語言在命令行直接運行編譯後的a.out二進位文件,JAVA的則是在命令行執行java classFile,從命令的區別我們知道操作系統啟動的是java進程,而HelloWorld類只是命令行的入參,在操作系統來看java也就是一個普通的應用進程而已,而這個進程就是JVM的執行形態(JVM靜態就是硬碟里JDK包下的二進位文件集合)。
學習過JAVA的都知道入口方法是public static void main(String[] args),缺一不可,那我猜執行java命令時JVM對該入口方法做了唯一驗證,通過了才允許啟動JVM進程,下麵我們來看這個入口方法有啥特點。
-
去掉public限定
$ javac src/main/java/com/zooncool/example/theory/jvm/HelloWorld.java $ java -cp src/main/java/ com.zooncool.example.theory.jvm.HelloWorld 錯誤: 在類 com.zooncool.example.theory.jvm.HelloWorld 中找不到 main 方法, 請將 main 方法定義為: public static void main(String[] args) 否則 JavaFX 應用程式類必須擴展javafx.application.Application
說名入口方法需要被public修飾,當然JVM調用main方法是底層的JNI方法調用不受修飾符影響。
-
去掉static限定
$ javac src/main/java/com/zooncool/example/theory/jvm/HelloWorld.java $ java -cp src/main/java/ com.zooncool.example.theory.jvm.HelloWorld 錯誤: main 方法不是類 com.zooncool.example.theory.jvm.HelloWorld 中的static, 請將 main 方法定義為: public static void main(String[] args)
我們是從類對象調用而不是類創建的對象才調用,索引需要靜態修飾
-
返回類型改為int
$ javac src/main/java/com/zooncool/example/theory/jvm/HelloWorld.java $ java -cp src/main/java/ com.zooncool.example.theory.jvm.HelloWorld 錯誤: main 方法必須返回類 com.zooncool.example.theory.jvm.HelloWorld 中的空類型值, 請 將 main 方法定義為: public static void main(String[] args)
void返回類型讓JVM調用後無需關心調用者的使用情況,執行完就停止,簡化JVM的設計。
-
方法簽名改為main1
-
$ javac src/main/java/com/zooncool/example/theory/jvm/HelloWorld.java $ java -cp src/main/java/ com.zooncool.example.theory.jvm.HelloWorld 錯誤: 在類 com.zooncool.example.theory.jvm.HelloWorld 中找不到 main 方法, 請將 main 方法定義為: public static void main(String[] args) 否則 JavaFX 應用程式類必須擴展javafx.application.Application
這個我也不清楚,可能是約定俗成吧,畢竟C/C++也是用main方法的。
說了這麼多main方法的規則,其實我們關心的只有兩點:
-
HelloWorld類是如何被JVM使用的
-
HelloWorld類裡面的main方法是如何被執行的
關於JVM如何使用HelloWorld下文我們會詳細講到。
我們知道JVM是由C/C++語言實現的,那麼JVM跟CLASS打交道則需要JNI(Java Native Interface)這座橋梁,當我們在命令行執行java時,由C/C++實現的java應用通過JNI找到了HelloWorld裡面符合規範的main方法,然後開始調用。我們來看下java命令的源碼就知道了
類載入器
上一節我們留了一個核心的環節,就是JVM在執行類的入口之前,首先得找到類再然後再把類裝到JVM實例裡面,也即是JVM進程維護的記憶體區域內。我們當然知道是一個叫做類載入器的工具把類載入到JVM實例裡面,拋開細節從操作系統層面觀察,那麼就是JVM實例在運行過程中通過IO從硬碟或者網路讀取CLASS二進位文件,然後在JVM管轄的記憶體區域存放對應的文件。我們目前還不知道類載入器的實現,但是我們從功能上判斷無非就是讀取文件到記憶體,這個是很普通也很簡單的操作。
如果類載入器是C/C++實現的話,那麼大概就是如下代碼就可以實現
char *fgets( char *buf, int n, FILE *fp );
如果是JAVA實現,那麼也很簡單
InputStream f = new FileInputStream("theory/jvm/HelloWorld.class");
從操作系統層面看的話,如果只是載入,以上代碼就足以把類文件載入到JVM記憶體裡面了。但是結果就是亂糟糟的把一堆毫無秩序的類文件往記憶體裡面扔,沒有良好的管理也沒法用,所以需要我們需要設計一套規則來管理存放記憶體裡面的CLASS文件,我們稱為類載入的設計模式或者類載入機制,這個下文會重點解釋。
根據官網的定義A class loader is an object that is responsible for loading classes. 類載入器就是負責載入類的。我們知道啟動JVM的時候會把JRE預設的一些類載入到記憶體,這部分類使用的載入器是JVM預設內置的由C/C++實現的,比如我們上文載入的HelloWorld.class。但是內置的類載入器有明確的範圍限定,也就是只能載入指定路徑下的jar包(類文件的集合)。如果只是載入JRE的類,那可玩的花樣就少很多,JRE只是提供了底層所需的類,更多的業務需要我們從外部載入類來支持,所以我們需要指定新的規則,以方便我們載入外部路徑的類文件。
系統預設載入器
-
Bootstrap class loader
作用:啟動類載入器,載入JDK核心類
類載入器:C/C++實現
類載入路徑: /jre/lib
URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs(); /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/resources.jar ... /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/rt.jar
實現原理:本地方法由C++實現
-
Extensions class loader
作用:擴展類載入器,載入JAVA擴展類庫。
類載入器:JAVA實現
類載入路徑:/jre/lib/ext
System.out.println(System.getProperty("java.ext.dirs")); /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/ext:
實現原理:擴展類載入器ExtClassLoader本質上也是URLClassLoader
Launcher.java
//構造方法返回擴展類載入器 public Launcher() { //定義擴展類載入器 Launcher.ExtClassLoader var1; try { //1、獲取擴展類載入器 var1 = Launcher.ExtClassLoader.getExtClassLoader(); } catch (IOException var10) { throw new InternalError("Could not create extension class loader", var10); } ... } //擴展類載入器 static class ExtClassLoader extends URLClassLoader { private static volatile Launcher.ExtClassLoader instance; //2、獲取擴展類載入器實現 public static Launcher.ExtClassLoader getExtClassLoader() throws IOException { if (instance == null) { Class var0 = Launcher.ExtClassLoader.class; synchronized(Launcher.ExtClassLoader.class) { if (instance == null) { //3、構造擴展類載入器 instance = createExtClassLoader(); } } } return instance; } //4、構造擴展類載入器具體實現 private static Launcher.ExtClassLoader createExtClassLoader() throws IOException { try { return (Launcher.ExtClassLoader)AccessController.doPrivileged(new PrivilegedExceptionAction<Launcher.ExtClassLoader>() { public Launcher.ExtClassLoader run() throws IOException { //5、獲取擴展類載入器載入目標類的目錄 File[] var1 = Launcher.ExtClassLoader.getExtDirs(); int var2 = var1.length; for(int var3 = 0; var3 < var2; ++var3) { MetaIndex.registerDirectory(var1[var3]); } //7、構造擴展類載入器 return new Launcher.ExtClassLoader(var1); } }); } catch (PrivilegedActionException var1) { throw (IOException)var1.getException(); } } //6、擴展類載入器目錄路徑 private static File[] getExtDirs() { String var0 = System.getProperty("java.ext.dirs"); File[] var1; if (var0 != null) { StringTokenizer var2 = new StringTokenizer(var0, File.pathSeparator); int var3 = var2.countTokens(); var1 = new File[var3]; for(int var4 = 0; var4 < var3; ++var4) { var1[var4] = new File(var2.nextToken()); } } else { var1 = new File[0]; } return var1; } //8、擴展類載入器構造方法 public ExtClassLoader(File[] var1) throws IOException { super(getExtURLs(var1), (ClassLoader)null, Launcher.factory); SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this); } }
-
System class loader
作用:系統類載入器,載入應用指定環境變數路徑下的類
類載入器:sun.misc.Launcher$AppClassLoader
類載入路徑:-classpath下麵的所有類
實現原理:系統類載入器AppClassLoader本質上也是URLClassLoader
Launcher.java
//構造方法返回系統類載入器 public Launcher() { try { //獲取系統類載入器 this.loader = Launcher.AppClassLoader.getAppClassLoader(var1); } catch (IOException var9) { throw new InternalError("Could not create application class loader", var9); } } static class AppClassLoader extends URLClassLoader { final URLClassPath ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this); //系統類載入器實現邏輯 public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException { //類比擴展類載入器,相似的邏輯 final String var1 = System.getProperty("java.class.path"); final File[] var2 = var1 == null ? new File[0] : Launcher.getClassPath(var1); return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction<Launcher.AppClassLoader>() { public Launcher.AppClassLoader run() { URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2); return new Launcher.AppClassLoader(var1x, var0); } }); } //系統類載入器構造方法 AppClassLoader(URL[] var1, ClassLoader var2) { super(var1, var2, Launcher.factory); this.ucp.initLookupCache(this); } }
通過上文運行HelloWorld我們知道JVM系統預設載入的類大改是1560個,如下圖
自定義類載入器
內置類載入器只載入了最少需要的核心JAVA基礎類和環境變數下的類,但是我們應用往往需要依賴第三方中間件來完成額外的業務,那麼如何把它們的類載入進來就顯得格外重要了。幸好JVM提供了自定義類載入器,可以很方便的完成自定義操作,最終目的也是把外部的類文件載入到JVM記憶體。通過繼承ClassLoader類並且覆寫findClass和loadClass方法就可以達到自定義獲取CLASS文件的目的。
首先我們看ClassLoader的核心方法loadClass
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded,看緩存有沒有沒有才去找 Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { //先看是不是最頂層,如果不是則parent為空,然後獲取父類 if (parent != null) { c = parent.loadClass(name, false); } else { //如果為空則說明應用啟動類載入器,讓它去載入 c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { // If still not found, then invoke findClass in order //如果還是沒有就調用自己的方法,確保調用自己方法前都使用了父類方法,如此遞歸三次到頂 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; } } protected Class<?> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); }
通過覆寫loadClass方法,我們甚至可以讀取一份加了密的文件,然後在記憶體裡面解密,這樣別人反編譯你的源碼也沒用,因為class是經過加密的,也就是理論上我們通過自定義類載入器可以做到為所欲為,但是有個重要的原則下文介紹類載入器設計模式會提到。
一下給出一個自定義類載入器極簡的案例,來說明自定義類載入器的實現。
執行結果如下,我們可以看到載入到記憶體方法區的兩個類的包名+名稱是一樣的,而對應的類載入器卻不一樣,而且輸出被載入類的值也是不一樣的。
----------------class name----------------- com.zooncool.example.theory.jvm.ClassIsolationPrinciple2$Demo com.zooncool.example.theory.jvm.ClassIsolationPrinciple2$Demo -----------------classLoader name----------------- sun.misc.Launcher$AppClassLoader@18b4aac2 com.zooncool.example.theory.jvm.ClassIsolationPrinciple2$MyClassLoader@511d50c0 -----------------field value----------------- 1 0
設計模式
現有的載入器分為內置類載入器和自定義載入器,不管它們是通過C或者JAVA實現的最終都是為了把外部的CLASS文件載入到JVM記憶體裡面。那麼我們就需要設計一套規則來管理組織記憶體裡面的CLASS文件,下麵我們就來介紹下通過這套規則如何來協調好內置類載入器和自定義類載入器之間的權責。
我們知道通過自定義類載入器可以乾出很多黑科技,但是有個基本的雷區就是,不能隨便替代JAVA的核心基礎類,或者說即是你寫了一個跟核心類一模一樣的類,JVM也不會使用。你想一下,如果為所欲為的你可以把最基礎本的java.lang.Object都換成你自己定義的同名類,然後搞個後門進去,而且JVM還使用的話,那誰還敢用JAVA了是吧,所以我們會介紹一個重要的原則,在此之前我們先介紹一下內置類載入器和自定義類載入器是如何協同的。
-
雙親委派機制
定義:某個特定的類載入器在接到載入類的請求時,首先將載入任務委托給父類載入器,依次遞歸,如果父類載入器可以完成類載入任務,就成功返回;只有父類載入器無法完成此載入任務時,才自己去載入。
實現:參考上文loadClass方法的源碼和註釋,通過最多三次遞歸可以到啟動類載入器,如果還是找不到這調用自定義方法。
雙親委派機制很好理解,目的就是為了不重覆載入已有的類,提高效率,還有就是強制從父類載入器開始逐級搜索類文件,確保核心基礎類優先載入。下麵介紹的是破壞雙親委派機制,瞭解為什麼要破壞這種看似穩固的雙親委派機制。
-
破壞委派機制
定義:打破類載入自上而上委托的約束。
實現:1、繼承ClassLoader並且重寫loadClass方法體,覆蓋依賴上層類載入器的邏輯;
2、”啟動類載入器”可以指定“線程上下文類載入器”為任意類載入器,即是“父類載入器”委托“子類載入器”去載入不屬於它載入範圍的類文件;
說明:雙親委派機制的好處上面我們已經提過了,但是由於一些歷史原因(JDK1.2加上雙親委派機制前的JDK1.1就已經存在,為了向前相容不得不開這個後門讓1.2版本的類載入器擁有1.1隨意載入的功能)。還有就是JNDI的服務調用機制,例如調用JDBC需要從外部載入相關類到JVM實例的記憶體空間。
介紹完內置類載入器和自定義類載入器的協同關係後,我們要重點強調上文提到的重要原則。
-
唯一標識
定義:JVM實例由類載入器+類的全限定包名和類名組成類的唯一標誌。
實現:載入類的時候,JVM 判斷類是否來自相同的載入器,如果相同而且全限定名則直接返回記憶體已有的類。
說明:上文我們提到如何防止相同類的後門問題,有了這個黃金法則,即使相同的類路徑和類,但是由於是由自定義類載入器載入的,即使編譯通過能被載入到記憶體,也無法使用,因為JVM核心類是由內置類載入器載入標誌和使用的,從而保證了JVM的安全載入。通過緩存類載入器和全限定包名和類名作為類唯一索引,載入重覆類則拋異常提示”attempted duplicate class definition for name”。
原理:雙親委派機制父類檢查緩存,源碼我們介紹loadClass方法的時候已經講過,破壞雙親委派的自定義類載入器在載入類二進位位元組碼後需要調用defineClass方法,而該方法同樣會從JVM方法區檢索緩存類,存在的話則提示重覆定義。
載入過程
至此我們已經深刻認識到類載入器的工作原理及其存在的意義,下麵我們將介紹類從外部介質載入使用到卸載整個閉環的生命周期。
載入
上文花了不少的篇幅說明瞭類的結構和類是如何被載入到JVM記憶體裡面的,那究竟什麼時候JVM才會觸發類載入器去載入外部的CLASS文件呢?通常有如下四種情況會觸發到:
-
顯式位元組碼指令集(new/getstatic/putstatic/invokestatic):對應的場景就是創建對象或者調用到類文件的靜態變數/靜態方法/靜態代碼塊
-
反射:通過對象反射獲取類對象時
-
繼承:創建子類觸發父類載入
-
入口:包含main方法的類首先被載入
JVM只定了類載入器的規範,但卻不明確規定類載入器的目標文件,把載入的具體邏輯充分交給了用戶,包括重硬碟載入的CLASS類到網路,中間文件等,只要載入進去記憶體的二進位數據流符合JVM規定的格式,都是合法的。
鏈接
類載入器載入完類到JVM實例的指定記憶體區域(方法區下文會提到)後,是使用前會經過驗證,準備解析的階段。
-
驗證:主要包含對類文件對應記憶體二進位數據的格式、語義關聯、語法邏輯和符合引用的驗證,如果驗證不通過則跑出VerifyError的錯誤。但是該階段並非強制執行,可以通過-Xverify:none來關閉,提高性能。
-
準備:但我們驗證通過時,記憶體的方法區存放的是被“緊密壓縮”的數據段,這個時候會對static的變數進行記憶體分配,也就是擴展記憶體段的空間,為該變數匹配對應類型的記憶體空間,但還未初始化數據,也就是0或者null的值。
-
解析:我們知道類的數據結構類似一個資料庫,裡面多張不同類型的“表”緊湊的挨在一起,最大的節省類占用的空間。多數表都會應用到常量池表裡面的字面量,這個時候就是把引用的字面量轉化為直接的變數空間。比如某一個複雜類變數字面量在類文件里只占2個位元組,但是通過常量池引用的轉換為實際的變數類型,需要占用32個位元組。所以經過解析階段後,類在方法區占用的空間就會膨脹,長得更像一個”類“了。
初始化
方法區經過解析後類已經為各個變數占好坑了,初始化就是把變數的初始值和構造方法的內容初始化到變數的空間裡面。這時候我們介質的類二進位文件所定義的內容,已經完全被“翻譯”方法區的某一段記憶體空間了。萬事俱備只待使用了。
使用
使用呼應了我們載入類的觸發條件,也即是觸發類載入的條件也是類應用的條件,該操作會在初始化完成後進行。
卸載
我們知道JVM有垃圾回收機制(下文會詳細介紹),不需要我們操心,總體上有三個條件會觸發垃圾回收期清理方法區的空間:
-
類對應實例被回收
-
類對應載入器被回收
-
類無反射引用
本節結束我們已經對整個類的生命周期爛熟於胸了,下麵我們來介紹類載入機制最核心的幾種應用場景,來加深對類載入技術的認識。
3. 應用場景
通過前文的剖析我們已經非常清楚類載入器的工作原理,那麼我們該如何利用類載入器的特點,最大限度的發揮它的作用呢?
熱部署
背景
熱部署這個辭彙我們經常聽說也經常提起,但是卻很少能夠準確的描述出它的定義。說到熱部署我們第一時間想到的可能是生產上的機器更新代碼後無需重啟應用容器就能更新服務,這樣的好處就是服務無需中斷可持續運行,那麼與之對應的冷部署當然就是要重啟應用容器實例了。還有可能會想到的是使用IDE工具開發時不需要重啟服務,修改代碼後即時生效,這看起來可能都是服務無需重啟,但背後的運行機制確截然不同,首先我們需要對熱部署下一個準確的定義。
-
熱部署(Hot Deployment):熱部署是應用容器自動更新應用的一種能力。
首先熱部署應用容器擁有的一種能力,這種能力是容器本身設計出來的,跟具體的IDE開發工具無關。而且熱部署無需重啟伺服器,應用可以保持用戶態不受影響。上文提到我們開發環境使用IDE工具通常也可以設置無需重啟的功能,有別於熱部署的是此時我們應用的是JVM的本身附帶的熱替換能力(HotSwap)。熱部署和熱替換是兩個完全不同概念,在開發過程中也常常相互配合使用,導致我們很多人經常混淆概念,所以接下來我們來剖析熱部署的實現原理,而熱替換的高級特性我們會在下文位元組碼增強的章節中介紹。
原理
從熱部署的定義我們知道它是應用容器蘊含的一項能力,要達到的目的就是在服務沒有重啟的情況下更新應用,也就是把新的代碼編譯後產生的新類文件替換掉記憶體里的舊類文件。結合前文我們介紹的類載入器特性,這似乎也不是很難,分兩步應該可以完成。由於同一個類載入器只能載入一次類文件,那麼新增一個類載入器把新的類文件載入進記憶體。此時記憶體裡面同時存在新舊的兩個類(類名路徑一樣,但是類載入器不一樣),要做的就是如何使用新的類,同時卸載舊的類及其對象,完成這兩步其實也就是熱部署的過程了。也即是通過使用新的類載入器,重新載入應用的類,從而達到新代碼熱部署。
實現
理解了熱部署的工作原理,下麵通過一系列極簡的例子來一步步實現熱部署,為了方便讀者演示,以下例子我儘量都在一個java文件裡面完成所有功能,運行的時候複製下去就可以跑起來。
-
實現自定義類載入器
參考4.2.2中自定義類載入器區別系統預設載入器的案例,從該案例實踐中我們可以將相同的類(包名+類名),不同”版本“(類載入器不一樣)的類同時載入進JVM記憶體方法區。
-
替換自定義類載入器
既然一個類通過不同類載入器可以被多次載入到JVM記憶體裡面,那麼類的經過修改編譯後再載入進記憶體。有別於上一步給出的例子只是修改對象的值,這次我們是直接修改類的內容,從應用的視角看其實就是應用更新,那如何做到線上程運行不中斷的情況下更換新類呢?
下麵給出的也是一個很簡單的例子,ClassReloading啟動main方法通過死迴圈不斷創建類載入器,同時不斷載入類而且執行類的方法。註意new MyClassLoader(“target/classes”)的路徑更加編譯的class路徑來修改,其他直接複製過去就可以執行演示了。
package com.zooncool.example.theory.jvm; import java.io.FileInputStream; import java.lang.reflect.InvocationTargetException; public class ClassReloading { public static void main(String[] args) throws NoSuchMethodException, ClassNotFoundException, IllegalAccessException, InstantiationException, InvocationTargetException, InterruptedException { for (;;){//用死迴圈讓線程持續運行未中斷狀態 //通過反射調用目標類的入口方法 String className = "com.zooncool.example.theory.jvm.ClassReloading$User"; Class<?> target = new MyClassLoader("target/classes").loadClass(className); //載入進來的類,通過反射調用execute方法 target.getDeclaredMethod("execute").invoke(targetClass.newInstance()); //HelloWorld.class.getDeclaredMethod("execute").invoke(HelloWorld.class.newInstance()); //如果換成系統預設類載入器的話,因為雙親委派原則,預設使用應用類載入器,而且能載入一次 //休眠是為了在刪除舊類編譯新類的這段時間內不執行載入動作 //不然會找不到類文件 Thread.sleep(10000); } } //自定義類載入器載入的目標類 public static class User { public void execute() throws InterruptedException { //say(); ask(); } public void ask(){ System.out.println("what is your name"); } public void say(){ System.out.println("my name is lucy"); } } //下麵是自定義類載入器,跟第一個例子一樣,可略過 public static class MyClassLoader extends ClassLoader{ ... } }
ClassReloading線程執行過程不斷輪流註釋say()和ask()代碼,然後編譯類,觀察程式輸出。
如下輸出結果,我們可以看出每一次迴圈調用都新創建一個自定義類載入器,然後通過反射創建對象調用方法,在修改代碼編譯後,新的類就會通過反射創建對象執行新的代碼業務,而主線程則一直沒有中斷運行。讀到這裡,其實我們已經基本觸達了熱部署的本質了,也就是實現了手動無中斷部署。但是缺點就是需要我們手動編譯代碼,而且記憶體不斷新增類載入器和對象,如果速度過快而且頻繁更新,還可能造成堆溢出,下一個例子我們將增加一些機制來保證舊的類和對象能被垃圾收集器自動回收。
what is your name what is your name what is your name//修改代碼,編譯新類 my name is lucy my name is lucy what is your name//修改代碼,編譯新類
-
回收自定義類載入器
通常情況下類載入器會持有該載入器載入過的所有類的引用,所有如果類是經過系統預設類載入器載入的話,那就很難被垃圾收集器回收,除非符合根節點不可達原則才會被回收。
下麵繼續給出一個很簡單的例子,我們知道ClassReloading只是不斷創建新的類載入器來載入新類從而更新類的方法。下麵的例子我們模擬WEB應用,更新整個應用的上下文Context。下麵代碼本質上跟上個例子的功能是一樣的,只不過我們通過載入Model層、DAO層和Service層來模擬web應用,顯得更加真實。
package com.zooncool.example.theory.jvm; import java.io.FileInputStream; import java.lang.reflect.InvocationTargetException; //應用上下文熱載入 public class ContextReloading { public static void main(String[] args) throws NoSuchMethodException, ClassNotFoundException, IllegalAccessException, InstantiationException, InvocationTargetException, InterruptedException { for (;;){ Object context = new Context();//創建應用上下文 invokeContext(context);//通過上下文對象context調用業務方法 Thread.sleep(5000); } } //創建應用的上下文,context是整個應用的GC roots,創建完返回對象之前調用init()初始化對象 public static Object newContext() throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException { String className = "com.zooncool.example.theory.jvm.ContextReloading$Context"; //通過自定義類載入器載入Context類 Class<?> contextClass = new MyClassLoader("target/classes").loadClass(className); Object context = contextClass.newInstance();//通過反射創建對象 contextClass.getDeclaredMethod("init").invoke(context);//通過反射調用初始化方法init() return context; } //業務方法,調用context的業務方法showUser() public static void invokeContext(Object context) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { context.getClass().getDeclaredMethod("showUser").invoke(context); } public static class Context{ private UserService userService = new UserService(); public String showUser(){ return userService.getUserMessage(); } //初始化對象 public void init(){ UserDao userDao = new UserDao(); userDao.setUser(new User()); userService.setUserDao(userDao); } } public static class UserService{ private UserDao userDao; public String getUserMessage(){ return userDao.getUserName(); } public void setUserDao(UserDao userDao) { this.userDao = userDao; } } public static class UserDao{