愛鋒貝

 找回密碼
 立即注冊(cè)

只需一步,快速開始

扫一扫,极速登录

查看: 792|回復(fù): 0
打印 上一主題 下一主題
收起左側(cè)

JVM核心知識(shí)體系

[復(fù)制鏈接]

1356

主題

1397

帖子

5663

積分

Rank: 8Rank: 8

跳轉(zhuǎn)到指定樓層
樓主
發(fā)表于 2023-2-8 17:06:01 | 只看該作者 回帖獎(jiǎng)勵(lì) |倒序?yàn)g覽 |閱讀模式

一鍵注冊(cè),加入手機(jī)圈

您需要 登錄 才可以下載或查看,沒有帳號(hào)?立即注冊(cè)   

x
1.問題


  • 1、如何理解類文件結(jié)構(gòu)布局?
  • 2、如何應(yīng)用類加載器的工作原理進(jìn)行將應(yīng)用輾轉(zhuǎn)騰挪?
  • 3、熱部署與熱替換有何區(qū)別,如何隔離類沖突?
  • 4、JVM如何管理內(nèi)存,有何內(nèi)存淘汰機(jī)制?
  • 5、JVM執(zhí)行引擎的工作機(jī)制是什么?
  • 6、JVM調(diào)優(yōu)應(yīng)該遵循什么原則,使用什么工具?
  • 7、JPDA架構(gòu)是什么,如何應(yīng)用代碼熱替換?
  • 8、JVM字節(jié)碼增強(qiáng)技術(shù)有哪些?
2.關(guān)鍵詞

類結(jié)構(gòu),類加載器,加載,鏈接,初始化,雙親委派,熱部署,隔離,堆,棧,方法區(qū),計(jì)數(shù)器,內(nèi)存回收,執(zhí)行引擎,調(diào)優(yōu)工具,JVMTI,JDWP,JDI,熱替換,字節(jié)碼,ASM,CGLIB,DCEVM
3.全文概要(文末有驚喜,PC端閱讀代碼更佳)

作為三大工業(yè)級(jí)別語(yǔ)言之一的JAVA如此受企業(yè)青睞有加,離不開她背后JVM的默默復(fù)出。只是由于JAVA過于成功以至于我們常常忘了JVM平臺(tái)上還運(yùn)行著像Clojure/Groovy/Kotlin/Scala/JRuby/Jython這樣的語(yǔ)言。我們享受著JVM帶來跨平臺(tái)“一次編譯到處執(zhí)行”臺(tái)的便利和自動(dòng)內(nèi)存回收的安逸。本文從JVM的最小元素類的結(jié)構(gòu)出發(fā),介紹類加載器的工作原理和應(yīng)用場(chǎng)景,思考類加載器存在的意義。進(jìn)而描述JVM邏輯內(nèi)存的分布和管理方式,同時(shí)列舉常用的JVM調(diào)優(yōu)工具和使用方法,最后介紹高級(jí)特性JDPA框架和字節(jié)碼增強(qiáng)技術(shù),實(shí)現(xiàn)熱替換。從微觀到宏觀,從靜態(tài)到動(dòng)態(tài),從基礎(chǔ)到高階介紹JVM的知識(shí)體系。
4.類的裝載

4.1類的結(jié)構(gòu)

我們知道不只JAVA文本文件,像Clojure/Groovy/Kotlin/Scala這些文本文件也同樣會(huì)經(jīng)過JDK的編譯器編程成class文件。進(jìn)入到JVM領(lǐng)域后,其實(shí)就跟JAVA沒什么關(guān)系了,JVM只認(rèn)得class文件,那么我們需要先了解class這個(gè)黑箱里面包含的是什么東西。
JVM規(guī)范嚴(yán)格定義了CLASS文件的格式,有嚴(yán)格的數(shù)據(jù)結(jié)構(gòu),下面我們可以觀察一個(gè)簡(jiǎn)單CLASS文件包含的字段和數(shù)據(jù)類型。
ClassFile {
    u4             magic;
    u2             minor_version;
    u2             major_version;
    u2             constant_pool_count;
    cp_info        constant_pool[constant_pool_count-1];
    u2             access_flags;
    u2             this_class;
    u2             super_class;
    u2             interfaces_count;
    u2             interfaces[interfaces_count];
    u2             fields_count;
    field_info     fields[fields_count];
    u2             methods_count;
    method_info    methods[methods_count];
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}
詳細(xì)的描述我們可以從JVM規(guī)范說明書里面查閱類文件格式(https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html),類的整體布局如下圖展示的。



在我的理解,我想把每個(gè)CLASS文件類別成一個(gè)一個(gè)的數(shù)據(jù)庫(kù),里面包含的常量池/類索引/屬性表集合就像數(shù)據(jù)庫(kù)的表,而且表之間也有關(guān)聯(lián),常量池則存放著其他表所需要的所有字面量。了解完類的數(shù)據(jù)結(jié)構(gòu)后,我們需要來觀察JVM是如何使用這些從硬盤上或者網(wǎng)絡(luò)傳輸過來的CLASS文件。
4.2加載機(jī)制

4.2.1類的入口

在我們探究JVM如何使用CLASS文件之前,我們快速回憶一下編寫好的C語(yǔ)言文件是如何執(zhí)行的?我們從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!
這個(gè)過程就是gcc編譯器將hello.c文本文件編譯成機(jī)器指令集,然后讀取到內(nèi)存直接在計(jì)算機(jī)的CPU運(yùn)行。從操作系統(tǒng)層面看的話,就是一個(gè)進(jìn)程的啟動(dòng)到結(jié)束的生命周期。
下面我們看JAVA是怎么運(yùn)行的。學(xué)習(xí)JAVA開發(fā)的第一件事就是先下載JDK安裝包,安裝完配置好環(huán)境變量,然后寫一個(gè)名字為helloWorld的類,然后編譯執(zhí)行,我們來觀察一下發(fā)生了什么事情?
先看源碼,有夠簡(jiǎn)單了吧。
package com.zooncool.example.theory.jvm;
public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("my classLoader is " + HelloWorld.class.getClassLoader());
    }
}
編譯執(zhí)行
$ 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
對(duì)比C語(yǔ)言在命令行直接運(yùn)行編譯后的a.out二進(jìn)制文件,JAVA的則是在命令行執(zhí)行java classFile,從命令的區(qū)別我們知道操作系統(tǒng)啟動(dòng)的是java進(jìn)程,而HelloWorld類只是命令行的入?yún)?,在操作系統(tǒng)來看java也就是一個(gè)普通的應(yīng)用進(jìn)程而已,而這個(gè)進(jìn)程就是JVM的執(zhí)行形態(tài)(JVM靜態(tài)就是硬盤里JDK包下的二進(jìn)制文件集合)。
學(xué)習(xí)過JAVA的都知道入口方法是public static void main(String[] args),缺一不可,那我猜執(zhí)行java命令時(shí)JVM對(duì)該入口方法做了唯一驗(yàn)證,通過了才允許啟動(dòng)JVM進(jìn)程,下面我們來看這個(gè)入口方法有啥特點(diǎn)。

  • 去掉public限定
    $ javac src/main/java/com/zooncool/example/theory/jvm/HelloWorld.java
    $ java -cp src/main/java/ com.zooncool.example.theory.jvm.HelloWorld
    錯(cuò)誤: 在類 com.zooncool.example.theory.jvm.HelloWorld 中找不到 main 方法, 請(qǐng)將 main 方法定義為:
       public static void main(String[] args)
    否則 JavaFX 應(yīng)用程序類必須擴(kuò)展javafx.application.Application
說名入口方法需要被public修飾,當(dāng)然JVM調(diào)用main方法是底層的JNI方法調(diào)用不受修飾符影響。

  • 去掉static限定
    $ javac src/main/java/com/zooncool/example/theory/jvm/HelloWorld.java
    $ java -cp src/main/java/ com.zooncool.example.theory.jvm.HelloWorld
    錯(cuò)誤: main 方法不是類 com.zooncool.example.theory.jvm.HelloWorld 中的static, 請(qǐng)將 main 方法定義為:
       public static void main(String[] args)
我們是從類對(duì)象調(diào)用而不是類創(chuàng)建的對(duì)象才調(diào)用,索引需要靜態(tài)修飾

  • 返回類型改為int
    $ javac src/main/java/com/zooncool/example/theory/jvm/HelloWorld.java
    $ java -cp src/main/java/ com.zooncool.example.theory.jvm.HelloWorld
    錯(cuò)誤: main 方法必須返回類 com.zooncool.example.theory.jvm.HelloWorld 中的空類型值, 請(qǐng)
    將 main 方法定義為:
       public static void main(String[] args)
void返回類型讓JVM調(diào)用后無需關(guān)心調(diào)用者的使用情況,執(zhí)行完就停止,簡(jiǎn)化JVM的設(shè)計(jì)。

  • 方法簽名改為main1
    $ javac src/main/java/com/zooncool/example/theory/jvm/HelloWorld.java
    $ java -cp src/main/java/ com.zooncool.example.theory.jvm.HelloWorld
    錯(cuò)誤: 在類 com.zooncool.example.theory.jvm.HelloWorld 中找不到 main 方法, 請(qǐng)將 main 方法定義為:
       public static void main(String[] args)
    否則 JavaFX 應(yīng)用程序類必須擴(kuò)展javafx.application.Application
這個(gè)我也不清楚,可能是約定俗成吧,畢竟C/C++也是用main方法的。
說了這么多main方法的規(guī)則,其實(shí)我們關(guān)心的只有兩點(diǎn):

  • HelloWorld類是如何被JVM使用的
  • HelloWorld類里面的main方法是如何被執(zhí)行的
關(guān)于JVM如何使用HelloWorld下文我們會(huì)詳細(xì)講到。
我們知道JVM是由C/C++語(yǔ)言實(shí)現(xiàn)的,那么JVM跟CLASS打交道則需要JNI(Java Native Interface)這座橋梁,當(dāng)我們?cè)诿钚袌?zhí)行java時(shí),由C/C++實(shí)現(xiàn)的java應(yīng)用通過JNI找到了HelloWorld里面符合規(guī)范的main方法,然后開始調(diào)用。我們來看下java命令的源碼就知道了
/*
* Get the application's main class.
*/
if (jarfile != 0) {
mainClassName = GetMainClassName(env, jarfile);
... ...
mainClass = LoadClass(env, classname);
if(mainClass == NULL) { /* exception occured */
... ...
/* Get the application's main method */
mainID = (*env)->GetStaticMethodID(env, mainClass, "main", "([Ljava/lang/String;)V");
... ...
{/* Make sure the main method is public */
jint mods;
jmethodID mid;
jobject obj = (*env)->ToReflectedMethod(env, mainClass, mainID, JNI_TRUE);
... ...
/* Build argument array */
mainArgs = NewPlatformStringArray(env, argv, argc);
if (mainArgs == NULL) {
ReportExceptionDescription(env);
goto leave;
}
/* Invoke main method. */
(*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs);
4.2.2類加載器

上一節(jié)我們留了一個(gè)核心的環(huán)節(jié),就是JVM在執(zhí)行類的入口之前,首先得找到類再然后再把類裝到JVM實(shí)例里面,也即是JVM進(jìn)程維護(hù)的內(nèi)存區(qū)域內(nèi)。我們當(dāng)然知道是一個(gè)叫做類加載器的工具把類加載到JVM實(shí)例里面,拋開細(xì)節(jié)從操作系統(tǒng)層面觀察,那么就是JVM實(shí)例在運(yùn)行過程中通過IO從硬盤或者網(wǎng)絡(luò)讀取CLASS二進(jìn)制文件,然后在JVM管轄的內(nèi)存區(qū)域存放對(duì)應(yīng)的文件。我們目前還不知道類加載器的實(shí)現(xiàn),但是我們從功能上判斷無非就是讀取文件到內(nèi)存,這個(gè)是很普通也很簡(jiǎn)單的操作。
如果類加載器是C/C++實(shí)現(xiàn)的話,那么大概就是如下代碼就可以實(shí)現(xiàn)
char *fgets( char *buf, int n, FILE *fp );
如果是JAVA實(shí)現(xiàn),那么也很簡(jiǎn)單
InputStream f = new FileInputStream("theory/jvm/HelloWorld.class");
從操作系統(tǒng)層面看的話,如果只是加載,以上代碼就足以把類文件加載到JVM內(nèi)存里面了。但是結(jié)果就是亂糟糟的把一堆毫無秩序的類文件往內(nèi)存里面扔,沒有良好的管理也沒法用,所以需要我們需要設(shè)計(jì)一套規(guī)則來管理存放內(nèi)存里面的CLASS文件,我們稱為類加載的設(shè)計(jì)模式或者類加載機(jī)制,這個(gè)下文會(huì)重點(diǎn)解釋。
根據(jù)官網(wǎng)的定義A class loader is an object that is responsible for loading classes. 類加載器就是負(fù)責(zé)加載類的。我們知道啟動(dòng)JVM的時(shí)候會(huì)把JRE默認(rèn)的一些類加載到內(nèi)存,這部分類使用的加載器是JVM默認(rèn)內(nèi)置的由C/C++實(shí)現(xiàn)的,比如我們上文加載的HelloWorld.class。但是內(nèi)置的類加載器有明確的范圍限定,也就是只能加載指定路徑下的jar包(類文件的集合)。如果只是加載JRE的類,那可玩的花樣就少很多,JRE只是提供了底層所需的類,更多的業(yè)務(wù)需要我們從外部加載類來支持,所以我們需要指定新的規(guī)則,以方便我們加載外部路徑的類文件。
系統(tǒng)默認(rèn)加載器


  • Bootstrap class loader
    作用:?jiǎn)?dòng)類加載器,加載JDK核心類
    類加載器:C/C++實(shí)現(xiàn)
    類加載路徑: /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

    實(shí)現(xiàn)原理:本地方法由C++實(shí)現(xiàn)
  • Extensions class loader
    作用:擴(kuò)展類加載器,加載JAVA擴(kuò)展類庫(kù)。
    類加載器:JAVA實(shí)現(xiàn)
    類加載路徑:/jre/lib/ext
    System.out.println(System.getProperty("java.ext.dirs"));
    /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/ext:

    實(shí)現(xiàn)原理:擴(kuò)展類加載器ExtClassLoader本質(zhì)上也是URLClassLoader
    Launcher.java
    //構(gòu)造方法返回?cái)U(kuò)展類加載器
    public Launcher() {
    //定義擴(kuò)展類加載器
        Launcher.ExtClassLoader var1;
    try {
    //1、獲取擴(kuò)展類加載器
            var1 = Launcher.ExtClassLoader.getExtClassLoader();
        } catch (IOException var10) {
    throw new InternalError("Could not create extension class loader", var10);
        }
        ...
    }

    //擴(kuò)展類加載器
    static class ExtClassLoader extends URLClassLoader {
    private static volatile Launcher.ExtClassLoader instance;
    //2、獲取擴(kuò)展類加載器實(shí)現(xiàn)
    public static Launcher.ExtClassLoader getExtClassLoader() throws IOException {
    if (instance == null) {
                    Class var0 = Launcher.ExtClassLoader.class;
    synchronized(Launcher.ExtClassLoader.class) {
    if (instance == null) {
    //3、構(gòu)造擴(kuò)展類加載器
                            instance = createExtClassLoader();
                        }
                    }
                }
    return instance;
         }
    //4、構(gòu)造擴(kuò)展類加載器具體實(shí)現(xiàn)
    private static Launcher.ExtClassLoader createExtClassLoader() throws IOException {
    try {
    return (Launcher.ExtClassLoader)AccessController.doPrivileged(new PrivilegedExceptionAction<Launcher.ExtClassLoader>() {
    public Launcher.ExtClassLoader run() throws IOException {
    //5、獲取擴(kuò)展類加載器加載目標(biāo)類的目錄
                        File[] var1 = Launcher.ExtClassLoader.getExtDirs();
    int var2 = var1.length;
    for(int var3 = 0; var3 < var2; ++var3) {
                            MetaIndex.registerDirectory(var1[var3]);
                        }
    //7、構(gòu)造擴(kuò)展類加載器
    return new Launcher.ExtClassLoader(var1);
                    }
                });
            } catch (PrivilegedActionexception var1) {
    throw (IOException)var1.getException();
            }
        }
    //6、擴(kuò)展類加載器目錄路徑
    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、擴(kuò)展類加載器構(gòu)造方法
    public ExtClassLoader(File[] var1) throws IOException {
    super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
            SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
        }
    }
  • System class loader
    作用:系統(tǒng)類加載器,加載應(yīng)用指定環(huán)境變量路徑下的類
    類加載器:sun.misc.Launcher$AppClassLoader
    類加載路徑:-classpath下面的所有類
    實(shí)現(xiàn)原理:系統(tǒng)類加載器AppClassLoader本質(zhì)上也是URLClassLoader
    Launcher.java
    //構(gòu)造方法返回系統(tǒng)類加載器
    public Launcher() {
    try {
    //獲取系統(tǒng)類加載器
    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);
    //系統(tǒng)類加載器實(shí)現(xiàn)邏輯
    public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
    //類比擴(kuò)展類加載器,相似的邏輯
    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);
                }
            });
        }
    //系統(tǒng)類加載器構(gòu)造方法
        AppClassLoader(URL[] var1, ClassLoader var2) {
    super(var1, var2, Launcher.factory);
    this.ucp.initLookupCache(this);
        }
    }
