類結構,類加載器,加載,連接,初始化,雙親委派,熱部署,隔離,堆,棧,方法區,計數器,內存回收,執行引擎,調優工具,JVMTI,JDWP,JDI,熱替換,字節碼,ASM,CGLIB,DCEVMhtml
做爲三大工業級別語言之一的JAVA如此受企業青睞有加,離不開她背後JVM的默默復出。只是因爲JAVA過於成功以致於咱們經常忘了JVM平臺上還運行着像Clojure/Groovy/Kotlin/Scala/JRuby/Jython這樣的語言。咱們享受着JVM帶來跨平臺「一次編譯處處執行」臺的便利和自動內存回收的安逸。本文從JVM的最小元素類的結構出發,介紹類加載器的工做原理和應用場景,思考類加載器存在的意義。進而描述JVM邏輯內存的分佈和管理方式,同時列舉經常使用的JVM調優工具和使用方法,最後介紹高級特性JDPA框架和字節碼加強技術,實現熱替換。從微觀到宏觀,從靜態到動態,從基礎到高階介紹JVM的知識體系。java
咱們知道不僅JAVA文本文件,像Clojure/Groovy/Kotlin/Scala這些文本文件也一樣會通過JDK的編譯器編程成class文件。進入到JVM領域後,其實就跟JAVA沒什麼關係了,JVM只認得class文件,那麼咱們須要先了解class這個黑箱裏麪包含的是什麼東西。git
JVM規範嚴格定義了CLASS文件的格式,有嚴格的數據結構,下面咱們能夠觀察一個簡單CLASS文件包含的字段和數據類型。github
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]; }
詳細的描述咱們能夠從JVM規範說明書裏面查閱類文件格式,類的總體佈局以下圖展現的。web
在個人理解,我想把每一個CLASS文件類別成一個一個的數據庫,裏面包含的常量池/類索引/屬性表集合就像數據庫的表,並且表之間也有關聯,常量池則存放着其餘表所須要的全部字面量。瞭解完類的數據結構後,咱們須要來觀察JVM是如何使用這些從硬盤上或者網絡傳輸過來的CLASS文件。算法
在咱們探究JVM如何使用CLASS文件以前,咱們快速回憶一下編寫好的C語言文件是如何執行的?咱們從C的HelloWorld入手看看先。spring
#include <stdio.h> int main() { /* my first program in C */ printf("Hello, World! \n"); return 0; }
編輯完保存爲hello.c文本文件,而後安裝gcc編譯器(GNU C/C++)shell
$ gcc hello.c $ ./a.out Hello, World!
這個過程就是gcc編譯器將hello.c文本文件編譯成機器指令集,而後讀取到內存直接在計算機的CPU運行。從操做系統層面看的話,就是一個進程的啓動到結束的生命週期。數據庫
下面咱們看JAVA是怎麼運行的。學習JAVA開發的第一件事就是先下載JDK安裝包,安裝完配置好環境變量,而後寫一個名字爲helloWorld的類,而後編譯執行,咱們來觀察一下發生了什麼事情?apache
先看源碼,有夠簡單了吧。
package com.zooncool.example.theory.jvm; /** * Created with IntelliJ IDEA. User: linzhenhua Date: 2019/1/3 Time: 11:56 PM * @author linzhenhua */ public class HelloWorld { public static void main(String[] args) { System.out.println("my classLoader is " + HelloWorld.class.getClassLoader()); } }
編譯執行
$ javac src/main/java/com/zooncool/example/theory/jvm/HelloWorld.java $ java -cp src/main/java/ com.zooncool.example.theory.jvm.HelloWorld my classLoader is sun.misc.Launcher$AppClassLoader@2a139a55
對比C語言在命令行直接運行編譯後的a.out二進制文件,JAVA的則是在命令行執行java classFile,從命令的區別咱們知道操做系統啓動的是java進程,而HelloWorld類只是命令行的入參,在操做系統來看java也就是一個普通的應用進程而已,而這個進程就是JVM的執行形態(JVM靜態就是硬盤裏JDK包下的二進制文件集合)。
學習過JAVA的都知道入口方法是public static void main(String[] args),缺一不可,那我猜執行java命令時JVM對該入口方法作了惟一驗證,經過了才容許啓動JVM進程,下面咱們來看這個入口方法有啥特色。
去掉public限定
$ javac src/main/java/com/zooncool/example/theory/jvm/HelloWorld.java $ java -cp src/main/java/ com.zooncool.example.theory.jvm.HelloWorld 錯誤: 在類 com.zooncool.example.theory.jvm.HelloWorld 中找不到 main 方法, 請將 main 方法定義爲: public static void main(String[] args) 不然 JavaFX 應用程序類必須擴展javafx.application.Application
說名入口方法須要被public修飾,固然JVM調用main方法是底層的JNI方法調用不受修飾符影響。
去掉static限定
$ javac src/main/java/com/zooncool/example/theory/jvm/HelloWorld.java $ java -cp src/main/java/ com.zooncool.example.theory.jvm.HelloWorld 錯誤: main 方法不是類 com.zooncool.example.theory.jvm.HelloWorld 中的static, 請將 main 方法定義爲: public static void main(String[] args)
咱們是從類對象調用而不是類建立的對象才調用,索引須要靜態修飾
返回類型改成int
$ javac src/main/java/com/zooncool/example/theory/jvm/HelloWorld.java $ java -cp src/main/java/ com.zooncool.example.theory.jvm.HelloWorld 錯誤: main 方法必須返回類 com.zooncool.example.theory.jvm.HelloWorld 中的空類型值, 請 將 main 方法定義爲: public static void main(String[] args)
void返回類型讓JVM調用後無需關心調用者的使用狀況,執行完就中止,簡化JVM的設計。
方法簽名改成main1
$ javac src/main/java/com/zooncool/example/theory/jvm/HelloWorld.java $ java -cp src/main/java/ com.zooncool.example.theory.jvm.HelloWorld 錯誤: 在類 com.zooncool.example.theory.jvm.HelloWorld 中找不到 main 方法, 請將 main 方法定義爲: public static void main(String[] args) 不然 JavaFX 應用程序類必須擴展javafx.application.Application
這個我也不清楚,多是約定俗成吧,畢竟C/C++也是用main方法的。
說了這麼多main方法的規則,其實咱們關心的只有兩點:
關於JVM如何使用HelloWorld下文咱們會詳細講到。
咱們知道JVM是由C/C++語言實現的,那麼JVM跟CLASS打交道則須要JNI(Java Native Interface)這座橋樑,當咱們在命令行執行java時,由C/C++實現的java應用經過JNI找到了HelloWorld裏面符合規範的main方法,而後開始調用。咱們來看下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);
上一節咱們留了一個核心的環節,就是JVM在執行類的入口以前,首先得找到類再而後再把類裝到JVM實例裏面,也便是JVM進程維護的內存區域內。咱們固然知道是一個叫作類加載器的工具把類加載到JVM實例裏面,拋開細節從操做系統層面觀察,那麼就是JVM實例在運行過程當中經過IO從硬盤或者網絡讀取CLASS二進制文件,而後在JVM管轄的內存區域存放對應的文件。咱們目前還不知道類加載器的實現,可是咱們從功能上判斷無非就是讀取文件到內存,這個是很普通也很簡單的操做。
若是類加載器是C/C++實現的話,那麼大概就是以下代碼就能夠實現
char *fgets( char *buf, int n, FILE *fp );
若是是JAVA實現,那麼也很簡單
InputStream f = new FileInputStream("theory/jvm/HelloWorld.class");
從操做系統層面看的話,若是隻是加載,以上代碼就足以把類文件加載到JVM內存裏面了。可是結果就是亂糟糟的把一堆毫無秩序的類文件往內存裏面扔,沒有良好的管理也無法用,因此須要咱們須要設計一套規則來管理存放內存裏面的CLASS文件,咱們稱爲類加載的設計模式或者類加載機制,這個下文會重點解釋。
根據官網的定義A class loader is an object that is responsible for loading classes. 類加載器就是負責加載類的。咱們知道啓動JVM的時候會把JRE默認的一些類加載到內存,這部分類使用的加載器是JVM默認內置的由C/C++實現的,好比咱們上文加載的HelloWorld.class。可是內置的類加載器有明確的範圍限定,也就是隻能加載指定路徑下的jar包(類文件的集合)。若是隻是加載JRE的類,那可玩的花樣就少不少,JRE只是提供了底層所需的類,更多的業務須要咱們從外部加載類來支持,因此咱們須要指定新的規則,以方便咱們加載外部路徑的類文件。
Bootstrap class loader
做用:啓動類加載器,加載JDK核心類
類加載器:C/C++實現
類加載路徑: <java_home>/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 /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/sunrsasig.jar /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/jsse.jar /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/jce.jar /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/charsets.jar /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/jfr.jar /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/classes
實現原理:本地方法由C++實現
Extensions class loader
做用:擴展類加載器,加載JAVA擴展類庫。
類加載器:JAVA實現
類加載路徑:<java_home>/jre/lib/ext
System.out.println(System.getProperty("java.ext.dirs")); /Users/linzhenhua/Library/Java/Extensions: /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/ext: /Library/Java/Extensions: /Network/Library/Java/Extensions: /System/Library/Java/Extensions: /usr/lib/java
實現原理:擴展類加載器ExtClassLoader本質上也是URLClassLoader
Launcher.java
//構造方法返回擴展類加載器 public Launcher() { //定義擴展類加載器 Launcher.ExtClassLoader var1; try { //一、獲取擴展類加載器 var1 = Launcher.ExtClassLoader.getExtClassLoader(); } catch (IOException var10) { throw new InternalError("Could not create extension class loader", var10); } ... } //擴展類加載器 static class ExtClassLoader extends URLClassLoader { private static volatile Launcher.ExtClassLoader instance; //二、獲取擴展類加載器實現 public static Launcher.ExtClassLoader getExtClassLoader() throws IOException { if (instance == null) { Class var0 = Launcher.ExtClassLoader.class; synchronized(Launcher.ExtClassLoader.class) { if (instance == null) { //三、構造擴展類加載器 instance = createExtClassLoader(); } } } return instance; } //四、構造擴展類加載器具體實現 private static Launcher.ExtClassLoader createExtClassLoader() throws IOException { try { return (Launcher.ExtClassLoader)AccessController.doPrivileged(new PrivilegedExceptionAction<Launcher.ExtClassLoader>() { public Launcher.ExtClassLoader run() throws IOException { //五、獲取擴展類加載器加載目標類的目錄 File[] var1 = Launcher.ExtClassLoader.getExtDirs(); int var2 = var1.length; for(int var3 = 0; var3 < var2; ++var3) { MetaIndex.registerDirectory(var1[var3]); } //七、構造擴展類加載器 return new Launcher.ExtClassLoader(var1); } }); } catch (PrivilegedActionException var1) { throw (IOException)var1.getException(); } } //六、擴展類加載器目錄路徑 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