日韩av黄I国产麻豆传媒I国产91av视频在线观看I日韩一区二区三区在线看I美女国产在线I麻豆视频国产在线观看I成人黄色短片

歡迎訪問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 编程资源 > 编程问答 >内容正文

编程问答

JVM核心知识体系

發(fā)布時(shí)間:2025/3/21 编程问答 38 豆豆
生活随笔 收集整理的這篇文章主要介紹了 JVM核心知识体系 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

1.問(wèn)題

  • 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過(guò)于成功以至于我們常常忘了JVM平臺(tái)上還運(yùn)行著像Clojure/Groovy/Kotlin/Scala/JRuby/Jython這樣的語(yǔ)言。我們享受著JVM帶來(lái)跨平臺(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)過(guò)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ī)范說(shuō)明書里面查閱類文件格式(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)后,我們需要來(lái)觀察JVM是如何使用這些從硬盤上或者網(wǎng)絡(luò)傳輸過(guò)來(lái)的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è)過(guò)程就是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í)行,我們來(lái)觀察一下發(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)?#xff0c;在操作系統(tǒng)來(lái)看java也就是一個(gè)普通的應(yīng)用進(jìn)程而已,而這個(gè)進(jìn)程就是JVM的執(zhí)行形態(tài)(JVM靜態(tài)就是硬盤里JDK包下的二進(jìn)制文件集合)。

學(xué)習(xí)過(guò)JAVA的都知道入口方法是public static void main(String[] args),缺一不可,那我猜執(zhí)行java命令時(shí)JVM對(duì)該入口方法做了唯一驗(yàn)證,通過(guò)了才允許啟動(dòng)JVM進(jìn)程,下面我們來(lái)看這個(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

說(shuō)名入口方法需要被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)用后無(wú)需關(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方法的。

說(shuō)了這么多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)用通過(guò)JNI找到了HelloWorld里面符合規(guī)范的main方法,然后開始調(diào)用。我們來(lái)看下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)行過(guò)程中通過(guò)IO從硬盤或者網(wǎng)絡(luò)讀取CLASS二進(jìn)制文件,然后在JVM管轄的內(nèi)存區(qū)域存放對(duì)應(yīng)的文件。我們目前還不知道類加載器的實(shí)現(xiàn),但是我們從功能上判斷無(wú)非就是讀取文件到內(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é)果就是亂糟糟的把一堆毫無(wú)秩序的類文件往內(nèi)存里面扔,沒有良好的管理也沒法用,所以需要我們需要設(shè)計(jì)一套規(guī)則來(lái)管理存放內(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ù)需要我們從外部加載類來(lái)支持,所以我們需要指定新的規(guī)則,以方便我們加載外部路徑的類文件。

系統(tǒng)默認(rèn)加載器

  • Bootstrap class loader

    作用:啟動(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);} }

通過(guò)上文運(yùn)行HelloWorld我們知道JVM系統(tǒng)默認(rèn)加載的類大改是1560個(gè),如下圖

自定義類加載器

內(nèi)置類加載器只加載了最少需要的核心JAVA基礎(chǔ)類和環(huán)境變量下的類,但是我們應(yīng)用往往需要依賴第三方中間件來(lái)完成額外的業(yè)務(wù),那么如何把它們的類加載進(jìn)來(lái)就顯得格外重要了。幸好JVM提供了自定義類加載器,可以很方便的完成自定義操作,最終目的也是把外部的類文件加載到JVM內(nèi)存。通過(guò)繼承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 {//如果為空則說(shuō)明應(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 statssun.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); }

通過(guò)復(fù)寫loadClass方法,我們甚至可以讀取一份加了密的文件,然后在內(nèi)存里面解密,這樣別人反編譯你的源碼也沒用,因?yàn)閏lass是經(jīng)過(guò)加密的,也就是理論上我們通過(guò)自定義類加載器可以做到為所欲為,但是有個(gè)重要的原則下文介紹類加載器設(shè)計(jì)模式會(huì)提到。

一下給出一個(gè)自定義類加載器極簡(jiǎn)的案例,來(lái)說(shuō)明自定義類加載器的實(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)自己的邏輯@Overridepublic 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)置類加載器和自定義加載器,不管它們是通過(guò)C或者JAVA實(shí)現(xiàn)的最終都是為了把外部的CLASS文件加載到JVM內(nèi)存里面。那么我們就需要設(shè)計(jì)一套規(guī)則來(lái)管理組織內(nèi)存里面的CLASS文件,下面我們就來(lái)介紹下通過(guò)這套規(guī)則如何來(lái)協(xié)調(diào)好內(nèi)置類加載器和自定義類加載器之間的權(quán)責(zé)。

我們知道通過(guò)自定義類加載器可以干出很多黑科技,但是有個(gè)基本的雷區(qū)就是,不能隨便替代JAVA的核心基礎(chǔ)類,或者說(shuō)即是你寫了一個(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ú)法完成此加載任務(wù)時(shí),才自己去加載。

    實(shí)現(xiàn):參考上文loadClass方法的源碼和注釋,通過(guò)最多三次遞歸可以到啟動(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)類加載器”可以指定“線程上下文類加載器”為任意類加載器,即是“父類加載器”委托“子類加載器”去加載不屬于它加載范圍的類文件;

    說(shuō)明:雙親委派機(jī)制的好處上面我們已經(jīng)提過(guò)了,但是由于一些歷史原因(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 判斷類是否來(lái)自相同的加載器,如果相同而且全限定名則直接返回內(nèi)存已有的類。

    說(shuō)明:上文我們提到如何防止相同類的后門問(wèn)題,有了這個(gè)黃金法則,即使相同的類路徑和類,但是由于是由自定義類加載器加載的,即使編譯通過(guò)能被加載到內(nèi)存,也無(wú)法使用,因?yàn)镴VM核心類是由內(nèi)置類加載器加載標(biāo)志和使用的,從而保證了JVM的安全加載。通過(guò)緩存類加載器和全限定包名和類名作為類唯一索引,加載重復(fù)類則拋異常提示”attempted duplicate class definition for name”。

    原理:雙親委派機(jī)制父類檢查緩存,源碼我們介紹loadClass方法的時(shí)候已經(jīng)講過(guò),破壞雙親委派的自定義類加載器在加載類二進(jìn)制字節(jié)碼后需要調(diào)用defineClass方法,而該方法同樣會(huì)從JVM方法區(qū)檢索緩存類,存在的話則提示重復(fù)定義。

4.2.4加載過(guò)程

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

加載

上文花了不少的篇幅說(shuō)明了類的結(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)代碼塊

  • 反射:通過(guò)對(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)過(guò)驗(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)證不通過(guò)則跑出VerifyError的錯(cuò)誤。但是該階段并非強(qiáng)制執(zhí)行,可以通過(guò)-Xverify:none來(lái)關(guān)閉,提高性能。

  • 準(zhǔn)備:但我們驗(yàn)證通過(guò)時(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é),但是通過(guò)常量池引用的轉(zhuǎn)換為實(shí)際的變量類型,需要占用32個(gè)字節(jié)。所以經(jīng)過(guò)解析階段后,類在方法區(qū)占用的空間就會(huì)膨脹,長(zhǎng)得更像一個(gè)”類“了。

初始化

方法區(qū)經(jīng)過(guò)解析后類已經(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)加載器被回收

  • 類無(wú)反射引用

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

4.3應(yīng)用場(chǎng)景

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

4.3.1熱部署

背景

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

  • 熱部署(Hot Deployment):熱部署是應(yīng)用容器自動(dòng)更新應(yīng)用的一種能力。

首先熱部署應(yīng)用容器擁有的一種能力,這種能力是容器本身設(shè)計(jì)出來(lái)的,跟具體的IDE開發(fā)工具無(wú)關(guān)。而且熱部署無(wú)需重啟服務(wù)器,應(yīng)用可以保持用戶態(tài)不受影響。上文提到我們開發(fā)環(huán)境使用IDE工具通常也可以設(shè)置無(wú)需重啟的功能,有別于熱部署的是此時(shí)我們應(yīng)用的是JVM的本身附帶的熱替換能力(HotSwap)。熱部署和熱替換是兩個(gè)完全不同概念,在開發(fā)過(guò)程中也常常相互配合使用,導(dǎo)致我們很多人經(jīng)常混淆概念,所以接下來(lái)我們來(lái)剖析熱部署的實(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í)也就是熱部署的過(guò)程了。也即是通過(guò)使用新的類加載器,重新加載應(yīng)用的類,從而達(dá)到新代碼熱部署。