通過上文運(yùn)行HelloWorld我們知道JVM系統(tǒng)默認(rèn)加載的類大改是1560個(gè),如下圖



自定義類加載器

內(nèi)置類加載器只加載了最少需要的核心JAVA基礎(chǔ)類和環(huán)境變量下的類,但是我們應(yīng)用往往需要依賴第三方中間件來完成額外的業(yè)務(wù),那么如何把它們的類加載進(jìn)來就顯得格外重要了。幸好JVM提供了自定義類加載器,可以很方便的完成自定義操作,最終目的也是把外部的類文件加載到JVM內(nèi)存。通過繼承ClassLoader類并且復(fù)寫findClass和loadClass方法就可以達(dá)到自定義獲取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 {
                    //如果為空則說明應(yīng)用啟動(dòng)類加載器,讓它去加載
                    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
                //如果還是沒有就調(diào)用自己的方法,確保調(diào)用自己方法前都使用了父類方法,如此遞歸三次到頂
                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);
}
通過復(fù)寫loadClass方法,我們甚至可以讀取一份加了密的文件,然后在內(nèi)存里面解密,這樣別人反編譯你的源碼也沒用,因?yàn)閏lass是經(jīng)過加密的,也就是理論上我們通過自定義類加載器可以做到為所欲為,但是有個(gè)重要的原則下文介紹類加載器設(shè)計(jì)模式會(huì)提到。
一下給出一個(gè)自定義類加載器極簡(jiǎn)的案例,來說明自定義類加載器的實(shí)現(xiàn)。
package com.zooncool.example.theory.jvm;
import java.io.FileInputStream;
import static java.lang.System.out;

public class ClassIsolationPrinciple {
    public static void main(String[] args) {
        try {
            String className = "com.zooncool.example.theory.jvm.ClassIsolationPrinciple$Demo"; //定義要加載類的全限定名
            Class<?> class1 = Demo.class;  //第一個(gè)類又系統(tǒng)默認(rèn)類加載器加載
            //第二個(gè)類MyClassLoader為自定義類加載器,自定義的目的是覆蓋加載類的邏輯
            Class<?> class2 = new MyClassLoader("target/classes").loadClass(className);
            out.println("-----------------class name-----------------");
            out.println(class1.getName());
            out.println(class2.getName());
            out.println("-----------------classLoader name-----------------");
            out.println(class1.getClassLoader());
            out.println(class2.getClassLoader());
            Demo.example = 1;//這里修改的系統(tǒng)類加載器加載的那個(gè)類的對(duì)象,而自定義加載器加載進(jìn)去的類的對(duì)象保持不變,也即是同時(shí)存在內(nèi)存,但沒有修改example的值。
            out.println("-----------------field value-----------------");
            out.println(class1.getDeclaredField("example").get(null));
            out.println(class2.getDeclaredField("example").get(null));
        }  catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    public static class Demo {
        public static int example = 0;
    }

    public static class MyClassLoader extends ClassLoader{
        private String classPath;
        public MyClassLoader(String classPath) {
            this.classPath = classPath;
        }
        //自定義類加載器繼承了ClassLoader,稱為一個(gè)可以加載類的加載器,同時(shí)覆蓋了loadClass方法,實(shí)現(xiàn)自己的邏輯
        @Override
        public Class<?> loadClass(String name) throws ClassNotFoundException {
            if(!name.contains("java.lang")){//排除掉加載系統(tǒng)默認(rèn)需要加載的內(nèi)心類,因?yàn)樾╊愔荒苡帜J(rèn)類加載器去加載,第三方加載會(huì)拋異常,具體原因下文解釋
                byte[] data = new byte[0];
                try {
                    data = loadByte(name);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return defineClass(name,data,0,data.length);
            }else{
                return super.loadClass(name);
            }
        }
        //把影片的二進(jìn)制類文件讀入內(nèi)存字節(jié)流
        private byte[] loadByte(String name) throws Exception {
            name = name.replaceAll("\\.", "/");
            String dir = classPath + "/" + name + ".class";
            FileInputStream fis = new FileInputStream(dir);
            int len = fis.available();
            byte[] data = new byte[len];
            fis.read(data);
            fis.close();
            return data;
        }
    }
}
執(zhí)行結(jié)果如下,我們可以看到加載到內(nèi)存方法區(qū)的兩個(gè)類的包名+名稱是一樣的,而對(duì)應(yīng)的類加載器卻不一樣,而且輸出被加載類的值也是不一樣的。
-----------------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
4.2.3設(shè)計(jì)模式

現(xiàn)有的加載器分為內(nèi)置類加載器和自定義加載器,不管它們是通過C或者JAVA實(shí)現(xiàn)的最終都是為了把外部的CLASS文件加載到JVM內(nèi)存里面。那么我們就需要設(shè)計(jì)一套規(guī)則來管理組織內(nèi)存里面的CLASS文件,下面我們就來介紹下通過這套規(guī)則如何來協(xié)調(diào)好內(nèi)置類加載器和自定義類加載器之間的權(quán)責(zé)。
我們知道通過自定義類加載器可以干出很多黑科技,但是有個(gè)基本的雷區(qū)就是,不能隨便替代JAVA的核心基礎(chǔ)類,或者說即是你寫了一個(gè)跟核心類一模一樣的類,JVM也不會(huì)使用。你想一下,如果為所欲為的你可以把最基礎(chǔ)本的java.lang.Object都換成你自己定義的同名類,然后搞個(gè)后門進(jìn)去,而且JVM還使用的話,那誰(shuí)還敢用JAVA了是吧,所以我們會(huì)介紹一個(gè)重要的原則,在此之前我們先介紹一下內(nèi)置類加載器和自定義類加載器是如何協(xié)同的。

  • 雙親委派機(jī)制
    定義:某個(gè)特定的類加載器在接到加載類的請(qǐng)求時(shí),首先將加載任務(wù)委托給父類加載器,依次遞歸,如果父類加載器可以完成類加載任務(wù),就成功返回;只有父類加載器無法完成此加載任務(wù)時(shí),才自己去加載。
    實(shí)現(xiàn):參考上文loadClass方法的源碼和注釋,通過最多三次遞歸可以到啟動(dòng)類加載器,如果還是找不到這調(diào)用自定義方法。




雙親委派機(jī)制很好理解,目的就是為了不重復(fù)加載已有的類,提高效率,還有就是強(qiáng)制從父類加載器開始逐級(jí)搜索類文件,確保核心基礎(chǔ)類優(yōu)先加載。下面介紹的是破壞雙親委派機(jī)制,了解為什么要破壞這種看似穩(wěn)固的雙親委派機(jī)制。

  • 破壞委派機(jī)制
    定義:打破類加載自上而上委托的約束。
    實(shí)現(xiàn):1、繼承ClassLoader并且重寫loadClass方法體,覆蓋依賴上層類加載器的邏輯;
    2、”啟動(dòng)類加載器”可以指定“線程上下文類加載器”為任意類加載器,即是“父類加載器”委托“子類加載器”去加載不屬于它加載范圍的類文件;
    說明:雙親委派機(jī)制的好處上面我們已經(jīng)提過了,但是由于一些歷史原因(JDK1.2加上雙親委派機(jī)制前的JDK1.1就已經(jīng)存在,為了向前兼容不得不開這個(gè)后門讓1.2版本的類加載器擁有1.1隨意加載的功能)。還有就是JNDI的服務(wù)調(diào)用機(jī)制,例如調(diào)用JDBC需要從外部加載相關(guān)類到JVM實(shí)例的內(nèi)存空間。
介紹完內(nèi)置類加載器和自定義類加載器的協(xié)同關(guān)系后,我們要重點(diǎn)強(qiáng)調(diào)上文提到的重要原則。

  • 唯一標(biāo)識(shí)
    定義:JVM實(shí)例由類加載器+類的全限定包名和類名組成類的唯一標(biāo)志。
    實(shí)現(xiàn):加載類的時(shí)候,JVM 判斷類是否來自相同的加載器,如果相同而且全限定名則直接返回內(nèi)存已有的類。
    說明:上文我們提到如何防止相同類的后門問題,有了這個(gè)黃金法則,即使相同的類路徑和類,但是由于是由自定義類加載器加載的,即使編譯通過能被加載到內(nèi)存,也無法使用,因?yàn)镴VM核心類是由內(nèi)置類加載器加載標(biāo)志和使用的,從而保證了JVM的安全加載。通過緩存類加載器和全限定包名和類名作為類唯一索引,加載重復(fù)類則拋異常提示”attempted duplicate class definition for name”。
    原理:雙親委派機(jī)制父類檢查緩存,源碼我們介紹loadClass方法的時(shí)候已經(jīng)講過,破壞雙親委派的自定義類加載器在加載類二進(jìn)制字節(jié)碼后需要調(diào)用defineClass方法,而該方法同樣會(huì)從JVM方法區(qū)檢索緩存類,存在的話則提示重復(fù)定義。
4.2.4加載過程

至此我們已經(jīng)深刻認(rèn)識(shí)到類加載器的工作原理及其存在的意義,下面我們將介紹類從外部介質(zhì)加載使用到卸載整個(gè)閉環(huán)的生命周期。
加載

上文花了不少的篇幅說明了類的結(jié)構(gòu)和類是如何被加載到JVM內(nèi)存里面的,那究竟什么時(shí)候JVM才會(huì)觸發(fā)類加載器去加載外部的CLASS文件呢?通常有如下四種情況會(huì)觸發(fā)到:

  • 顯式字節(jié)碼指令集(new/getstatic/putstatic/invokestatic):對(duì)應(yīng)的場(chǎng)景就是創(chuàng)建對(duì)象或者調(diào)用到類文件的靜態(tài)變量/靜態(tài)方法/靜態(tài)代碼塊
  • 反射:通過對(duì)象反射獲取類對(duì)象時(shí)
  • 繼承:創(chuàng)建子類觸發(fā)父類加載
  • 入口:包含main方法的類首先被加載
JVM只定了類加載器的規(guī)范,但卻不明確規(guī)定類加載器的目標(biāo)文件,把加載的具體邏輯充分交給了用戶,包括重硬盤加載的CLASS類到網(wǎng)絡(luò),中間文件等,只要加載進(jìn)去內(nèi)存的二進(jìn)制數(shù)據(jù)流符合JVM規(guī)定的格式,都是合法的。
鏈接

類加載器加載完類到JVM實(shí)例的指定內(nèi)存區(qū)域(方法區(qū)下文會(huì)提到)后,是使用前會(huì)經(jīng)過驗(yàn)證,準(zhǔn)備解析的階段。

  • 驗(yàn)證:主要包含對(duì)類文件對(duì)應(yīng)內(nèi)存二進(jìn)制數(shù)據(jù)的格式、語(yǔ)義關(guān)聯(lián)、語(yǔ)法邏輯和符合引用的驗(yàn)證,如果驗(yàn)證不通過則跑出VerifyError的錯(cuò)誤。但是該階段并非強(qiáng)制執(zhí)行,可以通過-Xverify:none來關(guān)閉,提高性能。
  • 準(zhǔn)備:但我們驗(yàn)證通過時(shí),內(nèi)存的方法區(qū)存放的是被“緊密壓縮”的數(shù)據(jù)段,這個(gè)時(shí)候會(huì)對(duì)static的變量進(jìn)行內(nèi)存分配,也就是擴(kuò)展內(nèi)存段的空間,為該變量匹配對(duì)應(yīng)類型的內(nèi)存空間,但還未初始化數(shù)據(jù),也就是0或者null的值。
  • 解析:我們知道類的數(shù)據(jù)結(jié)構(gòu)類似一個(gè)數(shù)據(jù)庫(kù),里面多張不同類型的“表”緊湊的挨在一起,最大的節(jié)省類占用的空間。多數(shù)表都會(huì)應(yīng)用到常量池表里面的字面量,這個(gè)時(shí)候就是把引用的字面量轉(zhuǎn)化為直接的變量空間。比如某一個(gè)復(fù)雜類變量字面量在類文件里只占2個(gè)字節(jié),但是通過常量池引用的轉(zhuǎn)換為實(shí)際的變量類型,需要占用32個(gè)字節(jié)。所以經(jīng)過解析階段后,類在方法區(qū)占用的空間就會(huì)膨脹,長(zhǎng)得更像一個(gè)”類“了。
初始化

方法區(qū)經(jīng)過解析后類已經(jīng)為各個(gè)變量占好坑了,初始化就是把變量的初始值和構(gòu)造方法的內(nèi)容初始化到變量的空間里面。這時(shí)候我們介質(zhì)的類二進(jìn)制文件所定義的內(nèi)容,已經(jīng)完全被“翻譯”方法區(qū)的某一段內(nèi)存空間了。萬(wàn)事俱備只待使用了。
使用

使用呼應(yīng)了我們加載類的觸發(fā)條件,也即是觸發(fā)類加載的條件也是類應(yīng)用的條件,該操作會(huì)在初始化完成后進(jìn)行。
卸載

我們知道JVM有垃圾回收機(jī)制(下文會(huì)詳細(xì)介紹),不需要我們操心,總體上有三個(gè)條件會(huì)觸發(fā)垃圾回收期清理方法區(qū)的空間:

  • 類對(duì)應(yīng)實(shí)例被回收
  • 類對(duì)應(yīng)加載器被回收
  • 類無反射引用
本節(jié)結(jié)束我們已經(jīng)對(duì)整個(gè)類的生命周期爛熟于胸了,下面我們來介紹類加載機(jī)制最核心的幾種應(yīng)用場(chǎng)景,來加深對(duì)類加載技術(shù)的認(rèn)識(shí)。
4.3應(yīng)用場(chǎng)景

通過前文的剖析我們已經(jīng)非常清楚類加載器的工作原理,那么我們?cè)撊绾卫妙惣虞d器的特點(diǎn),最大限度的發(fā)揮它的作用呢?
4.3.1熱部署

背景

熱部署這個(gè)詞匯我們經(jīng)常聽說也經(jīng)常提起,但是卻很少能夠準(zhǔn)確的描述出它的定義。說到熱部署我們第一時(shí)間想到的可能是生產(chǎn)上的機(jī)器更新代碼后無需重啟應(yīng)用容器就能更新服務(wù),這樣的好處就是服務(wù)無需中斷可持續(xù)運(yùn)行,那么與之對(duì)應(yīng)的冷部署當(dāng)然就是要重啟應(yīng)用容器實(shí)例了。還有可能會(huì)想到的是使用IDE工具開發(fā)時(shí)不需要重啟服務(wù),修改代碼后即時(shí)生效,這看起來可能都是服務(wù)無需重啟,但背后的運(yùn)行機(jī)制確截然不同,首先我們需要對(duì)熱部署下一個(gè)準(zhǔn)確的定義。

  • 熱部署(Hot Deployment):熱部署是應(yīng)用容器自動(dòng)更新應(yīng)用的一種能力。
首先熱部署應(yīng)用容器擁有的一種能力,這種能力是容器本身設(shè)計(jì)出來的,跟具體的IDE開發(fā)工具無關(guān)。而且熱部署無需重啟服務(wù)器,應(yīng)用可以保持用戶態(tài)不受影響。上文提到我們開發(fā)環(huán)境使用IDE工具通常也可以設(shè)置無需重啟的功能,有別于熱部署的是此時(shí)我們應(yīng)用的是JVM的本身附帶的熱替換能力(HotSwap)。熱部署和熱替換是兩個(gè)完全不同概念,在開發(fā)過程中也常常相互配合使用,導(dǎo)致我們很多人經(jīng)常混淆概念,所以接下來我們來剖析熱部署的實(shí)現(xiàn)原理,而熱替換的高級(jí)特性我們會(huì)在下文字節(jié)碼增強(qiáng)的章節(jié)中介紹。
原理

從熱部署的定義我們知道它是應(yīng)用容器蘊(yùn)含的一項(xiàng)能力,要達(dá)到的目的就是在服務(wù)沒有重啟的情況下更新應(yīng)用,也就是把新的代碼編譯后產(chǎn)生的新類文件替換掉內(nèi)存里的舊類文件。結(jié)合前文我們介紹的類加載器特性,這似乎也不是很難,分兩步應(yīng)該可以完成。由于同一個(gè)類加載器只能加載一次類文件,那么新增一個(gè)類加載器把新的類文件加載進(jìn)內(nèi)存。此時(shí)內(nèi)存里面同時(shí)存在新舊的兩個(gè)類(類名路徑一樣,但是類加載器不一樣),要做的就是如何使用新的類,同時(shí)卸載舊的類及其對(duì)象,完成這兩步其實(shí)也就是熱部署的過程了。也即是通過使用新的類加載器,重新加載應(yīng)用的類,從而達(dá)到新代碼熱部署。
實(shí)現(xiàn)

理解了熱部署的工作原理,下面通過一系列極簡(jiǎn)的例子來一步步實(shí)現(xiàn)熱部署,為了方便讀者演示,以下例子我盡量都在一個(gè)java文件里面完成所有功能,運(yùn)行的時(shí)候復(fù)制下去就可以跑起來。

  • 實(shí)現(xiàn)自定義類加載器
參考4.2.2中自定義類加載器區(qū)別系統(tǒng)默認(rèn)加載器的案例,從該案例實(shí)踐中我們可以將相同的類(包名+類名),不同”版本“(類加載器不一樣)的類同時(shí)加載進(jìn)JVM內(nèi)存方法區(qū)。

  • 替換自定義類加載器
既然一個(gè)類通過不同類加載器可以被多次加載到JVM內(nèi)存里面,那么類的經(jīng)過修改編譯后再加載進(jìn)內(nèi)存。有別于上一步給出的例子只是修改對(duì)象的值,這次我們是直接修改類的內(nèi)容,從應(yīng)用的視角看其實(shí)就是應(yīng)用更新,那如何做到在線程運(yùn)行不中斷的情況下更換新類呢?
下面給出的也是一個(gè)很簡(jiǎn)單的例子,ClassReloading啟動(dòng)main方法通過死循環(huán)不斷創(chuàng)建類加載器,同時(shí)不斷加載類而且執(zhí)行類的方法。注意new MyClassLoader(“target/classes”)的路徑更加編譯的class路徑來修改,其他直接復(fù)制過去就可以執(zhí)行演示了。
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 (;;){//用死循環(huán)讓線程持續(xù)運(yùn)行未中斷狀態(tài)
            //通過反射調(diào)用目標(biāo)類的入口方法
            String className = "com.zooncool.example.theory.jvm.ClassReloading$User";
            Class<?> target = new MyClassLoader("target/classes").loadClass(className);
            //加載進(jìn)來的類,通過反射調(diào)用execute方法
            target.getDeclaredMethod("execute").invoke(targetClass.newInstance());
            //HelloWorld.class.getDeclaredMethod("execute").invoke(HelloWorld.class.newInstance());
            //如果換成系統(tǒng)默認(rèn)類加載器的話,因?yàn)殡p親委派原則,默認(rèn)使用應(yīng)用類加載器,而且能加載一次
            //休眠是為了在刪除舊類編譯新類的這段時(shí)間內(nèi)不執(zhí)行加載動(dòng)作
            //不然會(huì)找不到類文件
            Thread.sleep(10000);
        }
    }
    //自定義類加載器加載的目標(biāo)類
    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");
        }
    }
    //下面是自定義類加載器,跟第一個(gè)例子一樣,可略過
    public static class MyClassLoader extends ClassLoader{
        ...
    }
}
ClassReloading線程執(zhí)行過程不斷輪流注釋say()和ask()代碼,然后編譯類,觀察程序輸出。
如下輸出結(jié)果,我們可以看出每一次循環(huán)調(diào)用都新創(chuàng)建一個(gè)自定義類加載器,然后通過反射創(chuàng)建對(duì)象調(diào)用方法,在修改代碼編譯后,新的類就會(huì)通過反射創(chuàng)建對(duì)象執(zhí)行新的代碼業(yè)務(wù),而主線程則一直沒有中斷運(yùn)行。讀到這里,其實(shí)我們已經(jīng)基本觸達(dá)了熱部署的本質(zhì)了,也就是實(shí)現(xiàn)了手動(dòng)無中斷部署。但是缺點(diǎn)就是需要我們手動(dòng)編譯代碼,而且內(nèi)存不斷新增類加載器和對(duì)象,如果速度過快而且頻繁更新,還可能造成堆溢出,下一個(gè)例子我們將增加一些機(jī)制來保證舊的類和對(duì)象能被垃圾收集器自動(dòng)回收。
what is your name
what is your name
what is your name//修改代碼,編譯新類
my name is lucy
my name is lucy
what is your name//修改代碼,編譯新類

  • 回收自定義類加載器
通常情況下類加載器會(huì)持有該加載器加載過的所有類的引用,所有如果類是經(jīng)過系統(tǒng)默認(rèn)類加載器加載的話,那就很難被垃圾收集器回收,除非符合根節(jié)點(diǎn)不可達(dá)原則才會(huì)被回收。
下面繼續(xù)給出一個(gè)很簡(jiǎn)單的例子,我們知道ClassReloading只是不斷創(chuàng)建新的類加載器來加載新類從而更新類的方法。下面的例子我們模擬WEB應(yīng)用,更新整個(gè)應(yīng)用的上下文Context。下面代碼本質(zhì)上跟上個(gè)例子的功能是一樣的,只不過我們通過加載Model層、DAO層和Service層來模擬web應(yīng)用,顯得更加真實(shí)。
package com.zooncool.example.theory.jvm;
import java.io.FileInputStream;
import java.lang.reflect.InvocationTargetException;
//應(yīng)用上下文熱加載
public class ContextReloading {
    public static void main(String[] args)
        throws NoSuchMethodException, ClassNotFoundException, IllegalAccessException, InstantiationException,
        InvocationTargetException, InterruptedException {
        for (;;){
            Object context = newContext();//創(chuàng)建應(yīng)用上下文
            invokeContext(context);//通過上下文對(duì)象context調(diào)用業(yè)務(wù)方法
            Thread.sleep(5000);
        }
    }
    //創(chuàng)建應(yīng)用的上下文,context是整個(gè)應(yīng)用的GC roots,創(chuàng)建完返回對(duì)象之前調(diào)用init()初始化對(duì)象
    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();//通過反射創(chuàng)建對(duì)象
        contextClass.getDeclaredMethod("init").invoke(context);//通過反射調(diào)用初始化方法init()
        return context;
    }
    //業(yè)務(wù)方法,調(diào)用context的業(yè)務(wù)方法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();
        }
        //初始化對(duì)象
        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{
        private User user;
        public String getUserName(){
            //關(guān)鍵操作,運(yùn)行main方法后切換下面方法,編譯后下一次調(diào)用生效
            return user.getName();
            //return user.getFullName();
        }
        public void setUser(User user) {
            this.user = user;
        }
    }

    public static class User{
        private String name = "lucy";
        private String fullName = "hank.lucy";
        public String getName() {
            System.out.println("my name is " + name);
            return name;
        }
        public String getFullName() {
            System.out.println("my full name is " + fullName);
            return name;
        }
    }
    //跟之前的類加載器一模一樣,可以略過
    public static class MyClassLoader extends ClassLoader{
        ...
    }
}
輸出結(jié)果跟上一個(gè)例子相似,可以自己運(yùn)行試試。我們更新業(yè)務(wù)方法編譯通過后,無需重啟main方法,新的業(yè)務(wù)就能生效,而且也解決了舊類卸載的核心問題,因?yàn)閏ontext的應(yīng)用對(duì)象的跟節(jié)點(diǎn),context是由我們自定義類加載器所加載,由于User/Dao/Service都是依賴context,所以其類也是又自定義類加載器所加載。根據(jù)GC roots原理,在創(chuàng)建新的自定義類加載器之后,舊的類加載器已經(jīng)沒有任何引用鏈可訪達(dá),符合GC回收規(guī)則,將會(huì)被GC收集器回收釋放內(nèi)存。至此已經(jīng)完成應(yīng)用熱部署的流程,但是細(xì)心的朋友可能會(huì)發(fā)現(xiàn),我們熱部署的策略是整個(gè)上下文context都替換成新的,那么用戶的狀態(tài)也將無法保留。而實(shí)際情況是我們只需要?jiǎng)討B(tài)更新某些模塊的功能,而不是全局。這個(gè)其實(shí)也好辦,就是我們從業(yè)務(wù)上把需要熱部署的由自定義類加載器加載,而持久化的類資源則由系統(tǒng)默認(rèn)類加載器去完成。

  • 自動(dòng)加載類加載器
其實(shí)設(shè)計(jì)到代碼設(shè)計(jì)優(yōu)雅問題,基本上我們拿出設(shè)計(jì)模式23章經(jīng)對(duì)號(hào)入座基本可以解決問題,畢竟這是前人經(jīng)過千萬(wàn)實(shí)踐錘煉出來的軟件構(gòu)建內(nèi)功心法。那么針對(duì)我們熱部署的場(chǎng)景,如果想把熱部署細(xì)節(jié)封裝出來,那代理模式無疑是最符合要求的,也就是咱們弄出個(gè)代理對(duì)象來面向用戶,把類加載器的更替,回收,隔離等細(xì)節(jié)都放在代理對(duì)象里面完成,而對(duì)于用戶來說是透明無感知的,那么終端用戶體驗(yàn)起來就是純粹的熱部署了。至于如何實(shí)現(xiàn)自動(dòng)熱部署,方式也很簡(jiǎn)單,監(jiān)聽我們部署的目錄,如果文件時(shí)間和大小發(fā)生變化,則判斷應(yīng)用需要更新,這時(shí)候就觸發(fā)類加載器的創(chuàng)建和舊對(duì)象的回收,這個(gè)時(shí)候也可以引入觀察者模式來實(shí)現(xiàn)。由于篇幅限制,本例子就留給讀者朋友自行設(shè)計(jì),相信也是不難完成的。
案例

上一節(jié)我們深入淺出的從自定義類加載器的開始引入,到實(shí)現(xiàn)多個(gè)類加載器加載同個(gè)類文件,最后完成舊類加載器和對(duì)象的回收,整個(gè)流程闡述了熱部署的實(shí)現(xiàn)細(xì)節(jié)。那么這一節(jié)我們介紹現(xiàn)有實(shí)現(xiàn)熱部署的通用解決方案,本質(zhì)就是對(duì)上文原理的實(shí)現(xiàn),加上性能和設(shè)計(jì)上的優(yōu)化,注意本節(jié)我們應(yīng)用的只是類加載器的技術(shù),后面章節(jié)還會(huì)介紹的字節(jié)碼層面的底層操作技術(shù)。

  • OSGI
OSGI(Open Service Gateway Initiative)是一套開發(fā)和部署應(yīng)用程序的java框架。我們從官網(wǎng)可以看到OSGI其實(shí)是一套規(guī)范,好比Servlet定義了服務(wù)端對(duì)于處理來自網(wǎng)絡(luò)請(qǐng)求的一套規(guī)范,比如init,service,destroy的生命周期。然后我們通過實(shí)行這套規(guī)范來實(shí)現(xiàn)與客戶端的交互,在調(diào)用init初始化完Servlet對(duì)象后通過多線程模式使用service響應(yīng)網(wǎng)絡(luò)請(qǐng)求。如果從響應(yīng)模式比較我們還可以了解下Webflux的規(guī)范,以上兩種都是處理網(wǎng)絡(luò)請(qǐng)求的方式,當(dāng)然你舉例說CGI也是一種處理網(wǎng)絡(luò)請(qǐng)求的規(guī)范,CGI采用的是多進(jìn)程方式來處理網(wǎng)絡(luò)請(qǐng)求,我們暫時(shí)不對(duì)這兩種規(guī)范進(jìn)行優(yōu)劣評(píng)價(jià),只是說明在處理網(wǎng)絡(luò)請(qǐng)求的場(chǎng)景下可以采用不同的規(guī)范來實(shí)現(xiàn)。
好了現(xiàn)在回到OSGi,有了上面的鋪墊,相信對(duì)我們理解OSGI大有幫助。我們說OSGI首先是一種規(guī)范,既然是規(guī)范我們就要看看都規(guī)范了啥,比如Servlet也是一種規(guī)范,它規(guī)范了生命周期,規(guī)定應(yīng)用容器中WEB-INF/classes目錄或WEB-INF/lib目錄下的jar包才會(huì)被Web容器處理。同樣OSGI的實(shí)現(xiàn)框架對(duì)管轄的Bundle下面的目錄組織和文本格式也有嚴(yán)格規(guī)范,更重要的是OSGI對(duì)模塊化架構(gòu)生命周期的管理。而模塊化也不只是把系統(tǒng)拆分成不同的JAR包形成模塊而已,真正的模塊化必須將模塊中類的引入/導(dǎo)出、隱藏、依賴、版本管理貫穿到生命周期管理中去。
定義:OSGI是脫胎于(OSGI Alliance)技術(shù)聯(lián)盟由一組規(guī)范和對(duì)應(yīng)子規(guī)范共同定義的JAVA動(dòng)態(tài)模塊化技術(shù)。實(shí)現(xiàn)該規(guī)范的OSGI框架(如Apache Felix)使應(yīng)用程序的模塊能夠在本地或者網(wǎng)絡(luò)中實(shí)現(xiàn)端到端的通信,目前已經(jīng)發(fā)布了第7版。OSGI有很多優(yōu)點(diǎn)諸如熱部署,類隔離,高內(nèi)聚,低耦合的優(yōu)勢(shì),但同時(shí)也帶來了性能損耗,而且基于OSGI目前的規(guī)范繁多復(fù)雜,開發(fā)門檻較高。
組成:執(zhí)行環(huán)境,安全層,模塊層,生命周期層,服務(wù)層,框架API
核心服務(wù):
事件服務(wù)(Event Admin Service),
包管理服務(wù)(Package Admin Service)
日志服務(wù)(Log Service)
配置管理服務(wù)(Configuration Admin Service)
HTTP服務(wù)(HTTP Service)
用戶管理服務(wù)(User Admin Service)
設(shè)備訪問服務(wù)(Device Access Service)
IO連接器服務(wù)(IO Connector Service)
聲明式服務(wù)(Declarative Services)
其他OSGi標(biāo)準(zhǔn)服務(wù)



本節(jié)我們討論的核心是熱部署,所以我們不打算在這里講解全部得OSGI技術(shù),在上文實(shí)現(xiàn)熱部署后我們重點(diǎn)來剖析OSGI關(guān)于熱部署的機(jī)制。至于OSGI模塊化技術(shù)和java9的模塊化的對(duì)比和關(guān)聯(lián),后面有時(shí)間會(huì)開個(gè)專題專門介紹模塊化技術(shù)。
從類加載器技術(shù)應(yīng)用的角度切入我們知道OSGI規(guī)范也是打破雙親委派機(jī)制,除了框架層面需要依賴JVM默認(rèn)類加載器之外,其他Bundle(OSGI定義的模塊單元)都是由各自的類加載器來加載,而OSGI框架就負(fù)責(zé)模塊生命周期,模塊交互這些核心功能,同時(shí)創(chuàng)建各個(gè)Bundle的類加載器,用于直接加載Bundle定義的jar包。由于打破雙親委派模式,Bundle類加載器不再是雙親委派模型中的樹狀結(jié)構(gòu),而是進(jìn)一步發(fā)展為更加復(fù)雜的網(wǎng)狀結(jié)構(gòu)(因?yàn)楦鱾€(gè)Bundle之間有相互依賴關(guān)系),當(dāng)收到類加載請(qǐng)求時(shí),OSGi將按照下面的順序進(jìn)行類搜索:
1)將以java.*開頭的類委派給父類加載器加載。
2)否則,將委派列表名單內(nèi)(比如sun或者javax這類核心類的包加入白名單)的類委派給父類加載器加載。
3)否則,將Import列表中的類委派給Export這個(gè)類的Bundle的類加載器加載。
4)否則,查找當(dāng)前Bundle的ClassPath,使用自己的類加載器加載。
5)否則,查找類是否在自己的Fragment Bundle(OSGI框架緩存包)中,如果在,則委派給Fragment Bundle的類加載器加載。
6)否則,查找Dynamic Import列表的Bundle,委派給對(duì)應(yīng)Bundle的類加載器加載。
7)否則,類查找失敗。
這一系列的類加載操作,其實(shí)跟我們上節(jié)實(shí)現(xiàn)的自定義類加載技術(shù)本質(zhì)上是一樣的,只不過實(shí)現(xiàn)OSGI規(guī)范的框架需要提供模塊之間的注冊(cè)通信組件,還有模塊的生命周期管理,版本管理。OSGI也只是JVM上面運(yùn)行的一個(gè)普通應(yīng)用實(shí)例,只不過通過模塊內(nèi)聚,版本管理,服務(wù)依賴一系列的管理,實(shí)現(xiàn)了模塊的即時(shí)更新,實(shí)現(xiàn)了熱部署。
其他熱部署解決方案多數(shù)也是利用類加載器的特點(diǎn)做文章,當(dāng)然不止是類加載器,還會(huì)應(yīng)用字節(jié)碼技術(shù),下面我們主要簡(jiǎn)單列舉應(yīng)用類加載器實(shí)現(xiàn)的熱部署解決方案。

  • Groovy
Groovy兼顧動(dòng)態(tài)腳本語(yǔ)言的功能,使用的時(shí)候無外乎也是通過GroovyClassLoader來加載腳本文件,轉(zhuǎn)為JVM的類對(duì)象。那么每次更新groovy腳本就可以動(dòng)態(tài)更新應(yīng)用,也就達(dá)到了熱部署的功能了。
Class groovyClass = classLoader.parseClass(new GroovyCodeSource(sourceFile));
GroovyObject instance = (GroovyObject)groovyClass.newInstance();//proxy

  • Clojure
  • JSP
    JSP其實(shí)翻譯為Servlet后也是由對(duì)應(yīng)新的類加載器去加載,這跟我們上節(jié)講的流程一模一樣,所以這里就補(bǔ)展開講解了。
介紹完熱部署技術(shù),可能很多同學(xué)對(duì)熱部署的需求已經(jīng)沒有那么強(qiáng)烈,畢竟熱部署過程中帶來的弊端也不容忽視,比如替換舊的類加載器過程會(huì)產(chǎn)生大量的內(nèi)存碎片,導(dǎo)致JVM進(jìn)行高負(fù)荷的GC工作,反復(fù)進(jìn)行熱部署還會(huì)導(dǎo)致JVM內(nèi)存不足而導(dǎo)致內(nèi)存溢出,有時(shí)候甚至還不如直接重啟應(yīng)用來得更快一點(diǎn),而且隨著分布式架構(gòu)的演進(jìn)和微服務(wù)的流行,應(yīng)用重啟也早就實(shí)現(xiàn)服務(wù)編排化,配合豐富的部署策略,也可以同樣保證系統(tǒng)穩(wěn)定持續(xù)服務(wù),我們更多的是通過熱部署技術(shù)來深刻認(rèn)識(shí)到JVM加載類的技術(shù)演進(jìn)。
4.3.2類隔離

背景

先介紹一下類隔離的背景,我們費(fèi)了那么大的勁設(shè)計(jì)出類加載器,如果只是用于加載外部類字節(jié)流那就過于浪費(fèi)了。通常我們的應(yīng)用依賴不同的第三方類庫(kù)經(jīng)常會(huì)出現(xiàn)不同版本的類庫(kù),如果只是使用系統(tǒng)內(nèi)置的類加載器的話,那么一個(gè)類庫(kù)只能加載唯一的一個(gè)版本,想加載其他版本的時(shí)候會(huì)從緩存里面發(fā)現(xiàn)已經(jīng)存在而停止加載。但是我們的不同業(yè)務(wù)以來的往往是不同版本的類庫(kù),這時(shí)候就會(huì)出現(xiàn)ClassNotFoundException。為什么只有運(yùn)行的是才會(huì)出現(xiàn)這個(gè)異常呢,因?yàn)榫幾g的時(shí)候我們通常會(huì)使用MAVEN等編譯工具把沖突的版本排除掉。另外一種情況是WEB容器的內(nèi)核依賴的第三方類庫(kù)需要跟應(yīng)用依賴的第三方類庫(kù)隔離開來,避免一些安全隱患,不然如果共用的話,應(yīng)用升級(jí)依賴版本就會(huì)導(dǎo)致WEB容器不穩(wěn)定。
基于以上的介紹我們知道類隔離實(shí)在是剛需,那么接下來介紹一下如何實(shí)現(xiàn)這個(gè)剛需。
原理

首先我們要了解一下原理,其實(shí)原理很簡(jiǎn)單,真的很簡(jiǎn)單,請(qǐng)?jiān)试S我總結(jié)為“唯一標(biāo)識(shí)原理”。我們知道內(nèi)存里面定位類實(shí)例的坐標(biāo)<類加載器,類全限定名>。那么由這兩個(gè)因子組合起來我們可以得出一種普遍的應(yīng)用,用不同類加載器來加載類相同類(類全限定名一致,版本不一致)是可以實(shí)現(xiàn)的,也就是在JVM看來,有相同類全名的類是完全不同的兩個(gè)實(shí)例,但是在業(yè)務(wù)視角我們卻可以視為相同的類。
public static void main(String[] args) {
   Class<?> userClass1 = User.class;
   Class<?> userClass2 = new DynamicClassLoader("target/classes")
         .load("qj.blog.classreloading.example1.StaticInt$User");

   out.println("Seems to be the same class:");
   out.println(userClass1.getName());
   out.println(userClass2.getName());
   out.println();

   out.println("But why there are 2 different class loaders:");
   out.println(userClass1.getClassLoader());
   out.println(userClass2.getClassLoader());
   out.println();

   User.age = 11;
   out.println("And different age values:");
   out.println((int) ReflectUtil.getStaticFieldValue("age", userClass1));
   out.println((int) ReflectUtil.getStaticFieldValue("age", userClass2));
}

public static class User {
   public static int age = 10;
}
實(shí)現(xiàn)