實(shí)現(xiàn)

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

  • 實(shí)現(xiàn)自定義類加載器

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

  • 替換自定義類加載器

既然一個(gè)類通過(guò)不同類加載器可以被多次加載到JVM內(nèi)存里面,那么類的經(jīng)過(guò)修改編譯后再加載進(jìn)內(nèi)存。有別于上一步給出的例子只是修改對(duì)象的值,這次我們是直接修改類的內(nèi)容,從應(yīng)用的視角看其實(shí)就是應(yīng)用更新,那如何做到在線程運(yùn)行不中斷的情況下更換新類呢?

下面給出的也是一個(gè)很簡(jiǎn)單的例子,ClassReloading啟動(dòng)main方法通過(guò)死循環(huán)不斷創(chuàng)建類加載器,同時(shí)不斷加載類而且執(zhí)行類的方法。注意new MyClassLoader(“target/classes”)的路徑更加編譯的class路徑來(lái)修改,其他直接復(fù)制過(guò)去就可以執(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)//通過(guò)反射調(diào)用目標(biāo)類的入口方法String className = "com.zooncool.example.theory.jvm.ClassReloading$User";Class<?> target = new MyClassLoader("target/classes").loadClass(className);//加載進(jìn)來(lái)的類,通過(guò)反射調(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è)例子一樣,可略過(guò)public static class MyClassLoader extends ClassLoader{...} }

ClassReloading線程執(zhí)行過(guò)程不斷輪流注釋say()和ask()代碼,然后編譯類,觀察程序輸出。

如下輸出結(jié)果,我們可以看出每一次循環(huán)調(diào)用都新創(chuàng)建一個(gè)自定義類加載器,然后通過(guò)反射創(chuàng)建對(duì)象調(diào)用方法,在修改代碼編譯后,新的類就會(huì)通過(guò)反射創(chuàng)建對(duì)象執(zhí)行新的代碼業(yè)務(wù),而主線程則一直沒有中斷運(yùn)行。讀到這里,其實(shí)我們已經(jīng)基本觸達(dá)了熱部署的本質(zhì)了,也就是實(shí)現(xiàn)了手動(dòng)無(wú)中斷部署。但是缺點(diǎn)就是需要我們手動(dòng)編譯代碼,而且內(nèi)存不斷新增類加載器和對(duì)象,如果速度過(guò)快而且頻繁更新,還可能造成堆溢出,下一個(gè)例子我們將增加一些機(jī)制來(lái)保證舊的類和對(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ì)持有該加載器加載過(guò)的所有類的引用,所有如果類是經(jīng)過(guò)系統(tǒng)默認(rèn)類加載器加載的話,那就很難被垃圾收集器回收,除非符合根節(jié)點(diǎn)不可達(dá)原則才會(huì)被回收。

下面繼續(xù)給出一個(gè)很簡(jiǎn)單的例子,我們知道ClassReloading只是不斷創(chuàng)建新的類加載器來(lái)加載新類從而更新類的方法。下面的例子我們模擬WEB應(yīng)用,更新整個(gè)應(yīng)用的上下文Context。下面代碼本質(zhì)上跟上個(gè)例子的功能是一樣的,只不過(guò)我們通過(guò)加載Model層、DAO層和Service層來(lái)模擬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);//通過(guò)上下文對(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";//通過(guò)自定義類加載器加載Context類Class<?> contextClass = new MyClassLoader("target/classes").loadClass(className);Object context = contextClass.newInstance();//通過(guò)反射創(chuàng)建對(duì)象contextClass.getDeclaredMethod("init").invoke(context);//通過(guò)反射調(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;}}//跟之前的類加載器一模一樣,可以略過(guò)public static class MyClassLoader extends ClassLoader{...} }

輸出結(jié)果跟上一個(gè)例子相似,可以自己運(yùn)行試試。我們更新業(yè)務(wù)方法編譯通過(guò)后,無(wú)需重啟main方法,新的業(yè)務(wù)就能生效,而且也解決了舊類卸載的核心問(wèn)題,因?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)也將無(wú)法保留。而實(shí)際情況是我們只需要?jiǎng)討B(tài)更新某些模塊的功能,而不是全局。這個(gè)其實(shí)也好辦,就是我們從業(yè)務(wù)上把需要熱部署的由自定義類加載器加載,而持久化的類資源則由系統(tǒng)默認(rèn)類加載器去完成。

  • 自動(dòng)加載類加載器

其實(shí)設(shè)計(jì)到代碼設(shè)計(jì)優(yōu)雅問(wèn)題,基本上我們拿出設(shè)計(jì)模式23章經(jīng)對(duì)號(hào)入座基本可以解決問(wèn)題,畢竟這是前人經(jīng)過(guò)千萬(wàn)實(shí)踐錘煉出來(lái)的軟件構(gòu)建內(nèi)功心法。那么針對(duì)我們熱部署的場(chǎng)景,如果想把熱部署細(xì)節(jié)封裝出來(lái),那代理模式無(wú)疑是最符合要求的,也就是咱們弄出個(gè)代理對(duì)象來(lái)面向用戶,把類加載器的更替,回收,隔離等細(xì)節(jié)都放在代理對(duì)象里面完成,而對(duì)于用戶來(lái)說(shuō)是透明無(wú)感知的,那么終端用戶體驗(yàn)起來(lái)就是純粹的熱部署了。至于如何實(shí)現(xiàn)自動(dòng)熱部署,方式也很簡(jiǎn)單,監(jiān)聽我們部署的目錄,如果文件時(shí)間和大小發(fā)生變化,則判斷應(yīng)用需要更新,這時(shí)候就觸發(fā)類加載器的創(chuàng)建和舊對(duì)象的回收,這個(gè)時(shí)候也可以引入觀察者模式來(lái)實(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ì)于處理來(lái)自網(wǎng)絡(luò)請(qǐng)求的一套規(guī)范,比如init,service,destroy的生命周期。然后我們通過(guò)實(shí)行這套規(guī)范來(lái)實(shí)現(xiàn)與客戶端的交互,在調(diào)用init初始化完Servlet對(duì)象后通過(guò)多線程模式使用service響應(yīng)網(wǎng)絡(luò)請(qǐng)求。如果從響應(yīng)模式比較我們還可以了解下Webflux的規(guī)范,以上兩種都是處理網(wǎng)絡(luò)請(qǐng)求的方式,當(dāng)然你舉例說(shuō)CGI也是一種處理網(wǎng)絡(luò)請(qǐng)求的規(guī)范,CGI采用的是多進(jìn)程方式來(lái)處理網(wǎng)絡(luò)請(qǐng)求,我們暫時(shí)不對(duì)這兩種規(guī)范進(jìn)行優(yōu)劣評(píng)價(jià),只是說(shuō)明在處理網(wǎng)絡(luò)請(qǐng)求的場(chǎng)景下可以采用不同的規(guī)范來(lái)實(shí)現(xiàn)。

好了現(xiàn)在回到OSGi,有了上面的鋪墊,相信對(duì)我們理解OSGI大有幫助。我們說(shuō)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í)也帶來(lái)了性能損耗,而且基于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èn)服務(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)來(lái)剖析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定義的模塊單元)都是由各自的類加載器來(lái)加載,而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ì)上是一樣的,只不過(guò)實(shí)現(xiàn)OSGI規(guī)范的框架需要提供模塊之間的注冊(cè)通信組件,還有模塊的生命周期管理,版本管理。OSGI也只是JVM上面運(yùn)行的一個(gè)普通應(yīng)用實(shí)例,只不過(guò)通過(guò)模塊內(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í)候無(wú)外乎也是通過(guò)GroovyClassLoader來(lái)加載腳本文件,轉(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)烈,畢竟熱部署過(guò)程中帶來(lái)的弊端也不容忽視,比如替換舊的類加載器過(guò)程會(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)用來(lái)得更快一點(diǎn),而且隨著分布式架構(gòu)的演進(jìn)和微服務(wù)的流行,應(yīng)用重啟也早就實(shí)現(xiàn)服務(wù)編排化,配合豐富的部署策略,也可以同樣保證系統(tǒng)穩(wěn)定持續(xù)服務(wù),我們更多的是通過(guò)熱部署技術(shù)來(lái)深刻認(rèn)識(shí)到JVM加載類的技術(shù)演進(jìn)。

4.3.2類隔離

背景

先介紹一下類隔離的背景,我們費(fèi)了那么大的勁設(shè)計(jì)出類加載器,如果只是用于加載外部類字節(jié)流那就過(guò)于浪費(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ù)以來(lái)的往往是不同版本的類庫(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ù)隔離開來(lái),避免一些安全隱患,不然如果共用的話,應(yīng)用升級(jí)依賴版本就會(huì)導(dǎo)致WEB容器不穩(wěn)定。