原理很簡(jiǎn)單,比如我們知道Spring容器本質(zhì)就是一個(gè)生產(chǎn)和管理bean的集合對(duì)象,但是卻包含了大量的優(yōu)秀設(shè)計(jì)模式和復(fù)雜的框架實(shí)現(xiàn)。同理隔離容器雖然原理很簡(jiǎn)單,但是要實(shí)現(xiàn)一個(gè)高性能可擴(kuò)展的高可用隔離容器,卻不是那么簡(jiǎn)單。我們上文談的場(chǎng)景是在內(nèi)存運(yùn)行的時(shí)候才發(fā)現(xiàn)問題,介紹內(nèi)存隔離技術(shù)之前,我們先普及更為通用的沖突解決方法。

  • 沖突排除
    沖突總是先發(fā)生在編譯時(shí)期,那么基本Maven工具可以幫我們完成大部分的工作,Maven的工作模式就是將我們第三方類庫(kù)的所有依賴都依次檢索,最終排除掉產(chǎn)生沖突的jar包版本。
  • 沖突適配
    當(dāng)我們無法通過簡(jiǎn)單的排除來解決的時(shí)候,另外一個(gè)方法就是重新裝配第三方類庫(kù),這里我們要介紹一個(gè)開源工具jarjar (https://github.com/shevek/jarjar)。該工具包可以通過字節(jié)碼技術(shù)將我們依賴的第三方類庫(kù)重命名,同時(shí)修改代碼里面對(duì)第三方類庫(kù)引用的路徑。這樣如果出現(xiàn)同名第三方類庫(kù)的話,通過該“硬編碼”的方式修改其中一個(gè)類庫(kù),從而消除了沖突。
  • 沖突隔離
    上面兩種方式在小型系統(tǒng)比較適合,也比較敏捷高效。但是對(duì)于分布式大型系統(tǒng)的話,通過硬編碼方式來解決沖突就難以完成了。辦法就是通過隔離容器,從邏輯上區(qū)分類庫(kù)的作用域,從而對(duì)內(nèi)存的類進(jìn)行隔離。
5.內(nèi)存管理

5.1內(nèi)存結(jié)構(gòu)

5.1.1邏輯分區(qū)

JVM內(nèi)存從應(yīng)用邏輯上可分為如下區(qū)域。

  • 程序計(jì)數(shù)器:字節(jié)碼行號(hào)指示器,每個(gè)線程需要一個(gè)程序計(jì)數(shù)器
  • 虛擬機(jī)棧:方法執(zhí)行時(shí)創(chuàng)建棧幀(存儲(chǔ)局部變量,操作棧,動(dòng)態(tài)鏈接,方法出口)編譯時(shí)期就能確定占用空間大小,線程請(qǐng)求的棧深度超過jvm運(yùn)行深度時(shí)拋StackOverflowError,當(dāng)jvm棧無法申請(qǐng)到空閑內(nèi)存時(shí)拋OutOfMemoryError,通過-Xss,-Xsx來配置初始內(nèi)存
  • 本地方法棧:執(zhí)行本地方法,如操作系統(tǒng)native接口
  • 堆:存放對(duì)象的空間,通過-Xmx,-Xms配置堆大小,當(dāng)堆無法申請(qǐng)到內(nèi)存時(shí)拋OutOfMemoryError
  • 方法區(qū):存儲(chǔ)類數(shù)據(jù),常量,常量池,靜態(tài)變量,通過MaxPermSize參數(shù)配置
  • 對(duì)象訪問:初始化一個(gè)對(duì)象,其引用存放于棧幀,對(duì)象存放于堆內(nèi)存,對(duì)象包含屬性信息和該對(duì)象父類、接口等類型數(shù)據(jù)(該類型數(shù)據(jù)存儲(chǔ)在方法區(qū)空間,對(duì)象擁有類型數(shù)據(jù)的地址)
而實(shí)際上JVM內(nèi)存分類實(shí)際上的物理分區(qū)還有更為詳細(xì),整體上分為堆內(nèi)存和非堆內(nèi)存,具體介紹如下。
5.1.2 內(nèi)存模型

堆內(nèi)存

堆內(nèi)存是運(yùn)行時(shí)的數(shù)據(jù)區(qū),從中分配所有java類實(shí)例和數(shù)組的內(nèi)存,可以理解為目標(biāo)應(yīng)用依賴的對(duì)象。堆在JVM啟動(dòng)時(shí)創(chuàng)建,并且在應(yīng)用程序運(yùn)行時(shí)可能會(huì)增大或減小??梢允褂?Xms 選項(xiàng)指定堆的大小。堆可以是固定大小或可變大小,具體取決于垃圾收集策略??梢允褂?Xmx選項(xiàng)設(shè)置最大堆大小。默認(rèn)情況下,最大堆大小設(shè)置為64 MB。
JVM堆內(nèi)存在物理上分為兩部分:新生代和老年代。新生代是為分配新對(duì)象而保留堆空間。當(dāng)新生代占用完時(shí),Minor GC垃圾收集器會(huì)對(duì)新生代區(qū)域執(zhí)行垃圾回收動(dòng)作,其中在新生代中生活了足夠長(zhǎng)的所有對(duì)象被遷移到老年代,從而釋放新生代空間以進(jìn)行更多的對(duì)象分配。此垃圾收集稱為 Minor GC。新生代分為三個(gè)子區(qū)域:伊甸園Eden區(qū)和兩個(gè)幸存區(qū)S0和S1。



關(guān)于新生代內(nèi)存空間:

  • 大多數(shù)新創(chuàng)建的對(duì)象都位于Eden區(qū)內(nèi)存空間
  • 當(dāng)Eden區(qū)填滿對(duì)象時(shí),執(zhí)行Minor GC并將所有幸存對(duì)象移動(dòng)到其中一個(gè)幸存區(qū)空間
  • Minor GC還會(huì)檢查幸存區(qū)對(duì)象并將其移動(dòng)到其他幸存者空間,也即是幸存區(qū)總有一個(gè)是空的
  • 在多次GC后還存活的對(duì)象被移動(dòng)到老年代內(nèi)存空間。至于經(jīng)過多少次GC晉升老年代則由參數(shù)配置,通常為15
當(dāng)老年區(qū)填滿時(shí),老年區(qū)同樣會(huì)執(zhí)行垃圾回收,老年區(qū)還包含那些經(jīng)過多Minor GC后還存活的長(zhǎng)壽對(duì)象。垃圾收集器在老年代內(nèi)存中執(zhí)行的回收稱為Major GC,通常需要更長(zhǎng)的時(shí)間。
非堆內(nèi)存

JVM的堆以外內(nèi)存稱為非堆內(nèi)存。也即是JVM自身預(yù)留的內(nèi)存區(qū)域,包含JVM緩存空間,類結(jié)構(gòu)如常量池、字段和方法數(shù)據(jù),方法,構(gòu)造方法。類非堆內(nèi)存的默認(rèn)最大大小為64 MB??梢允褂?XX:MaxPermSize VM選項(xiàng)更改此選項(xiàng),非堆內(nèi)存通常包含如下性質(zhì)的區(qū)域空間:

  • 元空間(Metaspace)
在Java 8以上版本已經(jīng)沒有Perm Gen這塊區(qū)域了,這也意味著不會(huì)再由關(guān)于“java.lang.OutOfMemoryError:PermGen”內(nèi)存問題存在了。與駐留在Java堆中的Perm Gen不同,Metaspace不是堆的一部分。類元數(shù)據(jù)多數(shù)情況下都是從本地內(nèi)存中分配的。默認(rèn)情況下,元空間會(huì)自動(dòng)增加其大小(直接又底層操作系統(tǒng)提供),而Perm Gen始終具有固定的上限??梢允褂脙蓚€(gè)新標(biāo)志來設(shè)置Metaspace的大小,它們是:“ - XX:MetaspaceSize ”和“ -XX:MaxMetaspaceSize ”。Metaspace背后的含義是類的生命周期及其元數(shù)據(jù)與類加載器的生命周期相匹配。也就是說,只要類加載器處于活動(dòng)狀態(tài),元數(shù)據(jù)就會(huì)在元數(shù)據(jù)空間中保持活動(dòng)狀態(tài),并且無法釋放。

  • 代碼緩存
運(yùn)行Java程序時(shí),它以分層方式執(zhí)行代碼。在第一層,它使用客戶端編譯器(C1編譯器)來編譯代碼。分析數(shù)據(jù)用于服務(wù)器編譯的第二層(C2編譯器),以優(yōu)化的方式編譯該代碼。默認(rèn)情況下,Java 7中未啟用分層編譯,但在Java 8中啟用了分層編譯。實(shí)時(shí)(JIT)編譯器將編譯的代碼存儲(chǔ)在稱為代碼緩存的區(qū)域中。它是一個(gè)保存已編譯代碼的特殊堆。如果該區(qū)域的大小超過閾值,則該區(qū)域?qū)⒈凰⑿拢⑶褿C不會(huì)重新定位這些對(duì)象。Java 8中已經(jīng)解決了一些性能問題和編譯器未重新啟用的問題,并且在Java 7中避免這些問題的解決方案之一是將代碼緩存的大小增加到一個(gè)永遠(yuǎn)不會(huì)達(dá)到的程度。

  • 方法區(qū)
方法區(qū)域是Perm Gen中空間的一部分,用于存儲(chǔ)類結(jié)構(gòu)(運(yùn)行時(shí)常量和靜態(tài)變量)以及方法和構(gòu)造函數(shù)的代碼。

  • 內(nèi)存池
內(nèi)存池由JVM內(nèi)存管理器創(chuàng)建,用于創(chuàng)建不可變對(duì)象池。內(nèi)存池可以屬于Heap或Perm Gen,具體取決于JVM內(nèi)存管理器實(shí)現(xiàn)。

  • 常量池
常量包含類運(yùn)行時(shí)常量和靜態(tài)方法,常量池是方法區(qū)域的一部分。

  • Java堆棧內(nèi)存
Java堆棧內(nèi)存用于執(zhí)行線程。它們包含特定于方法的特定值,以及對(duì)從該方法引用的堆中其他對(duì)象的引用。

  • Java堆內(nèi)存配置項(xiàng)
Java提供了許多內(nèi)存配置項(xiàng),我們可以使用它們來設(shè)置內(nèi)存大小及其比例,常用的如下:
VM Switch描述
- Xms用于在JVM啟動(dòng)時(shí)設(shè)置初始堆大小
-Xmx用于設(shè)置最大堆大小
-Xmn設(shè)置新生區(qū)的大小,剩下的空間用于老年區(qū)
-XX:PermGen用于設(shè)置永久區(qū)存初始大小
-XX:MaxPermGen用于設(shè)置Perm Gen的最大尺寸
-XX:SurvivorRatio提供Eden區(qū)域的比例
-XX:NewRatio用于提供老年代/新生代大小的比例,默認(rèn)值為2
5.2垃圾回收

5.2.1垃圾回收策略

流程

垃圾收集是釋放堆中的空間以分配新對(duì)象的過程。垃圾收集器是JVM管理的進(jìn)程,它可以查看內(nèi)存中的所有對(duì)象,并找出程序任何部分未引用的對(duì)象,刪除并回收空間以分配給其他對(duì)象。通常會(huì)經(jīng)過如下步驟:

  • 標(biāo)記:標(biāo)記哪些對(duì)象被使用,哪些已經(jīng)是無法觸達(dá)的無用對(duì)象
  • 刪除:刪除無用對(duì)象并回收要分配給其他對(duì)象
  • 壓縮:性能考慮,在刪除無用的對(duì)象后,會(huì)將所有幸存對(duì)象集中移動(dòng)到一起,騰出整段空間
策略

虛擬機(jī)棧、本地棧和程序計(jì)數(shù)器在編譯完畢后已經(jīng)可以確定所需內(nèi)存空間,程序執(zhí)行完畢后也會(huì)自動(dòng)釋放所有內(nèi)存空間,所以不需要進(jìn)行動(dòng)態(tài)回收優(yōu)化。JVM內(nèi)存調(diào)優(yōu)主要針對(duì)堆和方法區(qū)兩大區(qū)域的內(nèi)存。通常對(duì)象分為Strong、sfot、weak和phantom四種類型,強(qiáng)引用不會(huì)被回收,軟引用在內(nèi)存達(dá)到溢出邊界時(shí)回收,弱引用在每次回收周期時(shí)回收,虛引用專門被標(biāo)記為回收對(duì)象,具體回收策略如下:

  • 對(duì)象優(yōu)先在Eden區(qū)分配:
  • 新生對(duì)象回收策略Minor GC(頻繁)
  • 老年代對(duì)象回收策略Full GC/Major GC(慢)
  • 大對(duì)象直接進(jìn)入老年代:超過3m的對(duì)象直接進(jìn)入老年區(qū) -XX:PretenureSizeThreshold=3145728(3M)
  • 長(zhǎng)期存貨對(duì)象進(jìn)入老年區(qū):
    Survivor區(qū)中的對(duì)象經(jīng)歷一次Minor GC年齡增加一歲,超過15歲進(jìn)入老年區(qū)
    -XX:MaxTenuringThreshold=15
  • 動(dòng)態(tài)對(duì)象年齡判定:設(shè)置Survivor區(qū)對(duì)象占用一半空間以上的對(duì)象進(jìn)入老年區(qū)
算法

垃圾收集有如下常用的算法:

  • 標(biāo)記-清除
  • 復(fù)制
  • 標(biāo)記-整理
  • 分代收集(新生用復(fù)制,老年用標(biāo)記-整理)
5.2.2 垃圾回收器

分類


  • serial收集器:?jiǎn)尉€程,主要用于client模式
  • ParNew收集器:多線程版的serial,主要用于server模式
  • Parallel Scavenge收集器:線程可控吞吐量(用戶代碼時(shí)間/用戶代碼時(shí)間+垃圾收集時(shí)間),自動(dòng)調(diào)節(jié)吞吐量,用戶新生代內(nèi)存區(qū)
  • Serial Old收集器:老年版本serial
  • Parallel Old收集器:老年版本Parallel Scavenge
  • CMS(Concurrent Mark Sweep)收集器:停頓時(shí)間短,并發(fā)收集
  • G1收集器:分塊標(biāo)記整理,不產(chǎn)生碎片
配置


  • 串行GC(-XX:+ UseSerialGC):串行GC使用簡(jiǎn)單的標(biāo)記-掃描-整理方法,用于新生代和老年代的垃圾收集,即Minor和Major GC
  • 并行GC(-XX:+ UseParallelGC):并行GC與串行GC相同,不同之處在于它為新生代垃圾收集生成N個(gè)線程,其中N是系統(tǒng)中的CPU核心數(shù)。我們可以使用-XX:ParallelGCThreads = n JVM選項(xiàng)來控制線程數(shù)
  • 并行舊GC(-XX:+ UseParallelOldGC):這與Parallel GC相同,只是它為新生代和老年代垃圾收集使用多個(gè)線程
  • 并發(fā)標(biāo)記掃描(CMS)收集器(-XX:+ UseConcMarkSweepGC):CMS也稱為并發(fā)低暫停收集器。它為老年代做垃圾收集。CMS收集器嘗試通過在應(yīng)用程序線程內(nèi)同時(shí)執(zhí)行大多數(shù)垃圾收集工作來最小化由于垃圾收集而導(dǎo)致的暫停。年輕一代的CMS收集器使用與并行收集器相同的算法。我們可以使用-XX限制CMS收集器中的線程數(shù) :ParallelCMSThreads = n
  • G1垃圾收集器(-XX:+ UseG1GC):G1從長(zhǎng)遠(yuǎn)看要是替換CMS收集器。G1收集器是并行,并發(fā)和遞增緊湊的低暫停垃圾收集器。G1收集器不像其他收集器那樣工作,并且沒有年輕和老一代空間的概念。它將堆空間劃分為多個(gè)大小相等的堆區(qū)域。當(dāng)調(diào)用垃圾收集器時(shí),它首先收集具有較少實(shí)時(shí)數(shù)據(jù)的區(qū)域,因此稱為“Garbage First”也即是G1
6.執(zhí)行引擎

6.1執(zhí)行流程

類加載器加載的類文件字節(jié)碼數(shù)據(jù)流由基于JVM指令集架構(gòu)的執(zhí)行引擎來執(zhí)行。執(zhí)行引擎以指令為單位讀取Java字節(jié)碼。我們知道匯編執(zhí)行的流程是CPU執(zhí)行每一行的匯編指令,同樣JVM執(zhí)行引擎就像CPU一個(gè)接一個(gè)地執(zhí)行機(jī)器命令。字節(jié)碼的每個(gè)命令都包含一個(gè)1字節(jié)的OpCode和附加的操作數(shù)。執(zhí)行引擎獲取一個(gè)OpCode并使用操作數(shù)執(zhí)行任務(wù),然后執(zhí)行下一個(gè)OpCode。但Java是用人們可以理解的語(yǔ)言編寫的,而不是用機(jī)器直接執(zhí)行的語(yǔ)言編寫的。因此執(zhí)行引擎必須將字節(jié)碼更改為JVM中的機(jī)器可以執(zhí)行的語(yǔ)言。字節(jié)碼可以通過以下兩種方式之一轉(zhuǎn)化為合適的語(yǔ)言。

  • 解釋器:逐個(gè)讀取,解釋和執(zhí)行字節(jié)碼指令。當(dāng)它逐個(gè)解釋和執(zhí)行指令時(shí),它可以快速解釋一個(gè)字節(jié)碼,但是同時(shí)也只能相對(duì)緩慢的地執(zhí)行解釋結(jié)果,這是解釋語(yǔ)言的缺點(diǎn)。
  • JIT(實(shí)時(shí))編譯器:引入了JIT編譯器來彌補(bǔ)解釋器的缺點(diǎn)。執(zhí)行引擎首先作為解釋器運(yùn)行,并在適當(dāng)?shù)臅r(shí)候,JIT編譯器編譯整個(gè)字節(jié)碼以將其更改為本機(jī)代碼。之后,執(zhí)行引擎不再解釋該方法,而是直接使用本機(jī)代碼執(zhí)行。本地代碼中的執(zhí)行比逐個(gè)解釋指令要快得多。由于本機(jī)代碼存儲(chǔ)在高速緩存中,因此可以快速執(zhí)行編譯的代碼。
但是,JIT編譯器編譯代碼需要花費(fèi)更多的時(shí)間,而不是解釋器逐個(gè)解釋代碼。因此,如果代碼只執(zhí)行一次,最好是選擇解釋而不是編譯。因此,使用JIT編譯器的JVM在內(nèi)部檢查方法執(zhí)行的頻率,并僅在頻率高于某個(gè)級(jí)別時(shí)編譯方法。



JVM規(guī)范中未定義執(zhí)行引擎的運(yùn)行方式。因此,JVM廠商使用各種技術(shù)改進(jìn)其執(zhí)行引擎,并引入各種類型的JIT編譯器。 大多數(shù)JIT編譯器運(yùn)行如下圖所示:



JIT編譯器將字節(jié)碼轉(zhuǎn)換為中間級(jí)表達(dá)式IR,以執(zhí)行優(yōu)化,然后將表達(dá)式轉(zhuǎn)換為本機(jī)代碼。Oracle Hotspot VM使用名為Hotspot Compiler的JIT編譯器。它被稱為Hotspot,因?yàn)镠otspot Compiler通過分析搜索需要以最高優(yōu)先級(jí)進(jìn)行編譯的“Hotspot”,然后將熱點(diǎn)編譯為本機(jī)代碼。如果不再頻繁調(diào)用編譯了字節(jié)碼的方法,換句話說,如果該方法不再是熱點(diǎn),則Hotspot VM將從緩存中刪除本機(jī)代碼并以解釋器模式運(yùn)行。Hotspot VM分為服務(wù)器VM和客戶端VM,兩個(gè)VM使用不同的JIT編譯器。
大多數(shù)Java性能改進(jìn)都是通過改進(jìn)執(zhí)行引擎來實(shí)現(xiàn)的。除了JIT編譯器之外,還引入了各種優(yōu)化技術(shù),因此可以不斷改進(jìn)JVM性能。初始JVM和最新JVM之間的最大區(qū)別是執(zhí)行引擎。
下面我們通過下圖可以看出JAVA執(zhí)行的流程。



6.2棧幀結(jié)構(gòu)

每個(gè)方法調(diào)用開始到執(zhí)行完成的過程,對(duì)應(yīng)這一個(gè)棧幀在虛擬機(jī)棧里面從入棧到出棧的過程。

  • 棧幀包含:局部變量表,操作數(shù)棧,動(dòng)態(tài)連接,方法返回
  • 方法調(diào)用:方法調(diào)用不等于方法執(zhí)行,而且確定調(diào)用方法的版本。
  • 方法調(diào)用字節(jié)碼指令:invokestatic,invokespecial,invokevirtual,invokeinterface
  • 靜態(tài)分派:靜態(tài)類型,實(shí)際類型,編譯器重載時(shí)通過參數(shù)的靜態(tài)類型來確定方法的版本。(選方法)
  • 動(dòng)態(tài)分派:invokevirtual指令把類方法符號(hào)引用解析到不同直接引用上,來確定棧頂?shù)膶?shí)際對(duì)象(選對(duì)象)
  • 單分派:靜態(tài)多分派,相同指令有多個(gè)方法版本。
  • 多分派:動(dòng)態(tài)單分派,方法接受者只能確定唯一一個(gè)。
下圖是JVM實(shí)例執(zhí)行方法是的內(nèi)存布局。



6.3早期編譯


  • javac編譯器:解析與符號(hào)表填充,注解處理,生成字節(jié)碼
  • java語(yǔ)法糖:語(yǔ)法糖有助于代碼開發(fā),但是編譯后就會(huì)解開糖衣,還原到基礎(chǔ)語(yǔ)法的class二進(jìn)制文件
    重載要求方法具備不同的特征簽名(不包括返回值),但是class文件中,只要描述不是完全一致的方法就可以共存。
6.4晚期編譯

HotSpot虛擬機(jī)內(nèi)的即時(shí)編譯
解析模式 -Xint
編譯模式 -Xcomp
混合模式 mixed mode
分層編譯:解釋執(zhí)行 -> C1(Client Compiler)編譯 -> C2編譯(Server Compiler)
觸發(fā)條件:基于采樣的熱點(diǎn)探測(cè),基于計(jì)數(shù)器的熱點(diǎn)探測(cè)
7.性能調(diào)優(yōu)

7.1調(diào)優(yōu)原則

我們知道調(diào)優(yōu)的前提是,程序沒有達(dá)到我們的預(yù)期要求,那么第一步要做的是衡量我們的預(yù)期。程序不可能十全十美,我們要做的是通過各種指標(biāo)來衡量系統(tǒng)的性能,最終整體達(dá)到我們的要求。
7.1.1 環(huán)境

首先我們要了解系統(tǒng)的運(yùn)行環(huán)境,包括操作系統(tǒng)層面的差異,JVM版本,位數(shù),乃至于硬件的時(shí)鐘周期,總線設(shè)計(jì)甚至機(jī)房溫度,都可能是我們需要考慮的前置條件。
7.1.2 度量

首先我們要先給出系統(tǒng)的預(yù)期指標(biāo),在特定的硬件/軟件的配置,然后給出目標(biāo)指標(biāo),比如系統(tǒng)整體輸出接口的QPS,RT,或者更進(jìn)一層,IO讀寫,cpu的load指標(biāo),內(nèi)存的使用率,GC情況都是我們需要預(yù)先考察的對(duì)象。
7.1.3 監(jiān)測(cè)

確定了環(huán)境前置條件,分析了度量指標(biāo),第三步是通過工具來監(jiān)測(cè)指標(biāo),下一節(jié)提供了常用JVM調(diào)優(yōu)工具,可以通過不同工具的組合來發(fā)現(xiàn)定位問題,結(jié)合JVM的工作機(jī)制已經(jīng)操作系統(tǒng)層面的調(diào)度流程,按圖索驥來發(fā)現(xiàn)問題,找出問題后才能進(jìn)行優(yōu)化。
7.1.4 原則

總體的調(diào)優(yōu)原則如下圖



圖片來源《Java Performance》
7.2 調(diào)優(yōu)參數(shù)

上節(jié)給出了JVM性能調(diào)優(yōu)的原則,我們理清思路后應(yīng)用不同的JVM工具來發(fā)現(xiàn)系統(tǒng)存在的問題,下面列舉的是常用的JVM參數(shù),通過這些參數(shù)指標(biāo)可以更快的幫助我們定位出問題所在。
7.2.1內(nèi)存查詢

最常見的與性能相關(guān)的做法之一是根據(jù)應(yīng)用程序要求初始化堆內(nèi)存。這就是我們應(yīng)該指定最小和最大堆大小的原因。以下參數(shù)可用于實(shí)現(xiàn)它:
-Xms<heap size>[unit] -Xmx<heap size>[unit]
unit表示要初始化內(nèi)存(由堆大小表示)的單元。單位可以標(biāo)記為GB的“g”,MB的“m”和KB的“k”。例如JVM分配最小2 GB和最大5 GB:
-Xms2G -Xmx5G
從Java 8開始Metaspace的大小未被定義,一旦達(dá)到限制JVM會(huì)自動(dòng)增加它,為了避免不必要的不穩(wěn)定性,我們可以設(shè)置Metaspace大?。?br /> -XX:MaxMetaspaceSize=<metaspace size>[unit]
默認(rèn)情況下YG的最小大小為1310 MB,最大大小不受限制,我們可以明確地指定它們:
-XX:NewSize=<young size>[unit]
-XX:MaxNewSize=<young size>[unit]
7.2.2垃圾回收

JVM有四種類型的GC實(shí)現(xiàn):

  • 串行垃圾收集器
  • 并行垃圾收集器
  • CMS垃圾收集器
  • G1垃圾收集器
可以使用以下參數(shù)聲明這些實(shí)現(xiàn):
-XX:+UseSerialGC
-XX:+UseParallelGC
-XX:+USeParNewGC
-XX:+UseG1GC
7.2.3GC記錄

要嚴(yán)格監(jiān)視應(yīng)用程序運(yùn)行狀況,我們應(yīng)始終檢查JVM的垃圾收集性能,使用以下參數(shù),我們可以記錄GC活動(dòng):
-XX:+UseGCLogFileRotation
-XX:NumberOfGCLogFiles=< number of log files >
-XX:GCLogFileSize=< file size >[ unit ]
-Xloggc:/path/to/gc.log
UseGCLogFileRotation指定日志文件滾動(dòng)的政策,就像log4j的,s4lj等 NumberOfGCLogFiles表示單個(gè)應(yīng)用程序記錄生命周期日志文件的最大數(shù)量。GCLogFileSize指定文件的最大大小。 loggc表示其位置。這里要注意的是,還有兩個(gè)可用的JVM參數(shù)(-XX:+ PrintGCTimeStamps-XX:+ PrintGCDateStamps),可用于在GC日志中打印日期時(shí)間戳。
7.2.4內(nèi)存溢出

大型應(yīng)用程序面臨內(nèi)存不足的錯(cuò)誤是很常見的,這是一個(gè)非常關(guān)鍵的場(chǎng)景,很難復(fù)制以解決問題。
這就是JVM帶有一些參數(shù)的原因,這些參數(shù)將堆內(nèi)存轉(zhuǎn)儲(chǔ)到一個(gè)物理文件中,以后可以用它來查找泄漏:
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=./java_pid<pid>.hprof
-XX:OnOutOfMemoryError="< cmd args >;< cmd args >"
-XX:+UseGCOverheadLimit
這里有幾點(diǎn)需要注意:

  • 在OutOfMemoryError的情況下, HeapDumpOnOutOfMemoryError指示JVM將堆轉(zhuǎn)儲(chǔ)到物理文件中
  • HeapDumpPath表示要寫入文件的路徑; 任何文件名都可以給出; 但是如果JVM在名稱中找到 標(biāo)記,則導(dǎo)致內(nèi)存不足錯(cuò)誤的進(jìn)程ID將以 .hprof格式附加到文件名
  • OnOutOfMemoryError用于發(fā)出緊急命令,以便在出現(xiàn)內(nèi)存不足錯(cuò)誤時(shí)執(zhí)行; 應(yīng)該在cmd args的空間中使用正確的命令。例如,如果我們想在內(nèi)存不足時(shí)重新啟動(dòng)服務(wù)器,我們可以設(shè)置參數(shù):
-XX:OnOutOfMemoryError="shutdown -r"

  • UseGCOverheadLimit是一種策略,用于限制在拋出 OutOfMemory錯(cuò)誤之前在GC中花費(fèi)的VM時(shí)間的比例
7.2.5其他配置


  • -server:?jiǎn)⒂谩癝erver Hotspot VM”; 默認(rèn)情況下,此參數(shù)在64位JVM中使用
  • -XX:+ UseStringDeduplication: Java 8引入了這個(gè)JVM參數(shù),通過創(chuàng)建相同 String的太多實(shí)例來減少不必要的內(nèi)存使用 ; 這通過將重復(fù)的 String值減少到單個(gè)全局char []數(shù)組來優(yōu)化堆內(nèi)存
  • -XX:+ UseLWPSynchronization:設(shè)置基于 LWP輕量級(jí)進(jìn)程)的同步策略而不是基于線程的同步
  • -XX:LargePageSizeInBytes:設(shè)置用于Java堆的大頁(yè)面大小; 它采用GB / MB / KB的參數(shù); 通過更大的頁(yè)面大小,我們可以更好地利用虛擬內(nèi)存硬件資源; 但是這可能會(huì)導(dǎo)致 PermGen的空間大小增加,從而可以強(qiáng)制減小Java堆空間的大小
  • -XX:MaxHeapFreeRatio:設(shè)置 GC后堆的最大自由百分比,以避免收縮
  • -XX:MinHeapFreeRatio:設(shè)置 GC后堆的最小自由百分比以避免擴(kuò)展,監(jiān)視堆使用情況
  • -XX:SurvivorRatio:Eden區(qū) /幸存者空間大小的比例
  • -XX:+ UseLargePages:如果系統(tǒng)支持,則使用大頁(yè)面內(nèi)存; 如果使用此JVM參數(shù),OpenJDK 7往往會(huì)崩潰
  • -XX:+ UseStringCache:啟用字符串池中可用的常用分配字符串的緩存
  • -XX:+ UseCompressedStrings:對(duì) String對(duì)象使用 byte []類型,可以用純ASCII格式表示
  • -XX:+ OptimizeStringConcat:它盡可能優(yōu)化字符串連接操作
7.3 調(diào)優(yōu)工具

7.3.1命令行工具


  • 虛擬機(jī)進(jìn)程狀況工具:jps -lvm
  • 診斷命令工具:jcmd
    用來發(fā)送診斷命令請(qǐng)求到JVM,這些請(qǐng)求是控制Java的運(yùn)行記錄,它必須在運(yùn)行JVM的同一臺(tái)機(jī)器上使用,并且具有用于啟動(dòng)JVM的相同有效用戶和分組,可以使用以下命令創(chuàng)建堆轉(zhuǎn)儲(chǔ)(hprof轉(zhuǎn)儲(chǔ)):
    jcmd GC.heap_dump filename =
  • 虛擬機(jī)統(tǒng)計(jì)信息監(jiān)視工具:jstat
    提供有關(guān)運(yùn)行的應(yīng)用程序的性能和資源消耗的信息。在診斷性能問題時(shí),可以使用該工具,特別是與堆大小調(diào)整和垃圾回收相關(guān)的問題。jstat不需要虛擬機(jī)啟動(dòng)任何特殊配置。
    jstat -gc pid interval count
  • java配置信息工具:jinfo
    jinfo -flag pid
  • java內(nèi)存映像工具:jmap
    用于生成堆轉(zhuǎn)儲(chǔ)文件
    jmap -dump:format=b,file=java.bin pid
  • 虛擬機(jī)堆轉(zhuǎn)儲(chǔ)快照分析工具:jhat
    jhat file 分析堆轉(zhuǎn)儲(chǔ)文件,通過瀏覽器訪問分析文件
  • java堆棧跟蹤工具:jstack
    用于生成虛擬機(jī)當(dāng)前時(shí)刻的線程快照threaddump或者Javacore
    jstack [ option ] vmid
  • 堆和CPU分析工具:HPROF
    HPROF是每個(gè)JDK版本附帶的堆和CPU分析工具。它是一個(gè)動(dòng)態(tài)鏈接庫(kù)(DLL),它使用Java虛擬機(jī)工具接口(JVMTI)與JVM連接。該工具將分析信息以ASCII或二進(jìn)制格式寫入文件或套接字。HPROF工具能夠顯示CPU使用情況,堆分配統(tǒng)計(jì)信息和監(jiān)視爭(zhēng)用配置文件。此外,它還可以報(bào)告JVM中所有監(jiān)視器和線程的完整堆轉(zhuǎn)儲(chǔ)和狀態(tài)。在診斷問題方面,HPROF在分析性能,鎖爭(zhēng)用,內(nèi)存泄漏和其他問題時(shí)非常有用。
    java -agentlib:hprof = heap = sites target.class
7.3.2可視化工具


  • jconsole
  • jvisualvm
8.字節(jié)增強(qiáng)