基于以上的介紹我們知道類隔離實(shí)在是剛需,那么接下來(lái)介紹一下如何實(shí)現(xiàn)這個(gè)剛需。

原理

首先我們要了解一下原理,其實(shí)原理很簡(jiǎn)單,真的很簡(jiǎn)單,請(qǐng)?jiān)试S我總結(jié)為“唯一標(biāo)識(shí)原理”。我們知道內(nèi)存里面定位類實(shí)例的坐標(biāo)<類加載器,類全限定名>。那么由這兩個(gè)因子組合起來(lái)我們可以得出一種普遍的應(yīng)用,用不同類加載器來(lái)加載類相同類(類全限定名一致,版本不一致)是可以實(shí)現(xiàn)的,也就是在JVM看來(lái),有相同類全名的類是完全不同的兩個(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)問(wèn)題,介紹內(nèi)存隔離技術(shù)之前,我們先普及更為通用的沖突解決方法。

  • 沖突排除

    沖突總是先發(fā)生在編譯時(shí)期,那么基本Maven工具可以幫我們完成大部分的工作,Maven的工作模式就是將我們第三方類庫(kù)的所有依賴都依次檢索,最終排除掉產(chǎn)生沖突的jar包版本。

  • 沖突適配

    當(dāng)我們無(wú)法通過(guò)簡(jiǎn)單的排除來(lái)解決的時(shí)候,另外一個(gè)方法就是重新裝配第三方類庫(kù),這里我們要介紹一個(gè)開源工具jarjar (https://github.com/shevek/jarjar)。該工具包可以通過(guò)字節(jié)碼技術(shù)將我們依賴的第三方類庫(kù)重命名,同時(shí)修改代碼里面對(duì)第三方類庫(kù)引用的路徑。這樣如果出現(xiàn)同名第三方類庫(kù)的話,通過(guò)該“硬編碼”的方式修改其中一個(gè)類庫(kù),從而消除了沖突。

  • 沖突隔離

    上面兩種方式在小型系統(tǒng)比較適合,也比較敏捷高效。但是對(duì)于分布式大型系統(tǒng)的話,通過(guò)硬編碼方式來(lái)解決沖突就難以完成了。辦法就是通過(guò)隔離容器,從邏輯上區(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)求的棧深度超過(guò)jvm運(yùn)行深度時(shí)拋StackOverflowError,當(dāng)jvm棧無(wú)法申請(qǐng)到空閑內(nèi)存時(shí)拋OutOfMemoryError,通過(guò)-Xss,-Xsx來(lái)配置初始內(nèi)存

  • 本地方法棧:執(zhí)行本地方法,如操作系統(tǒng)native接口

  • 堆:存放對(duì)象的空間,通過(guò)-Xmx,-Xms配置堆大小,當(dāng)堆無(wú)法申請(qǐng)到內(nèi)存時(shí)拋OutOfMemoryError

  • 方法區(qū):存儲(chǔ)類數(shù)據(jù),常量,常量池,靜態(tài)變量,通過(guò)MaxPermSize參數(shù)配置

  • 對(duì)象訪問(wèn):初始化一個(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)過(guò)多少次GC晉升老年代則由參數(shù)配置,通常為15

當(dāng)老年區(qū)填滿時(shí),老年區(qū)同樣會(huì)執(zhí)行垃圾回收,老年區(qū)還包含那些經(jīng)過(guò)多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)存問(wèn)題存在了。與駐留在Java堆中的Perm Gen不同,Metaspace不是堆的一部分。類元數(shù)據(jù)多數(shù)情況下都是從本地內(nèi)存中分配的。默認(rèn)情況下,元空間會(huì)自動(dòng)增加其大小(直接又底層操作系統(tǒng)提供),而Perm Gen始終具有固定的上限。可以使用兩個(gè)新標(biāo)志來(lái)設(shè)置Metaspace的大小,它們是:“ -?XX:MetaspaceSize?”和“?-XX:MaxMetaspaceSize?”。Metaspace背后的含義是類的生命周期及其元數(shù)據(jù)與類加載器的生命周期相匹配。也就是說(shuō),只要類加載器處于活動(dòng)狀態(tài),元數(shù)據(jù)就會(huì)在元數(shù)據(jù)空間中保持活動(dòng)狀態(tài),并且無(wú)法釋放。

  • 代碼緩存

運(yùn)行Java程序時(shí),它以分層方式執(zhí)行代碼。在第一層,它使用客戶端編譯器(C1編譯器)來(lái)編譯代碼。分析數(shù)據(jù)用于服務(wù)器編譯的第二層(C2編譯器),以優(yōu)化的方式編譯該代碼。默認(rèn)情況下,Java 7中未啟用分層編譯,但在Java 8中啟用了分層編譯。實(shí)時(shí)(JIT)編譯器將編譯的代碼存儲(chǔ)在稱為代碼緩存的區(qū)域中。它是一個(gè)保存已編譯代碼的特殊堆。如果該區(qū)域的大小超過(guò)閾值,則該區(qū)域?qū)⒈凰⑿?#xff0c;并且GC不會(huì)重新定位這些對(duì)象。Java 8中已經(jīng)解決了一些性能問(wèn)題和編譯器未重新啟用的問(wèn)題,并且在Java 7中避免這些問(wèn)題的解決方案之一是將代碼緩存的大小增加到一個(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),我們可以使用它們來(lái)設(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ì)象的過(guò)程。垃圾收集器是JVM管理的進(jìn)程,它可以查看內(nèi)存中的所有對(duì)象,并找出程序任何部分未引用的對(duì)象,刪除并回收空間以分配給其他對(duì)象。通常會(huì)經(jīng)過(guò)如下步驟:

  • 標(biāo)記:標(biāo)記哪些對(duì)象被使用,哪些已經(jīng)是無(wú)法觸達(dá)的無(wú)用對(duì)象

  • 刪除:刪除無(wú)用對(duì)象并回收要分配給其他對(duì)象

  • 壓縮:性能考慮,在刪除無(wú)用的對(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)入老年代:超過(guò)3m的對(duì)象直接進(jìn)入老年區(qū) -XX:PretenureSizeThreshold=3145728(3M)

  • 長(zhǎng)期存貨對(duì)象進(jìn)入老年區(qū):
    Survivor區(qū)中的對(duì)象經(jīng)歷一次Minor GC年齡增加一歲,超過(guò)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收集器:單線程,主要用于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)來(lái)控制線程數(shù)

  • 并行舊GC(-XX:+ UseParallelOldGC):這與Parallel GC相同,只是它為新生代和老年代垃圾收集使用多個(gè)線程

  • 并發(fā)標(biāo)記掃描(CMS)收集器(-XX:+ UseConcMarkSweepGC):CMS也稱為并發(fā)低暫停收集器。它為老年代做垃圾收集。CMS收集器嘗試通過(guò)在應(yīng)用程序線程內(nèi)同時(shí)執(zhí)行大多數(shù)垃圾收集工作來(lái)最小化由于垃圾收集而導(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í)行引擎來(lái)執(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é)碼可以通過(guò)以下兩種方式之一轉(zhuǎn)化為合適的語(yǔ)言。

  • 解釋器:逐個(gè)讀取,解釋和執(zhí)行字節(jié)碼指令。當(dāng)它逐個(gè)解釋和執(zhí)行指令時(shí),它可以快速解釋一個(gè)字節(jié)碼,但是同時(shí)也只能相對(duì)緩慢的地執(zhí)行解釋結(jié)果,這是解釋語(yǔ)言的缺點(diǎn)。

  • JIT(實(shí)時(shí))編譯器:引入了JIT編譯器來(lái)彌補(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通過(guò)分析搜索需要以最高優(yōu)先級(jí)進(jìn)行編譯的“Hotspot”,然后將熱點(diǎn)編譯為本機(jī)代碼。如果不再頻繁調(diào)用編譯了字節(jié)碼的方法,換句話說(shuō),如果該方法不再是熱點(diǎn),則Hotspot VM將從緩存中刪除本機(jī)代碼并以解釋器模式運(yùn)行。Hotspot VM分為服務(wù)器VM和客戶端VM,兩個(gè)VM使用不同的JIT編譯器。

大多數(shù)Java性能改進(jìn)都是通過(guò)改進(jìn)執(zhí)行引擎來(lái)實(shí)現(xiàn)的。除了JIT編譯器之外,還引入了各種優(yōu)化技術(shù),因此可以不斷改進(jìn)JVM性能。初始JVM和最新JVM之間的最大區(qū)別是執(zhí)行引擎。

下面我們通過(guò)下圖可以看出JAVA執(zhí)行的流程。

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

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

  • 棧幀包含:局部變量表,操作數(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í)通過(guò)參數(shù)的靜態(tài)類型來(lái)確定方法的版本。(選方法)

  • 動(dòng)態(tài)分派:invokevirtual指令把類方法符號(hào)引用解析到不同直接引用上,來(lái)確定棧頂?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ù)期。程序不可能十全十美,我們要做的是通過(guò)各種指標(biāo)來(lái)衡量系統(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),第三步是通過(guò)工具來(lái)監(jiān)測(cè)指標(biāo),下一節(jié)提供了常用JVM調(diào)優(yōu)工具,可以通過(guò)不同工具的組合來(lái)發(fā)現(xiàn)定位問(wèn)題,結(jié)合JVM的工作機(jī)制已經(jīng)操作系統(tǒng)層面的調(diào)度流程,按圖索驥來(lái)發(fā)現(xiàn)問(wèn)題,找出問(wèn)題后才能進(jìn)行優(yōu)化。

7.1.4 原則

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

圖片來(lái)源《Java Performance》

7.2 調(diào)優(yōu)參數(shù)

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

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大小:

-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ù)制以解決問(wèn)題。

這就是JVM帶有一些參數(shù)的原因,這些參數(shù)將堆內(nèi)存轉(zhuǎn)儲(chǔ)到一個(gè)物理文件中,以后可以用它來(lái)查找泄漏:

-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:啟用“Server Hotspot VM”; 默認(rèn)情況下,此參數(shù)在64位JVM中使用

  • -XX:+ UseStringDeduplication: Java 8引入了這個(gè)JVM參數(shù),通過(guò)創(chuàng)建相同?String的太多實(shí)例來(lái)減少不必要的內(nèi)存使用?;?這通過(guò)將重復(fù)的?String值減少到單個(gè)全局char []數(shù)組來(lái)優(yōu)化堆內(nèi)存

  • -XX:+ UseLWPSynchronization:設(shè)置基于?LWP(輕量級(jí)進(jìn)程)的同步策略而不是基于線程的同步

  • -XX:LargePageSizeInBytes:設(shè)置用于Java堆的大頁(yè)面大小; 它采用GB / MB / KB的參數(shù); 通過(guò)更大的頁(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

    用來(lái)發(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)用程序的性能和資源消耗的信息。在診斷性能問(wèn)題時(shí),可以使用該工具,特別是與堆大小調(diào)整和垃圾回收相關(guān)的問(wè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ǔ)文件,通過(guò)瀏覽器訪問(wèn)分析文件

  • 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)。在診斷問(wèn)題方面,HPROF在分析性能,鎖爭(zhēng)用,內(nèi)存泄漏和其他問(wèn)題時(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ī)制,并無(wú)應(yīng)用其他JVM 高級(jí)特性,本章節(jié)我們開始從字節(jié)增強(qiáng)的層面介紹JVM的一些高級(jí)特性。

說(shuō)到字節(jié)增強(qiáng)我們最先想到的是字節(jié)碼,也就是本文最開頭所要研究的class文件,任何合法的源碼編譯成class后被類加載器加載進(jìn)JVM的方法區(qū),也就是以字節(jié)碼的形態(tài)存活在JVM的內(nèi)存空間。這也就是我們?yōu)槭裁船F(xiàn)有講明白類的結(jié)構(gòu)和加載過(guò)程,而字節(jié)碼增強(qiáng)技術(shù)不只是在內(nèi)存里面對(duì)class的字節(jié)碼進(jìn)行操縱,更為復(fù)雜的是class聯(lián)動(dòng)的上下游對(duì)象生命周期的管理。

首先我們回憶一下我們開發(fā)過(guò)程中最為熟悉的一個(gè)場(chǎng)景就是本地debug調(diào)試代碼。可能很多同學(xué)都已經(jīng)習(xí)慣在IDE上對(duì)某句代碼打上斷點(diǎn),然后逐步往下追蹤代碼執(zhí)行的步驟。我們進(jìn)一步想想,這個(gè)是怎么實(shí)現(xiàn)的,是一股什么樣的力量能把已經(jīng)跑起來(lái)的線程踩下剎車,一步一步往前挪?我們知道線程運(yùn)行其實(shí)就是在JVM的棧空間上不斷的把代碼對(duì)應(yīng)的JVM指令集不斷的送到CPU執(zhí)行。那能阻止這個(gè)流程的力量也肯定是發(fā)生在JVM范圍內(nèi),所以我們可以很輕松的預(yù)測(cè)到這肯定是JVM提供的機(jī)制,而不是IDE真的有這樣的能力,只不過(guò)是JVM把這種能力封裝成接口暴露出去,然后提供給IDE調(diào)用,而IDE只不過(guò)是通過(guò)界面交互來(lái)調(diào)用這些接口而已。那么下面我們就來(lái)介紹JVM這種重要的能力。

8.1JPDA

上面所講的JVM提供的程序運(yùn)行斷點(diǎn)能力,其實(shí)JVM提供的一個(gè)工具箱JVMTI(JVM TOOL Interface)提供的接口,而這個(gè)工具箱是一套叫做JPDA的架構(gòu)定義的,本節(jié)我們就來(lái)聊聊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è)層面來(lái)講解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)。也就是說(shuō)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)求來(lái)源,也就是說(shuō)只要調(diào)用JVMTI的請(qǐng)求方式和數(shù)據(jù)格式對(duì)了就可以,不論是來(lái)做遠(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)求來(lái)源,而且也沒限制語(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é)合起來(lái)時(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)程上。這樣一來(lái)外部請(qǐng)求就通過(guò)代理客戶端間接調(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)行過(guò)程中也是可以的,這個(gè)下文我們講字節(jié)碼增強(qiáng)還會(huì)再說(shuō)到。

JNIEXPORT jint JNICALL Agent_OnAttach(JavaVM* vm, char *options, void *reserved);

有興趣的同學(xué)可以自己動(dòng)手寫一個(gè)Agent試試,通過(guò)調(diào)用JVMTI接口可以實(shí)現(xiàn)自己定制化的調(diào)試工具。

JDWP

上文我們知道調(diào)用JVMTI需要建立一個(gè)代理客戶端,但是假如我建立了包含通用功能的Agent想開發(fā)出去給所有調(diào)試器使用,有一種方式是資深開發(fā)者通過(guò)閱讀我的文檔后進(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卻是也是通過(guò)建立代理客戶端來(lái)實(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)用的過(guò)程就是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)是通過(guò)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)程序,然后通過(guò)調(diào)試器啟動(dòng)程序之前,調(diào)試器會(huì)先獲取JVM管理器,然后通過(guò)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)境同步過(guò)來(lái),不然我們壓根就不知道要調(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定義三種鏈接器:啟動(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)試過(guò)程中對(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)有了深入理解,接下來(lái)我們就通過(guò)對(duì)這些樸素的原理來(lái)實(shí)現(xiàn)程序的斷點(diǎn)調(diào)試。當(dāng)然我們不會(huì)在這里介紹從IDE的UI斷點(diǎn)調(diào)試的過(guò)程,因?yàn)閷?duì)這套是使用已經(jīng)非常熟悉了,我們知道IDE的UI斷點(diǎn)調(diào)試本質(zhì)上是調(diào)試器客戶端對(duì)JDI的調(diào)用,那我們就通過(guò)一個(gè)調(diào)試的案例來(lái)解釋一下這背后的原理。

搭建服務(wù)