我們從類加載的應(yīng)用介紹了熱部署和類隔離兩大應(yīng)用場(chǎng)景,但是基于類加載器的技術(shù)始終只是獨(dú)立于JVM內(nèi)核功能而存在的,也就是所有實(shí)現(xiàn)都只是基于最基礎(chǔ)的類加載機(jī)制,并無應(yīng)用其他JVM 高級(jí)特性,本章節(jié)我們開始從字節(jié)增強(qiáng)的層面介紹JVM的一些高級(jí)特性。
說到字節(jié)增強(qiáng)我們最先想到的是字節(jié)碼,也就是本文最開頭所要研究的class文件,任何合法的源碼編譯成class后被類加載器加載進(jìn)JVM的方法區(qū),也就是以字節(jié)碼的形態(tài)存活在JVM的內(nèi)存空間。這也就是我們?yōu)槭裁船F(xiàn)有講明白類的結(jié)構(gòu)和加載過程,而字節(jié)碼增強(qiáng)技術(shù)不只是在內(nèi)存里面對(duì)class的字節(jié)碼進(jìn)行操縱,更為復(fù)雜的是class聯(lián)動(dòng)的上下游對(duì)象生命周期的管理。
首先我們回憶一下我們開發(fā)過程中最為熟悉的一個(gè)場(chǎng)景就是本地debug調(diào)試代碼??赡芎芏嗤瑢W(xué)都已經(jīng)習(xí)慣在IDE上對(duì)某句代碼打上斷點(diǎn),然后逐步往下追蹤代碼執(zhí)行的步驟。我們進(jìn)一步想想,這個(gè)是怎么實(shí)現(xiàn)的,是一股什么樣的力量能把已經(jīng)跑起來的線程踩下剎車,一步一步往前挪?我們知道線程運(yùn)行其實(shí)就是在JVM的??臻g上不斷的把代碼對(duì)應(yīng)的JVM指令集不斷的送到CPU執(zhí)行。那能阻止這個(gè)流程的力量也肯定是發(fā)生在JVM范圍內(nèi),所以我們可以很輕松的預(yù)測(cè)到這肯定是JVM提供的機(jī)制,而不是IDE真的有這樣的能力,只不過是JVM把這種能力封裝成接口暴露出去,然后提供給IDE調(diào)用,而IDE只不過是通過界面交互來調(diào)用這些接口而已。那么下面我們就來介紹JVM這種重要的能力。
8.1JPDA

上面所講的JVM提供的程序運(yùn)行斷點(diǎn)能力,其實(shí)JVM提供的一個(gè)工具箱JVMTI(JVM TOOL Interface)提供的接口,而這個(gè)工具箱是一套叫做JPDA的架構(gòu)定義的,本節(jié)我們就來聊聊JPDA。
JPDA(Java Platform Debugger Architecture)Java平臺(tái)調(diào)試架構(gòu),既不是一個(gè)應(yīng)用程序,也不是調(diào)試工具,而是定義了一系列設(shè)計(jì)良好的接口和協(xié)議用于調(diào)試java代碼,我們將會(huì)從三個(gè)層面來講解JPDA。
8.1.1概念


  • JVMTI
    JVMTI(Java Virtual Machine Tool Interface)Java 虛擬機(jī)調(diào)試接口,處于最底層,是我們上文所提到的JVM開放的能力,JPDA規(guī)定了JDK必須提供一個(gè)叫做JVMTI(Java6之前是由JVMPI和JVMDI組成,Java6開始廢棄掉統(tǒng)一為JVMTI)的工具箱,也就是定義了一系列接口能力,比如獲取棧幀、設(shè)置斷點(diǎn)、斷點(diǎn)響應(yīng)等接口,具體開放的能力參考JVMDI官方API文檔。
  • JDWP
    JDWP(Java Debug Wire Protocol)Java 調(diào)試連線協(xié)議,存在在中間層,定義信息格式,定義調(diào)試者和被調(diào)試程序之間請(qǐng)求的協(xié)議轉(zhuǎn)換,位于JDI下一層,JDI更為抽象,JDWP則關(guān)注實(shí)現(xiàn)。也就是說JVM定義好提供的能力,但是如何調(diào)用JVM提供的接口也是需要規(guī)范的,就比如我們Servlet容器也接收正確合法的HTTP請(qǐng)求就可以成功調(diào)用接口。JPDA同樣也規(guī)范了調(diào)用JVMTI接口需要傳入數(shù)據(jù)的規(guī)范,也就是請(qǐng)求包的格式,類別HTTP的數(shù)據(jù)包格式。但是JPDA并不關(guān)心請(qǐng)求來源,也就是說只要調(diào)用JVMTI的請(qǐng)求方式和數(shù)據(jù)格式對(duì)了就可以,不論是來做遠(yuǎn)程調(diào)用還是本地調(diào)用。JDWP制定了調(diào)試者和被調(diào)試應(yīng)用的字節(jié)流動(dòng)機(jī)制,但沒有限定具體實(shí)現(xiàn),可以是遠(yuǎn)程的socket連接,或者本機(jī)的共享內(nèi)存,當(dāng)然還有自定義實(shí)現(xiàn)的通信協(xié)議。既然只是規(guī)范了調(diào)用協(xié)議,并不局限請(qǐng)求來源,而且也沒限制語(yǔ)言限制,所以非java語(yǔ)言只要發(fā)起調(diào)用符合規(guī)范就可以,這個(gè)大大豐富了異構(gòu)應(yīng)用場(chǎng)景,具體的協(xié)議細(xì)節(jié)可以參考JDWP官方規(guī)范文檔。
  • JDI
    JDI(Java Debug Interface)Java調(diào)試接口處在最上層,基于Java開發(fā)的調(diào)試接口,也就是我們調(diào)試客戶端,客戶端代碼封裝在jdk下面tools.jar的com.sun.jdi包里面,java程序可以直接調(diào)用的接口集合,具體提供的功能可以參考JDI官方API文檔。




8.1.2原理

介紹完JPDA的架構(gòu)體系后,我們了解到JAVA調(diào)試平臺(tái)各個(gè)層級(jí)的作用,這一節(jié)我們更近一步講解JPDA各個(gè)層面的工作原理,以及三個(gè)層級(jí)結(jié)合起來時(shí)如何交互的。
JVMTI

我們JVMTI是JVM提供的一套本地接口,包含了非常豐富的功能,我們調(diào)試和優(yōu)化代碼需要操作JVM,多數(shù)情況下就是調(diào)用到JVMTI,從官網(wǎng)我們可以看到,JVMTI包含了對(duì)JVM線程/內(nèi)存/堆/棧/類/方法/變量/事件/定時(shí)器處理等的20多項(xiàng)功能。但其實(shí)我們通常不是直接調(diào)用JVMTI,而是創(chuàng)建一個(gè)代理客戶端,我們可以自由的定義對(duì)JVMTI的操作然后打包到代理客戶端里面如libagent.so。當(dāng)目標(biāo)程序執(zhí)行時(shí)會(huì)啟動(dòng)JVM,這個(gè)時(shí)候在目標(biāo)程序運(yùn)行前會(huì)加載代理客戶端,所以代理客戶端是跟目標(biāo)程序運(yùn)行在同一個(gè)進(jìn)程上。這樣一來外部請(qǐng)求就通過代理客戶端間接調(diào)用到JVMTI,這樣的好處是我們可以在客戶端Agent里面定制高級(jí)功能,而且代理客戶端編譯打包成一個(gè)動(dòng)態(tài)鏈接庫(kù)之后可以復(fù)用,提高效率。我們簡(jiǎn)單描述一下代理客戶端Agent的工作流程。
建立代理客戶端首先需要定義Agent的入口函數(shù),猶如Java類的main方法一樣:
JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *vm, char *options, void *reserved);
然后JVM在啟動(dòng)的時(shí)候就會(huì)把JVMTI的指針JavaVM傳給代理的入口函數(shù),options則是傳參,有了這個(gè)指針后代理就可以充分調(diào)用JVMTI的函數(shù)了。
//設(shè)置斷點(diǎn),參數(shù)是調(diào)試目標(biāo)方法和行數(shù)位置
jvmtiError SetBreakpoint(jvmtiEnv* env,jmethodID method,jlocation location);
//當(dāng)目標(biāo)程序執(zhí)行到指定斷點(diǎn),目標(biāo)線程則被掛起
jvmtiError SuspendThread(jvmtiEnv* env,jthread thread);
當(dāng)然除了JVM啟動(dòng)時(shí)可以加載代理,運(yùn)行過程中也是可以的,這個(gè)下文我們講字節(jié)碼增強(qiáng)還會(huì)再說到。
JNIEXPORT jint JNICALL Agent_OnAttach(JavaVM* vm, char *options, void *reserved);
有興趣的同學(xué)可以自己動(dòng)手寫一個(gè)Agent試試,通過調(diào)用JVMTI接口可以實(shí)現(xiàn)自己定制化的調(diào)試工具。
JDWP

上文我們知道調(diào)用JVMTI需要建立一個(gè)代理客戶端,但是假如我建立了包含通用功能的Agent想開發(fā)出去給所有調(diào)試器使用,有一種方式是資深開發(fā)者通過閱讀我的文檔后進(jìn)行開發(fā)調(diào)用,還有另外一種方式就是我在我的Agent里面加入了JDWP協(xié)議模塊,這樣調(diào)試器就可以不用關(guān)心我的接口細(xì)節(jié),只需按照閱讀的協(xié)議發(fā)起請(qǐng)求即可。JDWP是調(diào)試器和JVM中間的協(xié)議規(guī)范,類似HTTP協(xié)議一樣,JDWP也定義規(guī)范了握手協(xié)議和報(bào)文格式。
調(diào)試器發(fā)起請(qǐng)求的握手流程:
1)調(diào)試器發(fā)送一段包含“JDWP-Handshake”的14個(gè)bytes的字符串
2)JVM回復(fù)同樣的內(nèi)容“JDWP-Handshake”
完成握手流程后就可以像HTTP一樣向JVM的代理客戶端發(fā)送請(qǐng)求數(shù)據(jù),同時(shí)回復(fù)所需參數(shù)。請(qǐng)求和回復(fù)的數(shù)據(jù)幀也有嚴(yán)格的結(jié)構(gòu),請(qǐng)求的數(shù)據(jù)格式為Command Packet,回復(fù)的格式為Reply Packet,包含包頭和數(shù)據(jù)兩部分,具體格式參考官網(wǎng)。實(shí)際上JDWP卻是也是通過建立代理客戶端來實(shí)現(xiàn)報(bào)文格式的規(guī)范,也就是JDWP Agent 里面的JDWPTI實(shí)現(xiàn)了JDWP對(duì)協(xié)議的定義。JDWP的功能是由JDWP傳輸接口(Java Debug Wire Protocol Transport Interface)實(shí)現(xiàn)的,具體流程其實(shí)跟JVMTI差不多,也是講JDWPTI編譯打包成代理庫(kù)后,在JVM啟動(dòng)的時(shí)候加載到目標(biāo)進(jìn)程。那么調(diào)試器調(diào)用的過程就是JDWP Agent接收到請(qǐng)求后,調(diào)用JVMTI Agent,JDWP負(fù)責(zé)定義好報(bào)文數(shù)據(jù),而JDWPTI則是具體的執(zhí)行命令和響應(yīng)事件。
JDI

前面已經(jīng)解釋了JVMTI和JDWP的工作原理和交互機(jī)制,剩下的就是搞清楚面向用戶的JDI是如何運(yùn)行的。首先JDI位于JPDA的最頂層入口,它的實(shí)現(xiàn)是通過JAVA語(yǔ)言編寫的,所以可以理解為Java調(diào)試客戶端對(duì)JDI接口的封裝調(diào)用,比如我們熟悉的IDE界面啟動(dòng)調(diào)試,或者JAVA的命令行調(diào)試客戶端JDB。
通常我們?cè)O(shè)置好目標(biāo)程序的斷點(diǎn)之后啟動(dòng)程序,然后通過調(diào)試器啟動(dòng)程序之前,調(diào)試器會(huì)先獲取JVM管理器,然后通過JVM管理器對(duì)象virtualMachineManager獲取連接器Connector,調(diào)試器與虛擬機(jī)獲得鏈接后就可以啟動(dòng)目標(biāo)程序了。如下代碼:
VirtualMachineManager virtualMachineManager = Bootstrap.virtualMachineManager();
JDI完成調(diào)試需要實(shí)現(xiàn)的功能有三個(gè)模塊:數(shù)據(jù)、鏈接、事件

  • 數(shù)據(jù)
    調(diào)試器要調(diào)試的程序在目標(biāo)JVM上,那么調(diào)試之前肯定需要將目標(biāo)程序的執(zhí)行環(huán)境同步過來,不然我們壓根就不知道要調(diào)試什么,所以需要一種鏡像機(jī)制,把目標(biāo)程序的堆棧方法區(qū)包含的數(shù)據(jù)以及接收到的事件請(qǐng)求都映射到調(diào)試器上面。那么JDI的底層接口Mirror就是干這樣的事,具體數(shù)據(jù)結(jié)構(gòu)可以查詢文檔。
  • 鏈接
    我們知道調(diào)試器跟目標(biāo)JVM直接的通訊是雙向的,所以鏈接雙方都可以發(fā)起。一個(gè)調(diào)試器可以鏈接多個(gè)目標(biāo)JVM,但是一個(gè)目標(biāo)虛擬機(jī)只能提供給一個(gè)調(diào)試器,不然就亂套了不知道聽誰(shuí)指令了。JDI定義三種鏈接器:?jiǎn)?dòng)鏈接器(LaunchingConnector)、依附鏈接器(AttachingConnector)、監(jiān)聽鏈接器(ListeningConnector)和。分別對(duì)應(yīng)的場(chǎng)景是目標(biāo)程序JVM啟動(dòng)時(shí)發(fā)起鏈接、調(diào)試器中途請(qǐng)求接入目標(biāo)程序JVM和調(diào)試器監(jiān)聽到被調(diào)試程序返回請(qǐng)求時(shí)發(fā)起的鏈接。
  • 事件
    也就是調(diào)試過程中對(duì)目標(biāo)JVM返回請(qǐng)求的響應(yīng)。
講解完JPDA體系的實(shí)現(xiàn)原理,我們?cè)俅问崂硪幌抡{(diào)試的整個(gè)流程:
調(diào)試器 —> JDI客戶端 —> JDWP Agent—> JVMTI Agent —>> JVMTI —> Application
8.1.3 實(shí)現(xiàn)

現(xiàn)在我們已經(jīng)對(duì)整個(gè)JPDA結(jié)構(gòu)有了深入理解,接下來我們就通過對(duì)這些樸素的原理來實(shí)現(xiàn)程序的斷點(diǎn)調(diào)試。當(dāng)然我們不會(huì)在這里介紹從IDE的UI斷點(diǎn)調(diào)試的過程,因?yàn)閷?duì)這套是使用已經(jīng)非常熟悉了,我們知道IDE的UI斷點(diǎn)調(diào)試本質(zhì)上是調(diào)試器客戶端對(duì)JDI的調(diào)用,那我們就通過一個(gè)調(diào)試的案例來解釋一下這背后的原理。
搭建服務(wù)

首先我們需要先搭建一個(gè)可供調(diào)試的web服務(wù),這里我首選springboot+來搭建,通過官網(wǎng)生成樣例project或者maven插件都可以,具體的太基礎(chǔ)的就不在這里演示,該服務(wù)只提供一個(gè)Controller包含的一個(gè)簡(jiǎn)單方法。如果使用Tomcat部署,則可以通過自有的開關(guān)catalina jpda start來啟動(dòng)debug模式。
package com.zooncool.debug.rest;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController("/debug")
public class DebugController {
    @GetMapping
    public String ask(@RequestParam("name") String name) {
        String message = "are you ok?" + name;
        return message;
    }
}
啟動(dòng)服務(wù)