首先我們需要先搭建一個(gè)可供調(diào)試的web服務(wù),這里我首選springboot+來(lái)搭建,通過(guò)官網(wǎng)生成樣例project或者maven插件都可以,具體的太基礎(chǔ)的就不在這里演示,該服務(wù)只提供一個(gè)Controller包含的一個(gè)簡(jiǎn)單方法。如果使用Tomcat部署,則可以通過(guò)自有的開關(guān)catalina jpda start來(lái)啟動(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 {@GetMappingpublic String ask(@RequestParam("name") String name) {String message = "are you ok?" + name;return message;} }

啟動(dòng)服務(wù)

搭建好服務(wù)之后我們先啟動(dòng)服務(wù),我們通過(guò)maven來(lái)啟動(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:啟動(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)試程序無(wú)需等待直接運(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來(lái)接入我們的目標(biāo)程序。

#jdb 通過(guò)attach參數(shù)選擇本地目標(biāo)程序,同時(shí)附上目標(biāo)程序的源碼,回想之前我們講到的JDI的鏡像接口,就是把目標(biāo)程序的堆棧結(jié)構(gòu)同步過(guò)來(lái),如果能我們提供的源碼對(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) 無(wú)法設(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)?http-nio-7001-exec-5[1] print namename = "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)?http-nio-7001-exec-5[1] print namename = "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è)重要的彩蛋就是通過(guò)JPDA進(jìn)入調(diào)試模式,我們可以動(dòng)態(tài)的修改JVM內(nèi)存對(duì)象和類的內(nèi)容,這也講引出下文我們要介紹的字節(jié)碼增強(qiáng)技術(shù)。

8.2 熱替換

8.2.1概念

終于來(lái)到熱替換這節(jié)了,前文我們做了好多鋪墊,介紹熱替換之前我們稍稍回顧一下熱部署。我們知道熱部署是“獨(dú)立”于JVM之外的一門對(duì)類加載器應(yīng)用的技術(shù),通常是應(yīng)用容器借助自定義類加載器的迭代,無(wú)需重啟JVM缺能更新代碼從而達(dá)到熱部署,也就是說(shuō)熱部署是JVM之外容器提供的一種能力。而本節(jié)我們介紹的熱替換技術(shù)是實(shí)打?qū)岼VM提供的能力,是JVM提供的一種能夠?qū)崟r(shí)更新內(nèi)存類結(jié)構(gòu)的一種能力,這種實(shí)時(shí)更新JVM方法區(qū)類結(jié)構(gòu)的能力當(dāng)然也是無(wú)需重啟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ù),而且僅僅局限在方法體和變量的修改,無(wú)法動(dòng)態(tài)修改方法簽名或者增刪方法,因?yàn)樽止?jié)碼增強(qiáng)涉及到垃圾回收機(jī)制,類結(jié)構(gòu)變更,對(duì)象引用,即時(shí)編譯等復(fù)雜問(wèn)題。在HotSwap被引進(jìn)后至今,JCP也未能通過(guò)正式的字節(jié)碼增強(qiáng)實(shí)現(xiàn)。

JAVA是一門靜態(tài)語(yǔ)言,而字節(jié)碼增強(qiáng)所要達(dá)的效果就是讓Java像動(dòng)態(tài)語(yǔ)言一樣跑起來(lái),無(wú)需重啟服務(wù)器。下面我們介紹字節(jié)碼增強(qiáng)的基本原理。

  • 反射代理

    反射代理不能直接修改內(nèi)存方法區(qū)的字節(jié)碼,但是可以抽象出一層代理,通過(guò)內(nèi)存新增實(shí)例來(lái)實(shí)現(xiàn)類的更新

  • 原生接口

    jdk上層提供面向java語(yǔ)言的字節(jié)碼增強(qiáng)接口java.lang.instrument,通過(guò)實(shí)現(xiàn)ClassFileTransformer接口來(lái)操作JVM方法區(qū)的類文件字節(jié)碼。

  • JVMTI代理

    JVM的JVMTI接口包含了操作方法區(qū)類文件字節(jié)碼的函數(shù),通過(guò)創(chuàng)建代理,將JVMTI的指針JavaVM傳給代理,從而擁有JVM 本地操作字節(jié)碼的方法引用。

  • 類加載器織入

    字節(jié)碼增強(qiáng)接口加上類加載器的織入,結(jié)合起來(lái)也是一種熱替換技術(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è)冰山串起來(lái),在腦海中對(duì)JVM的觀感有更加立體的認(rèn)識(shí)。更近一步的認(rèn)識(shí)JVM對(duì)程序設(shè)計(jì)的功力提示大有裨益,而本文也只是將海平面上的冰山鏈接起來(lái),但這只是冰山一角,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í)。最后還是例牌來(lái)兩張圖結(jié)束JVM的介紹,希望對(duì)更的同學(xué)有幫助。

?

from:https://www.javazhiyin.com/34166.html?

總結(jié)

以上是生活随笔為你收集整理的JVM核心知识体系的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。

久草在线资源免费 | 超碰国产在线播放 | 久久久影院官网 | 久免费视频 | 国产高清中文字幕 | 久久国产经典 | 天堂久色 | 992tv在线成人免费观看 | 国产一线在线 | 在线小视频你懂得 | 免费观看www视频 | 国产精品69av | 久久精品视频18 | 九九热在线免费观看 | 色综合久久88 | 国产精品一区二区三区视频免费 | 人人插人人草 | 超碰人人av | 欧美精品国产精品 | 欧美在线视频精品 | 国产区av在线 | 激情欧美xxxx| 亚洲欧洲一区二区在线观看 | 久久在线观看视频 | 美女免费网视频 | 在线观看视频免费播放 | 亚洲精品女人久久久 | 免费黄色一区 | 婷婷网在线 | 国产精品密入口果冻 | 欧美精品久久久久久久久老牛影院 | 97久久精品午夜一区二区 | 成人av免费| 国产成人精品一区二区三区在线观看 | 国产午夜精品久久 | 日日操夜夜操狠狠操 | 黄网站污| av黄色在线观看 | 欧美激情综合五月 | 国产精品久久久久毛片大屁完整版 | 日韩欧美综合在线视频 | 日韩最新理论电影 | 五月天激情视频在线观看 | 亚洲免费av在线播放 | 久青草影院 | 久久伊人精品一区二区三区 | www.久久爱.cn | 欧美日韩综合在线 | 91av观看| 精品黄色在线 | 亚洲精品午夜视频 | 国产精品一区二区白浆 | 中文在线免费视频 | av大全在线看 | 国产永久免费高清在线观看视频 | 97成人精品区在线播放 | 久久黄色免费视频 | 久久精品欧美日韩精品 | 久草国产在线观看 | 区一区二区三在线观看 | 欧美资源在线观看 | 96精品在线 | 欧美精品亚洲二区 | 色婷婷av在线| 国产一区 在线播放 | 欧美大片www | 美国三级黄色大片 | 麻豆高清免费国产一区 | 日韩精品一区在线观看 | 久色小说| 韩国av免费在线 | 国产精品一区二区久久精品爱涩 | 在线日韩精品视频 | 日韩免费在线网站 | 天天操天天射天天操 | 国产不卡在线观看 | 青草视频免费观看 | 在线观看国产一区二区 | 国产生活一级片 | 成人性生交视频 | 99久久婷婷国产综合精品 | 精品黄色片 | 国产精品美女久久久久久久 | 国产在线视频导航 | 久久久人人爽 | 在线а√天堂中文官网 | 91视视频在线直接观看在线看网页在线看 | 欧美日韩观看 | 精品亚洲网 | 中文字幕在线观看第二页 | 亚洲伊人成综合网 | 精品在线一区二区 | 97视频精品 | 亚洲黄色成人av | 手机av在线不卡 | 91女神的呻吟细腰翘臀美女 | 国产精品久久久久久久久久ktv | 丝袜美腿在线播放 | 激情av网| 麻豆国产精品永久免费视频 | 开心色激情网 | 91亚洲精品乱码久久久久久蜜桃 | 国产精品麻豆三级一区视频 | 日韩电影在线视频 | 日韩综合在线观看 | 亚洲综合狠狠干 | 免费看黄色大全 | 中文字幕精品一区二区精品 | 黄色av高清 | 99久久毛片| 久久精品久久99 | 成人免费在线视频观看 | 六月丁香色婷婷 | 四虎成人精品 | 韩国av一区 | 91视频这里只有精品 | 国产色综合天天综合网 | 91成人精品国产刺激国语对白 | 高清一区二区三区 | 久久国产精品电影 | 亚洲成人av在线电影 | www日韩| 一二区av | 日韩成人黄色av | 国产亚洲人成网站在线观看 | 天天爱天天操天天射 | 成人亚洲精品国产www | 国产91精品一区二区麻豆亚洲 | 99久在线精品99re8热视频 | 亚洲久草在线 | 成年人黄色免费视频 | 欧美aaa级片| 97国产精品久久 | 久久亚洲热| 精品久久久久久久久久久久 | 青青草国产精品视频 | 91久久精品一区二区三区 | 色噜噜狠狠狠狠色综合久不 | 久久久久区 | 一级精品视频在线观看宜春院 | 99在线免费观看 | 草免费视频 | 婷婷国产精品 | 欧美最新大片在线看 | 国产成人性色生活片 | 99久久久久免费精品国产 | 亚洲一区美女视频在线观看免费 | 久久久久亚洲最大xxxx | 久久精品中文视频 | 在线观看你懂的网站 | 亚洲精品视频在线观看免费视频 | 超级碰碰碰视频 | 久草久视频| 最新久久久 | 少妇bbb搡bbbb搡bbbb | 亚洲国产一区二区精品专区 | 日日干网 | 久久精品九色 | 97人人模人人爽人人少妇 | av在线日韩 | 在线视频1卡二卡三卡 | 日韩精品免费在线 | 日韩高清成人 | 麻豆免费视频 | 97电影在线观看 | 国产日韩欧美在线影视 | 天天摸天天干天天操天天射 | 狠狠狠色 | 亚洲免费精品一区二区 | 玖玖视频免费在线 | jizz欧美性9 国产一区高清在线观看 | 久久久久亚洲精品国产 | 人人玩人人添人人澡超碰 | 三级黄色免费 | 日韩在线 一区二区 | 99热精品国产一区二区在线观看 | 久久精品99久久 | 在线国产视频一区 | 美女视频黄色免费 | 亚洲精品福利在线 | 日韩成人一级大片 | 国产一区在线视频 | 国产精品一区二区三区在线 | 国产精品视频免费 | 国产在线理论片 | 狠狠躁夜夜躁人人爽超碰97香蕉 | 亚洲成年人av | 久久精品观看 | 九九久| 亚洲女在线 | 一级黄色在线视频 | 亚洲男女精品 | 国产一二三区av | 91久久丝袜国产露脸动漫 | 午夜91在线 | 中文字幕在线乱 | 日韩大片在线 | 在线观看a视频 | 欧美激情综合五月 | 久久国产一区二区 | 日韩av成人在线观看 | 色婷婷成人网 | 一区二区三区在线电影 | 国产亚洲视频在线免费观看 | 久久99精品久久久久久秒播蜜臀 | 91精品国 | 亚洲欧洲av在线 | 日产中文字幕 | 精品国偷自产国产一区 | 日韩超碰 | 久久一区91 | 欧美一二三在线 | 日韩av电影国产 | 精品国产123 | 国产亚洲日本 | 国产色就色 | 久草在线视频免费资源观看 | 成人三级av | 成人亚洲精品久久久久 | 日韩黄色免费在线观看 | 国产va饥渴难耐女保洁员在线观看 | 日韩午夜一级片 | 一级片视频免费观看 | 最新国产一区二区三区 | 蜜桃久久久 | 日韩首页 | 亚洲精品乱码久久久久v最新版 | 国产3p视频 | a级国产乱理论片在线观看 特级毛片在线观看 | 玖玖在线资源 | 最新黄色av网址 | 婷婷色婷婷 | 国产亚洲精品久久久久久移动网络 | 亚洲精品久久久久久中文传媒 | 久久精品国产一区 | 国产精品自产拍在线观看中文 | 亚洲涩涩网站 | 在线a视频 | 久久久久久久久毛片 | 国产精品久久久久久一区二区三区 | 久久久伊人网 | 国产精品免费观看视频 | 人人插人人看 | av在线收看| 久久在线免费观看 | 亚洲视屏 | 精品国产诱惑 | 久久免费a | 国产精品理论片在线播放 | 欧美俄罗斯性视频 | 欧美精品日韩 | 国内毛片毛片 | 久久免费视频2 | 国产精品久久久久久久久久久久久 | 香蕉色综合 | 五月婷婷色综合 | 亚洲激情在线播放 | 四虎影视精品永久在线观看 | 五月情婷婷 | 亚洲精品国产精品国自产 | 国产精品9999| 特级黄录像视频 | 久久香蕉电影网 | 色香天天 | 色婷婷午夜 | 欧美乱大交 | 中文字幕在线观看免费高清完整版 | 日韩美女久久 | 国产精品入口麻豆www | 国产成人av在线 | 亚洲视频大全 | 久久精品99 | 国产超碰97 | 色av婷婷| 久久九九国产精品 | 日韩久久精品一区二区 | 精品国产视频在线 | 亚洲综合激情网 | 精品久久久久久亚洲综合网 | 国产精品久久久久久久久岛 | 97精品国产一二三产区 | 美女久久一区 | 国产精品免费不卡 | 成人午夜片av在线看 | 久久久黄视频 | 色综合久久久久久久久五月 | 色在线网站 | 亚洲精品国久久99热 | 在线视频 成人 | 国产vs久久 | 亚洲香蕉视频 | 在线观看国产中文字幕 | 夜夜躁狠狠躁日日躁视频黑人 | 女人18精品一区二区三区 | 国产精品成人aaaaa网站 | 欧美日韩免费一区二区三区 | 中国一区二区视频 | www激情久久 | 亚洲国产视频直播 | 欧美日韩国产在线 | 999亚洲国产996395 | 中文字幕不卡在线88 | 亚洲另类视频在线 | 国产三级精品三级在线观看 | 一区二区三区免费在线播放 | av网站免费线看精品 | 狠狠躁天天躁综合网 | 成片免费观看视频999 | 成人h视频在线播放 | 免费精品在线观看 | 香蕉视频久久久 | 亚洲第一香蕉视频 | 国产资源网站 | 日韩精品中文字幕有码 | 国产精品久久久久一区二区国产 | 国产一区二区三区免费视频 | 日本黄色免费电影网站 | 69性欧美| 亚洲国产黄色片 | 一区二区欧美在线观看 | 性日韩欧美在线视频 | 日日天天| 久久九九久久精品 | 天天躁日日躁狠狠 | 成人国产精品免费观看 | 一级免费av| 欧美乱码精品一区二区 | 日日夜夜人人天天 | 国产精品视频最多的网站 | 免费一级特黄毛大片 | 婷婷黄色片 | 久久久99国产精品免费 | 国产69久久久 | 精品欧美一区二区精品久久 | 麻豆精品91 | 久久艹在线观看 | 亚洲天天看 | 国产成人黄色在线 | 国产在线精品视频 | 日韩视频在线观看视频 | 亚州欧美精品 | 91色一区二区三区 | 久久免费视频4 | 日韩视频免费 | 日韩精品综合在线 | 亚洲三区在线 | 麻豆视频免费观看 | 国产免费国产 | 999在线视频 | 久草网在线视频 | 视频91| 精品久久久免费 | 免费看一级 | 最近2019年日本中文免费字幕 | 在线免费观看麻豆 | 欧美激情精品久久 | 久99久精品 | 中文字幕国产精品一区二区 | 成人免费观看在线视频 | 夜夜躁日日躁狠狠久久88av | 人人狠狠综合久久亚洲 | a视频在线 | 国产成人三级在线 | 在线黄色免费 | 国产精品欧美久久久久天天影视 | 在线色亚洲 | 久草在线综合 | 久久艹艹 | 中文字幕在线播放第一页 | 波多野结衣在线播放一区 | 中文字幕在线观看网站 | 日韩中文字幕第一页 | 国产免费不卡 | 免费视频91蜜桃 | 九九九毛片| 成av人电影 | 久久久久久久久久久久久久免费看 | 久久免费电影网 | 日本护士三级少妇三级999 | 午夜视频在线瓜伦 | 91伊人久久大香线蕉蜜芽人口 | 婷婷在线视频观看 | 精品999国产| 五月天九九 | 亚洲一片黄 | 亚洲国产一区av | 国产亚洲婷婷免费 | 亚洲天堂网视频 | 久久亚洲国产精品 | 精品视频亚洲 | 五月天综合网 | 国产色女 | av三级在线播放 | 午夜视频色 | 色美女在线 | 日本动漫做毛片一区二区 | 欧美日韩国产精品一区二区三区 | 久久久免费观看 | av在线一级 | 中文在线a√在线 | 成人h视频 | 日本黄色免费大片 | 日韩在线字幕 | 日韩免费在线视频观看 | 亚洲aⅴ免费在线观看 | 亚洲午夜av电影 | 黄色一级免费电影 | 精品视频免费 | 日韩动态视频 | 超碰人在线 | 97视频免费播放 | 91av欧美| 97狠狠操| 免费日p视频 | 超碰在线中文字幕 | 色中色亚洲 | 国产一区二区三区在线 | 黄av免费| 狠狠色噜噜狠狠 | 综合天堂av久久久久久久 | 一级a性色生活片久久毛片波多野 | 99久久精品国产毛片 | av免费观看高清 | www.成人久久 | 91在线入口 | 97电影手机版| 亚洲激情精品 | 国产91粉嫩白浆在线观看 | 久久综合色天天久久综合图片 | 中文字幕免费一区 | 国产一区二区三区免费视频 | 97福利| 91亚洲精品久久久蜜桃网站 | 99精品国产成人一区二区 | 国产亚洲精品久久久久久网站 | 国产一级精品在线观看 | 九九视频网站 | 九九热中文字幕 | 中文在线免费看视频 | 91精品老司机久久一区啪 | 国产污视频在线观看 | 欧美日韩一级视频 | 国产精品自拍av | 色婷婷视频网 | 狠狠色丁香婷婷综合久小说久 | 免费在线观看av | 欧洲精品二区 | 人人爽人人乐 | 欧美日韩在线观看视频 | 91试看| 久久久久久久久国产 | 操久久免费视频 | 久久精品网址 | 在线香蕉视频 | 久久9999久久免费精品国产 | 在线观看免费黄色 | 又黄又爽的视频在线观看网站 | 久草 | 三上悠亚一区二区在线观看 | www.亚洲黄色 | 欧美日韩一区二区在线观看 | 91pony九色丨交换 | 中文字幕在线久一本久 | 不卡视频在线看 | 亚洲香蕉视频 | 99久久久精品| 成人av在线一区二区 | 99精品视频在线观看视频 | 国产最新在线 | 91精品久久久久久久久久久久久 | 日韩久久久久 | 中文字幕在线观看免费高清完整版 | 免费在线观看国产黄 | 免费看国产曰批40分钟 | 国内精品亚洲 | 超碰在线9| 黄色成人免费电影 | 波多野结衣视频一区二区三区 | 色黄www小说 | 欧美日韩在线观看不卡 | 99免费在线播放99久久免费 | 久久久久久免费毛片精品 | 一区二区三区四区影院 | 国产精品国产三级国产aⅴ9色 | 中文在线a∨在线 | 五月综合激情婷婷 | 91精品国产三级a在线观看 | av黄色免费看 | 99视频偷窥在线精品国自产拍 | 国产专区第一页 | 欧美精品成人在线 | 91视视频在线直接观看在线看网页在线看 | 日韩精品视频免费在线观看 | 在线精品视频在线观看高清 | 久久一久久 | 福利网址在线观看 | 婷婷久久婷婷 | 日本高清久久久 | 国产精品99久久久久人中文网介绍 | 美女网站黄免费 | 久久草网| 国产做爰视频 | 久久av福利 | av不卡网站 | 成人啪啪18免费游戏链接 | 亚洲国产成人久久综合 | 日韩高清av在线 | 欧美成年黄网站色视频 | av大片免费看| 天天色天天 | 激情欧美在线观看 | 国产精品免费成人 | 久章草在线 | 久久精彩视频 | 在线观看av黄色 | 精品国产激情 | 福利av影院 | 成人一级在线观看 | 91超在线 | 成人在线观看网址 | 亚洲精品一区二区在线观看 | 亚洲不卡123 | 天天综合网国产 | 91精品啪在线观看国产线免费 | 精品一区二区三区在线播放 | 婷婷综合成人 | 中文字幕a在线 | 色偷偷88欧美精品久久久 | 久草观看视频 | 高清精品在线 | 欧美极品少妇xbxb性爽爽视频 | 国产精久久久久久久 | 国产啊v在线 | 亚洲激情六月 | 黄色www在线观看 | 啪啪凸凸 | 国产精品黄色影片导航在线观看 | 夜夜骑天天操 | 中文字幕网站视频在线 | 久久成人精品视频 | 999视频网站 | 黄网站色 | 久久新视频 | 91精品毛片 | 国产资源在线免费观看 | 五月婷影院 | 国产黄色精品 | 日韩特级片 | 国产亚洲成人网 | 国产视频久久久 | 免费亚洲视频在线观看 | 久香蕉| 久久精品免费 | 久久精品99国产精品亚洲最刺激 | 中文字幕在线观看亚洲 | 天天天操操操 | 九九热只有精品 | 日本最大色倩网站www | 免费观看日韩av | 成人午夜电影免费在线观看 | 日本精品视频在线观看 | 日本韩国精品一区二区在线观看 | 不卡的av片 | 日韩欧美成| 久久精品亚洲 | 成人影片在线播放 | 中文字幕在线乱 | 成人黄色在线看 | 国产欧美日韩一区 | japanesexxxhd奶水| 日本久久久久久 | 久久久久久久久久久福利 | 97人人射| 97视频播放| 国产精品破处视频 | 一区二区三区中文字幕在线观看 | 激情视频免费在线 | 在线成人高清电影 | 色香网 | 波多野结衣在线视频免费观看 | 国产高清视频色在线www | 亚洲91中文字幕无线码三区 | 亚洲三级在线 | 激情欧美在线观看 | 精品国产观看 | a级片久久久 | 国产精品成人在线观看 | 国产精品九九久久99视频 | 成年人在线观看视频免费 | 亚洲精品97 | 欧美午夜精品久久久久久浪潮 | 国产精品久久久av | 日韩视频在线播放 | 色九九影院 | 91男人影院 | 碰碰影院| 国产乱对白刺激视频在线观看女王 | 国产又粗又硬又长又爽的视频 | 国产精品美女久久久久久免费 | 中文字幕刺激在线 | 免费手机黄色网址 | 国产手机视频在线观看 | 丁香综合五月 | 欧美粗又大 | 日日操网 | 米奇狠狠狠888 | 日韩a级免费视频 | 日韩在线中文字幕视频 | 国产经典三级 | 亚洲在线黄色 | 五月开心六月婷婷 | 久久久网站 | 91精品国自产在线观看欧美 | 正在播放国产一区二区 | 精品国产自在精品国产精野外直播 | 欧美日韩精品国产 | 日本三级不卡 | 最近中文字幕高清字幕在线视频 | 国产精品一区二区62 | 久久天天躁夜夜躁狠狠85麻豆 | 在线观看一区二区精品 | 99热手机在线观看 | 国产一区二区三精品久久久无广告 | 激情六月婷婷久久 | 国产精品18久久久久白浆 | 久久爱影视i | 欧美精品一区二区蜜臀亚洲 | 色综合咪咪久久网 | 欧美91av| 久草在线观| 亚洲精品久久久久www | av片在线观看免费 | 久草视频网| 黄色av电影网 | 97精品国自产拍在线观看 | 国产一二三区在线观看 | 美女国内精品自产拍在线播放 | 激情影音 | 天天操夜夜逼 | 成人动漫视频在线 | 久久综合网色—综合色88 | 九九涩涩av台湾日本热热 | 91视频免费网址 | 久艹在线免费观看 | 日韩在线视频免费播放 | 在线亚洲日本 | 免费在线成人av | 欧美做受69 | 91视频链接| 在线有码中文 | 国产精品热 | 亚洲精品高清在线观看 | 日韩在线视频网 | 97超级碰碰碰碰久久久久 | 韩国在线一区二区 | 天天操导航 | 亚洲精品成人在线 | 99超碰在线观看 | 日本字幕网 | 丁香高清视频在线看看 | 色婷婷激情综合 | 亚洲精品18日本一区app | 九九久久在线看 | 国产九色视频在线观看 | 国产精品久久久久久五月尺 | 九九九在线观看 | www天天操 | 精品一二区 | 天天操夜夜逼 | 四虎8848免费高清在线观看 | 日韩欧美99 | 婷婷社区五月天 | 国产福利在线免费观看 | 欧美伦理一区二区三区 | 午夜在线观看一区 | 91精品91 | 国产网站在线免费观看 | 97成人在线观看视频 | 国产精品亚洲视频 | 欧美a√在线| 久久精品视频播放 | 国产精品久久久久久久久久尿 | 久久久免费 | 国产免费国产 | 久久久久亚洲精品男人的天堂 | 四虎在线免费观看 | 亚洲理论在线观看电影 | 欧美怡红院视频 | 91av综合 | 午夜在线资源 | 最新中文在线视频 | 黄色一级大片免费看 | 91高清视频 | 美女网站在线观看 | 中文字幕在线观看日本 | 少妇av片 | 国产午夜精品一区二区三区嫩草 | 中文字幕二区在线观看 | 色免费在线 | 日本天天操| 免费看黄在线观看 | 粉嫩av一区二区三区免费 | 欧美精品中文在线免费观看 | 亚洲爽爽网| 最新国产精品久久精品 | 欧美日本一二三 | 天天干天天干天天色 | 在线视频观看成人 | 国产不卡高清 | 啪啪免费观看网站 | 欧美 日韩 国产 成人 在线 | 中文字幕在线观看第一页 | 午夜 在线 | 日日草夜夜操 | 亚洲精品乱码久久久久久写真 | a在线观看国产 | 亚洲精品国产精品国自产观看 | 久久成人久久 | 激情深爱五月 | 欧美精品久久久久久久久久丰满 | 欧美不卡视频在线 | 国产精品女 | 婷婷色五 | 操操操日日 | 又黄又爽又色无遮挡免费 | 制服丝袜天堂 | 久久理伦片 | 在线视频1卡二卡三卡 | 久久69精品| 日韩精品一区二区三区三炮视频 | 国产精久久 | 玖玖爱在线观看 | 91.麻豆视频| 色婷婷免费视频 | 色综合天天天天做夜夜夜夜做 | 国产精品嫩草影视久久久 | 亚洲砖区区免费 | 日日碰狠狠躁久久躁综合网 | 又黄又爽又无遮挡免费的网站 | 中文字幕免费在线 | 亚洲国产中文字幕在线视频综合 | 日韩激情精品 | 国产在线理论片 | 国产麻豆果冻传媒在线观看 | 国产小视频在线播放 | 亚洲高清视频在线 | 久久久久久99精品 | 3d黄动漫免费看 | 在线观看国产91 | 久久精品99北条麻妃 | 久久精品国产一区二区 | 日日干综合 | 亚洲视频免费在线观看 | 久久久久久久影视 | 久久视精品 | 久久精品99国产 | 中文字幕av全部资源www中文字幕在线观看 | 婷婷中文字幕在线观看 | 亚洲一区二区精品视频 | 亚洲精品视频在线 | 最新日韩在线 | 国产麻豆视频免费观看 | 国产成人精品在线播放 | 91热爆在线观看 | 日韩理论在线播放 | 亚洲韩国一区二区三区 | 久久精品日韩 | 国产视频精品免费播放 | 韩国精品福利一区二区三区 | 亚洲欧洲美洲av | 又黄又刺激视频 | 精品一区二区免费在线观看 | 久久 国产一区 | 午夜视频在线观看一区 | 91自拍视频在线观看 | 久久精品福利视频 | 日本久久视频 | 免费观看一区二区 | 亚洲国产中文字幕 | 久久久久欠精品国产毛片国产毛生 | 亚洲精品国偷自产在线99热 | 国产裸体视频bbbbb | 色的网站在线观看 | 日韩亚洲国产精品 | 国产视频久久久 | 麻豆传媒在线免费看 | 国产无套精品久久久久久 | 成人一级 | 在线中文字幕av观看 | 国产99久久久精品视频 | 亚洲精品高清视频在线观看 | 在线草 | 婷婷深爱| 99精品热视频 | 日韩精品五月天 | 亚洲成人国产 | 久久婷婷色 | 免费一级片久久 | 亚洲香蕉在线观看 | 在线成人免费电影 | 中文字幕在线播放一区二区 | 国产精品男女视频 | 国产精品久久av | 欧洲成人av | 国产黑丝袜在线 | 日本乱视频 | 天天超碰 | 在线观看精品一区 | 在线免费精品视频 | 成人h动漫在线看 | 91成人精品一区在线播放 | 亚洲一区二区三区精品在线观看 | 欧美人交a欧美精品 | 亚洲在线精品视频 | 午夜精品一区二区三区在线 | 久久久久国产免费免费 | 日韩com| 久久精品79国产精品 | 丁香 久久 综合 | 久久久精品日本 | 日韩av线观看 | 中国一级片视频 | 美女视频黄是免费的 | 亚洲精品乱码久久久久久久久久 | 探花视频免费观看高清视频 | 亚洲午夜大片 | 99久久日韩精品免费热麻豆美女 | 中文字幕专区高清在线观看 | 国产成人无码AⅤ片在线观 日韩av不卡在线 | 99久久精品免费看 | 久久高清国产视频 | 国产日韩欧美在线影视 | 天天拍天天色 | 色婷婷电影网 | 中文字幕视频在线播放 | 综合激情网| 久久精品毛片基地 | 亚洲视频在线视频 | 91 在线视频 | 亚洲国产午夜视频 | 久久久久99精品成人片三人毛片 | 久久综合狠狠综合久久综合88 | 丁香激情视频 | 97视频免费观看 | 久久久九色精品国产一区二区三区 | 波多野结衣电影一区二区三区 | 在线你懂的视频 | 免费高清在线视频一区· | 久久国产精品99国产精 | 亚洲高清视频在线观看免费 | 亚洲成av人电影 | 亚洲成人免费观看 | 日韩免费在线视频 | 国产免费观看久久 | 五月天综合网站 | 奇米影视777影音先锋 | 丁香激情综合久久伊人久久 | 免费高清在线一区 | 亚洲人成影院在线 | 久久精品久久国产 | 亚洲精品男人的天堂 | 在线国产日本 | 日韩高清免费电影 | 麻花传媒mv免费观看 | 人人插人人澡 | 欧美日韩精品久久久 | 精品视频免费观看 | 国产精品人成电影在线观看 | 亚洲天堂网站视频 | av资源免费在线观看 | 亚洲日本三级 | 成年人在线电影 | 国产日韩一区在线 | 18久久久久 | 狠狠躁日日躁 | 欧美日本一二三 | 国产一区欧美日韩 | 啪啪肉肉污av国网站 | 日本论理电影 | 精品一二区 | 国产做爰视频 | av在线播放国产 | 久久久综合九色合综国产精品 | 一级一级一片免费 | 99精品在线直播 | 日韩精品一区二区三区在线视频 | 女人18毛片a级毛片一区二区 | 久久夜色精品国产欧美一区麻豆 | 久久综合国产伦精品免费 | 丁香在线观看完整电影视频 | 国产一区二区免费看 | 国产盗摄精品一区二区 | 国产精品一区二区果冻传媒 | 中文字幕在线观看视频一区二区三区 | 天堂av在线网站 | 91一区一区三区 | 在线观看免费一级片 | 成人黄色小视频 | 91在线影院 | 亚洲日本va中文字幕 | 日本巨乳在线 | 久久蜜臀一区二区三区av | 天天干天天摸天天操 | 久久99国产精品自在自在app | 亚洲成人黄色网址 | 国产一级片免费播放 | 婷婷五月在线视频 | 国产中文视 | 国内精品久久久久久中文字幕 | 亚洲专区路线二 | 深爱开心激情 | 久久99国产精品二区护士 | 超碰在线人人97 | 91天堂在线观看 | 中文字幕av免费 | 永久免费在线 | 色五月色开心色婷婷色丁香 | 在线一二三四区 | 亚洲精品在线观看不卡 | 亚州性色 | 婷婷亚洲激情 | 久草精品在线观看 | 欧美日韩性视频 | 97超碰在线久草超碰在线观看 | 在线观看国产福利片 | 狠狠操导航| 精品国产乱码久久久久久久 | 99综合电影在线视频 | 人人澡人摸人人添学生av | 一区二区精品在线 | 亚洲成人av一区二区 | 999久久久免费精品国产 | 亚洲九九九 | 美女国产网站 | 六月婷婷久香在线视频 | 激情久久综合网 | 99精品久久久久久久久久综合 | 亚洲国产成人在线播放 | 五月婷婷激情网 | 欧美做受69 | 国产福利在线免费 | 国产在线精品视频 | 国产日韩欧美视频在线观看 | 九九久久国产精品 | 亚洲国产精品va在线看 | 狠狠操狠狠干天天操 | 免费看的毛片 | 99精品在线看 | 美女网站在线播放 | 国产麻豆电影在线观看 | 天天综合导航 | 日韩在线观看精品 | 91av电影| 亚洲禁18久人片 | 久久久毛片 | 91香蕉视频色版 | 在线视频婷婷 | 在线性视频日韩欧美 | 91精品国产自产91精品 | 综合色站| 亚洲在线视频观看 | 91精品福利在线 | 国产日韩在线看 | 日韩av在线不卡 | 一区在线观看 | 91av视频在线播放 | 狠狠狠色丁香综合久久天下网 | 久久视频这里只有精品 | 粉嫩av一区二区三区四区 | 久色婷婷| 亚洲国产精品va在线 | 亚洲第一区在线播放 | 久久高清片 | 天天色综合天天 | 日韩精品高清不卡 | 在线欧美最极品的av | www视频在线免费观看 | 欧美成人精品欧美一级乱 | 麻豆va一区二区三区久久浪 | 久久精品99国产精品亚洲最刺激 | 国产精品久久久久四虎 | 成人午夜精品久久久久久久3d | 日本在线观看中文字幕 | 久久国产精品久久久久 | 色插综合 | 中文字幕人成一区 | 日日干天天操 | 国产精品欧美久久久久久 | 欧美精品亚洲二区 | 视频在线观看一区 | 精品国模一区二区三区 | 色狠狠综合天天综合综合 | 在线观看 国产 | 免费久久网 | 米奇四色影视 | 美女在线黄| 97成人精品视频在线播放 | 青青看片 |