搭建好服務(wù)之后我們先啟動(dòng)服務(wù),我們通過maven來啟動(dòng)服務(wù),其中涉及到的一些參數(shù)下面解釋。
mvn spring-boot:run -Drun.jvmArguments="-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8001"
或者
mvn spring-boot:run -Drun.jvmArguments="-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8001"

  • mvn:maven的腳本命令這個(gè)不用解釋
  • Spring-boot:run:?jiǎn)?dòng)springboot工程
  • -Drun.jvmArguments:執(zhí)行jvm環(huán)境的參數(shù),里面的參數(shù)值才是關(guān)鍵
  • -Xdebug
    Xdebug開啟調(diào)試模式,為非標(biāo)準(zhǔn)參數(shù),也就是可能在其他JVM上面是不可用的,Java5之后提供了標(biāo)準(zhǔn)的執(zhí)行參數(shù)agentlib,下面兩種參數(shù)同樣可以開啟debug模式,但是在JIT方面有所差異,這里先不展開。
    java -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=8001
    java -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8001
  • Xrunjdwp/jdwp=transport:表示連接模式是本地內(nèi)存共享還是遠(yuǎn)程socket連接
  • server:y表示打開socket監(jiān)聽調(diào)試器的請(qǐng)求;n表示被調(diào)試程序像客戶端一樣主動(dòng)連接調(diào)試器
  • suspend:y表示被調(diào)試程序需要等到調(diào)試器的連接請(qǐng)求之后才能啟動(dòng)運(yùn)行,在此之前都是掛起的,n表示被調(diào)試程序無需等待直接運(yùn)行。
  • address:被調(diào)試程序啟動(dòng)debug模式后監(jiān)聽請(qǐng)求的地址和端口,地址缺省為本地。
執(zhí)行完上述命令后,就等著我們調(diào)試器的請(qǐng)求接入到目標(biāo)程序了。
調(diào)試接入

我們知道java的調(diào)試器客戶端為jdb,下面我們就使用jdb來接入我們的目標(biāo)程序。
#jdb 通過attach參數(shù)選擇本地目標(biāo)程序,同時(shí)附上目標(biāo)程序的源碼,回想之前我們講到的JDI的鏡像接口,就是把目標(biāo)程序的堆棧結(jié)構(gòu)同步過來,如果能我們提供的源碼對(duì)應(yīng)上,那就可以在源碼上面顯示斷點(diǎn)標(biāo)志
$ jdb -attach localhost:8001 -sourcepath /Users/linzhenhua/Documents/repositories/practice/stackify-master/remote-debugging/src/main/java/
設(shè)置未捕獲的java.lang.Throwable
設(shè)置延遲的未捕獲的java.lang.Throwable
正在初始化jdb...

#stop,選擇對(duì)應(yīng)方法設(shè)置斷點(diǎn)
> stop in com.zooncool.debug.rest.DebugController.ask(java.lang.String)
設(shè)置斷點(diǎn)com.zooncool.debug.rest.DebugController.ask(java.lang.String)

#如果我們?cè)O(shè)置不存在的方法為斷點(diǎn),則會(huì)有錯(cuò)誤提示
> stop in com.zooncool.debug.rest.DebugController.ask2(java.lang.String)
無法設(shè)置斷點(diǎn)com.zooncool.debug.rest.DebugController.ask2(java.lang.String): com.zooncool.debug.rest.DebugController中沒有方法ask2

#這時(shí)候我們已經(jīng)設(shè)置完斷點(diǎn),就可以發(fā)起個(gè)HTTP請(qǐng)求
#http://localhost:7001/remote-debugging/debug/ask?name=Jack
#發(fā)起請(qǐng)求后我們回到j(luò)db控制臺(tái),觀察是否命中斷點(diǎn)
> 斷點(diǎn)命中: "線程=http-nio-7001-exec-5", com.zooncool.debug.rest.DebugController.ask(), 行=14 bci=0
14            String message = "are you ok?" + name;

#list,對(duì)照源碼,確實(shí)是進(jìn)入ask方法第一行命中斷點(diǎn),也就是14行,這時(shí)候我們可以查看源碼
http-nio-7001-exec-5[1] list
10    @RestController("/debug")
11    public class DebugController {
12        @GetMapping
13        public String ask(@RequestParam("name") String name) {
14 =>         String message = "are you ok?" + name;
15            return message;
16        }
17    }

#locals,觀察完源碼,我們想獲取name的傳參,跟URL傳入的一致
http-nio-7001-exec-5[1] locals
方法參數(shù):
name = "Jack"
本地變量:

#print name,打印入?yún)?br /> http-nio-7001-exec-5[1] print name
name = "Jack"

#where,查詢方法調(diào)用的棧幀,從web容器入口調(diào)用方法到目標(biāo)方法的調(diào)用鏈路
http-nio-7001-exec-5[1] where
  [1] com.zooncool.debug.rest.DebugController.ask (DebugController.java:14)
  ...
  [55] java.lang.Thread.run (Thread.java:748)
#step,下一步到下一行代碼
http-nio-7001-exec-5[1] step
> 已完成的步驟: "線程=http-nio-7001-exec-5", com.zooncool.debug.rest.DebugController.ask(), 行=15 bci=20
15            return message;

#step up,完成當(dāng)前方法的調(diào)用
http-nio-7001-exec-5[1] step up
> 已完成的步驟: "線程=http-nio-7001-exec-5", sun.reflect.NativeMethodAccessorImpl.invoke(), 行=62 bci=103

#cont,結(jié)束調(diào)試,執(zhí)行完畢
http-nio-7001-exec-5[1] cont
>

#clear,完成調(diào)試任務(wù),清除斷點(diǎn)
> clear
斷點(diǎn)集:
        斷點(diǎn)com.zooncool.debug.rest.DebugController.ask(java.lang.String)
        斷點(diǎn)com.zooncool.debug.rest.DebugController.ask2(java.lang.String)
#選擇一個(gè)斷點(diǎn)刪除
> clear com.zooncool.debug.rest.DebugController.ask(java.lang.String)
已刪除: 斷點(diǎn)com.zooncool.debug.rest.DebugController.ask(java.lang.String)
我們已經(jīng)完成了命令行調(diào)試的全部流程,stop/list/locals/print name/where/step/step up/cont/clear這些命令其實(shí)就是IDE的UI后臺(tái)調(diào)用的腳本。而這些腳本就是基于JDI層面的接口所提供的能力,下面我們還有重點(diǎn)觀察一個(gè)核心功能,先從頭再設(shè)置一下斷點(diǎn)。
#stop,選擇對(duì)應(yīng)方法設(shè)置斷點(diǎn)
> stop in com.zooncool.debug.rest.DebugController.ask(java.lang.String)
設(shè)置斷點(diǎn)com.zooncool.debug.rest.DebugController.ask(java.lang.String)
#這時(shí)候我們已經(jīng)設(shè)置完斷點(diǎn),就可以發(fā)起個(gè)HTTP請(qǐng)求
#http://localhost:7001/remote-debugging/debug/ask?name=Jack
#發(fā)起請(qǐng)求后我們回到j(luò)db控制臺(tái),觀察是否命中斷點(diǎn)
> 斷點(diǎn)命中: "線程=http-nio-7001-exec-5", com.zooncool.debug.rest.DebugController.ask(), 行=14 bci=0
14            String message = "are you ok?" + name;
#print name,打印入?yún)?br /> http-nio-7001-exec-5[1] print name
name = "Jack"
#如果這個(gè)時(shí)候我們想替換掉Jack,換成Lucy
http-nio-7001-exec-6[1] set name = "Lucy"   
name = "Lucy" = "Lucy"
#進(jìn)入下一步
http-nio-7001-exec-6[1] step
> 已完成的步驟: "線程=http-nio-7001-exec-6", com.zooncool.debug.rest.DebugController.ask(), 行=15 bci=20
15            return message;
#查看變量,我們發(fā)現(xiàn)name的值已經(jīng)被修改了
http-nio-7001-exec-6[1] locals
方法參數(shù):
name = "Lucy"
本地變量:
message = "are you ok?Lucy"
至此我們已經(jīng)完成了JPDA的原理解析到調(diào)試實(shí)踐,也理解了JAVA調(diào)試的工作機(jī)制,其中留下一個(gè)重要的彩蛋就是通過JPDA進(jìn)入調(diào)試模式,我們可以動(dòng)態(tài)的修改JVM內(nèi)存對(duì)象和類的內(nèi)容,這也講引出下文我們要介紹的字節(jié)碼增強(qiáng)技術(shù)。
8.2 熱替換

8.2.1概念

終于來到熱替換這節(jié)了,前文我們做了好多鋪墊,介紹熱替換之前我們稍稍回顧一下熱部署。我們知道熱部署是“獨(dú)立”于JVM之外的一門對(duì)類加載器應(yīng)用的技術(shù),通常是應(yīng)用容器借助自定義類加載器的迭代,無需重啟JVM缺能更新代碼從而達(dá)到熱部署,也就是說熱部署是JVM之外容器提供的一種能力。而本節(jié)我們介紹的熱替換技術(shù)是實(shí)打?qū)岼VM提供的能力,是JVM提供的一種能夠?qū)崟r(shí)更新內(nèi)存類結(jié)構(gòu)的一種能力,這種實(shí)時(shí)更新JVM方法區(qū)類結(jié)構(gòu)的能力當(dāng)然也是無需重啟JVM實(shí)例。
熱替換HotSwap是Sun公司在Java 1.4版本引入的一種新實(shí)驗(yàn)性技術(shù),也就是上一節(jié)我們介紹JPDA提到的調(diào)試模式下可以動(dòng)態(tài)替換類結(jié)構(gòu)的彩蛋,這個(gè)功能被集成到JPDA框架的接口集合里面,首先我們定義好熱替換的概念。
熱替換(HotSwap):使用字節(jié)碼增強(qiáng)技術(shù)替換JVM內(nèi)存里面類的結(jié)構(gòu),包括對(duì)應(yīng)類的對(duì)象,而不需要重啟虛擬機(jī)。
8.2.2原理

前文從宏觀上介紹了JVM實(shí)例的內(nèi)存布局和垃圾回收機(jī)制,微觀上也解釋了類的結(jié)構(gòu)和類加載機(jī)制,上一節(jié)又學(xué)習(xí)了JAVA的調(diào)試框架,基本上我們對(duì)JVM的核心模塊都已經(jīng)摸透了,剩下的就是攻克字節(jié)碼增強(qiáng)的技術(shù)了。而之前講的字節(jié)碼增強(qiáng)技術(shù)也僅僅是放在JPDA里面作為實(shí)驗(yàn)性技術(shù),而且僅僅局限在方法體和變量的修改,無法動(dòng)態(tài)修改方法簽名或者增刪方法,因?yàn)樽止?jié)碼增強(qiáng)涉及到垃圾回收機(jī)制,類結(jié)構(gòu)變更,對(duì)象引用,即時(shí)編譯等復(fù)雜問題。在HotSwap被引進(jìn)后至今,JCP也未能通過正式的字節(jié)碼增強(qiáng)實(shí)現(xiàn)。
JAVA是一門靜態(tài)語(yǔ)言,而字節(jié)碼增強(qiáng)所要達(dá)的效果就是讓Java像動(dòng)態(tài)語(yǔ)言一樣跑起來,無需重啟服務(wù)器。下面我們介紹字節(jié)碼增強(qiáng)的基本原理。

  • 反射代理
    反射代理不能直接修改內(nèi)存方法區(qū)的字節(jié)碼,但是可以抽象出一層代理,通過內(nèi)存新增實(shí)例來實(shí)現(xiàn)類的更新
  • 原生接口
    jdk上層提供面向java語(yǔ)言的字節(jié)碼增強(qiáng)接口java.lang.instrument,通過實(shí)現(xiàn)ClassFileTransformer接口來操作JVM方法區(qū)的類文件字節(jié)碼。
  • JVMTI代理
    JVM的JVMTI接口包含了操作方法區(qū)類文件字節(jié)碼的函數(shù),通過創(chuàng)建代理,將JVMTI的指針JavaVM傳給代理,從而擁有JVM 本地操作字節(jié)碼的方法引用。
  • 類加載器織入
    字節(jié)碼增強(qiáng)接口加上類加載器的織入,結(jié)合起來也是一種熱替換技術(shù)。
  • JVM增強(qiáng)
    直接新增JVM分支,增加字節(jié)碼增強(qiáng)功能。
8.2.3實(shí)現(xiàn)

但是盡管字節(jié)碼增強(qiáng)是一門復(fù)雜的技術(shù),這并不妨礙我們進(jìn)一步的探索,下面我們介紹幾種常見的實(shí)現(xiàn)方案。

  • Instrumentation
  • AspectJ
  • ASM
  • DCEVM
  • JREBEL
  • CGLIB
  • javassist
  • BCEL
具體的我會(huì)挑兩個(gè)具有代表性的工具深入講解,篇幅所限,這里就補(bǔ)展開了。
9.總結(jié)

JVM是程序發(fā)展至今的一顆隗寶,是程序設(shè)計(jì)和工程實(shí)現(xiàn)的完美結(jié)合。JVM作為作為三大工業(yè)級(jí)程序語(yǔ)言為首JAVA的根基,本文試圖在瀚如煙海的JVM海洋中找出其中最耀眼的冰山,并力求用簡(jiǎn)潔的邏輯線索把各個(gè)冰山串起來,在腦海中對(duì)JVM的觀感有更加立體的認(rèn)識(shí)。更近一步的認(rèn)識(shí)JVM對(duì)程序設(shè)計(jì)的功力提示大有裨益,而本文也只是將海平面上的冰山鏈接起來,但這只是冰山一角,JVM更多的底層設(shè)計(jì)和實(shí)現(xiàn)細(xì)節(jié)還遠(yuǎn)遠(yuǎn)沒有涉及到,而且也不乏知識(shí)盲區(qū)而沒有提及到的,路漫漫其修遠(yuǎn)兮,JVM本身也在不斷的推陳出新,借此機(jī)會(huì)總結(jié)出JVM的核心體系,以此回顧對(duì)JVM知識(shí)的查漏補(bǔ)缺,也是一次JVM的認(rèn)知升級(jí)。最后還是例牌來兩張圖結(jié)束JVM的介紹,希望對(duì)更的同學(xué)有幫助。





-----------------------------
精選高品質(zhì)二手iPhone,上愛鋒貝APP
您需要登錄后才可以回帖 登錄 | 立即注冊(cè)   

本版積分規(guī)則

QQ|Archiver|手機(jī)版|小黑屋|愛鋒貝 ( 粵ICP備16041312號(hào)-5 )

GMT+8, 2025-2-27 11:30

Powered by Discuz! X3.4

© 2001-2013 Discuz Team. 技術(shù)支持 by 巔峰設(shè)計(jì).

快速回復(fù) 返回頂部 返回列表