初探JVM,只需要這篇文章!

  • 2019 年 10 月 10 日
  • 筆記

1 官網

1.1 尋找JDK文檔過程

www.oracle.com -> 右下角Product Documentation -> 往下拉選擇Java -> Java SE documentation-> Previous releases -> JDK 8 -> 此時定位到:https://docs.oracle.com/javas…

1.2 The relation of JDK/JRE/JVM

Reference -> Developer Guides -> 定位到:https://docs.oracle.com/javas…

Oracle has two products that implement Java Platform Standard Edition (Java SE) 8: Java SE Development Kit (JDK) 8 and Java SE Runtime Environment (JRE) 8.

JDK 8 is a superset of JRE 8, and contains everything that is in JRE 8, plus tools such as the compilers and debuggers necessary for developing applets and applications. JRE 8 provides the libraries, the Java Virtual Machine (JVM), and other components to run applets and applications written in the Java programming language. Note that the JRE includes components not required by the Java SE specification, including both standard and non-standard Java components.

2 源碼到類文件

2.1 源碼

···   java   class Person{   private String name;   private int age;   private static String address;   private final static String hobby="Programming";   public void say(){      System.out.println("person say...");   }   public int calc(int op1,int op2){      return op1+op2;   }   }  ···

編譯: javac Person.java —> Person.class

2.2 編譯過程

Person.java -> 詞法分析器 -> tokens流 -> 語法分析器 -> 語法樹/抽象語法樹 -> 語義分析器

-> 註解抽象語法樹 -> 位元組碼生成器 -> Person.class文件

2.3 類文件(Class文件)

官網TheclassFileFormat:

https://docs.oracle.com/javas…

cafe babe 0000 0034 0027 0a00 0600 1809

0019 001a 0800 1b0a 001c 001d 0700 1e07

001f 0100 046e 616d 6501 0012 4c6a 6176

612f 6c61 6e67 2f53 7472 696e 673b 0100

0361 6765 0100 0149 0100 0761 6464 7265

……

magic(魔數):

The magic item supplies the magic number identifying the class file format; it has the value 0xCAFEBABE.

cafe babe

minor_version, major_version

0000 0034 對應10進位的52,代表JDK 8中的一個版本

constant_pool_count

0027 對應十進位27,代表常量池中27個常量

```   1 ClassFile {   2     u4             magic;   3     u2             minor_version;   4     u2             major_version;   5     u2             constant_pool_count;   6     cp_info        constant_pool[constant_pool_count-1];   7     u2             access_flags;   8     u2             this_class;   9     u2             super_class;  10     u2             interfaces_count;  11     u2             interfaces[interfaces_count];  12     u2             fields_count;  13     field_info     fields[fields_count];  14     u2             methods_count;  15     method_info    methods[methods_count];  16     u2             attributes_count;  17     attribute_info attributes[attributes_count];  18 }  ```

.class位元組碼文件

魔數與class文件版本

常量池

訪問標誌

類索引、父類索引、介面索引

欄位表集合

方法表集合

屬性表集合

2.4 javap文件分解器

“`

javap -c Person.class > Person.txt

“`

查看位元組碼資訊:結構資訊/元數據/方法資訊

Compiled from "Person.java"

“`

1 class Person {

2 Person();

3 Code:

4 0: aload_0

5 1: invokespecial #1 // Method java/lang/Object."<init>":()V

6 4: return

“`

“`

public void say();

Code:

0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;

3: ldc #3 // String person say…

5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V

8: return

“`

“`

public int calc(int, int);

Code:

0: iload_1

1: iload_2

2: iadd

3: ireturn

}

“`

3 類文件到虛擬機(類載入機制)

類載入機制

虛擬機把Class文件載入到記憶體

並對數據進行校驗,轉換解析和初始化

形成可以虛擬機直接使用的Java類型,即java.lang.Class

3.1 裝載(Load)

查找和導入class文件

(1)通過一個類的全限定名獲取定義此類的二進位位元組流

(2)將這個位元組流所代表的靜態存儲結構轉化為方法區的運行時數據結構

(3)在Java堆中生成一個代表這個類的java.lang.Class對象,作為對方法區中這些數據的訪問入口

3.2 鏈接(Link)

3.2.1 驗證(Verify)

保證被載入類的正確性

文件格式驗證

元數據驗證

位元組碼驗證

符號引用驗證

3.2.2 準備(Prepare)

為類的靜態變數分配記憶體,並將其初始化為默認值

3.2.3 解析(Resolve)

把類中的符號引用轉換為直接引用

3.3 初始化(Initialize)

對類的靜態變數,靜態程式碼塊執行初始化操作

3.4 類載入機製圖解

使用和卸載不算是類載入過程中的階段,只是畫完整了一下

4 類裝載器ClassLoader

在裝載(Load)階段,其中第(1)步:通過類的全限定名獲取其定義的二進位位元組流,需要藉助類裝載器完成,顧名思義,就是用來裝載Class文件的。

(1)通過一個類的全限定名獲取定義此類的二進位位元組流

4.1 分類

Bootstrap ClassLoader 負責載入$JAVA_HOME中 jre/lib/rt.jar里所有的class或Xbootclassoath選項指定的jar包。由C++實現,不是ClassLoader子類。

Extension ClassLoader負責載入java平台中擴展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar 或-Djava.ext.dirs指定目錄下的jar包。

App ClassLoader 負責載入classpath中指定的jar包及 Djava.class.path所指定目錄下的類和jar包。

Custom ClassLoader通過java.lang.ClassLoader的子類自定義載入class,屬於應用程式根據自身需要自定義的ClassLoader,如tomcat、jboss都會根據j2ee規範自行實現ClassLoader。

4.2 圖解

4.3 載入原則

檢查某個類是否已經載入:順序是自底向上,從Custom ClassLoader到BootStrap ClassLoader逐層檢查,只要某個Classloader已載入,就視為已載入此類,保證此類只所有ClassLoader載入一次。

載入的順序:載入的順序是自頂向下,也就是由上層來逐層嘗試載入此類。

雙親委派機制

定義:如果一個類載入器在接到載入類的請求時,它首先不會自己嘗試去載入這個類,而是把這個請求任務委託給父類載入器去完成,依次遞歸,如果父類載入器可以完成類載入任務,就成功返回;只有父類載入器無法完成此載入任務時,才自己去載入。

優勢:Java類隨著載入它的類載入器一起具備了一種帶有優先順序的層次關係。比如,Java中的Object類,它存放在rt.jar之中,無論哪一個類載入器要載入這個類,最終都是委派給處於模型最頂端的啟動類載入器進行載入,因此Object在各種類載入環境中都是同一個類。如果不採用雙親委派模型,那麼由各個類載入器自己取載入的話,那麼系統中會存在多種不同的Object類。

5 運行時數據區(Run-Time Data Areas)

在裝載階段的第(2),(3)步可以發現有運行時數據,堆,方法區等名詞

(2)將這個位元組流所代表的靜態存儲結構轉化為方法區的運行時數據結構

(3)在Java堆中生成一個代表這個類的java.lang.Class對象,作為對方法區中這些數據的訪問入口

說白了就是類文件被類裝載器裝載進來之後,類中的內容(比如變數,常量,方法,對象等這些數據得要有個去處,也就是要存儲起來,存儲的位置肯定是在JVM中有對應的空間)

5.1 官網概括

https://docs.oracle.com/javas…

Summary

The Java Virtual Machine defines various run-time data areas that are used during execution of a program. Some of these data areas are created on Java Virtual Machine start-up and are destroyed only when the Java Virtual Machine exits. Other data areas are per thread. Per-thread data areas are created when a thread is created and destroyed when the thread exits.

5.2 圖解

5.3 常規理解

5.3.1 Method Area(方法區)

方法區是各個執行緒共享的記憶體區域,在虛擬機啟動時創建。

用於存儲已被虛擬機載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼等數據。

雖然Java虛擬機規範把方法區描述為堆的一個邏輯部分,但是它卻又一個別名叫做Non-Heap(非堆),目的是與Java堆區分開來。

當方法區無法滿足記憶體分配需求時,將拋出OutOfMemoryError異常。

The Java Virtual Machine has a method area that is shared among all Java Virtual Machine threads. The method area is created on virtual machine start-up.

Although the method area is logically part of the heap,…… If memory in the method area cannot be made available to satisfy an allocation request, the Java Virtual Machine throws an OutOfMemoryError.

此時回看裝載階段的第2步:

(2)將這個位元組流所代表的靜態存儲結構轉化為方法區的運行時數據結構

如果這時候把從Class文件到裝載的第(1)和(2)步合併起來理解的話,可以畫個圖

值得說明的

(1)方法區在JDK 8中就是Metaspace,在JDK6或7中就是Perm Space

(2)Run-Time Constant Pool

Class文件中除了有類的版本、欄位、方法、介面等描述資訊外,還有一項資訊就是常量池,用於存放編譯時期生成的各種字面量和符號引用,這部分內容將在類載入後進入方法區的運行時常量池中存放。

Each run-time constant pool is allocated from the Java Virtual Machine's method area (§2.5.4).s

5.3.2 Heap(堆)

Java堆是Java虛擬機所管理記憶體中最大的一塊,在虛擬機啟動時創建,被所有執行緒共享。

Java對象實例以及數組都在堆上分配。

The Java Virtual Machine has a heap that is shared among all Java Virtual Machine threads. The heap is the run-time data area from which memory for all class instances and arrays is allocated.

The heap is created on virtual machine start-up.

此時回看裝載階段的第3步:

(3)在Java堆中生成一個代表這個類的java.lang.Class對象,作為對方法區中這些數據的訪問入口

此時裝載(1)(2)(3)的圖可以改動一下

5.3.3 Java Virtual Machine Stacks(虛擬機棧)

經過上面的分析,類載入機制的裝載過程已經完成,後續的鏈接,初始化也會相應的生效。

假如目前的階段是初始化完成了,後續做啥呢?肯定是Use使用咯,不用的話這樣折騰來折騰去有什麼意義?那怎樣才能被使用到?換句話說裡面內容怎樣才能被執行?比如通過主函數main調用其他方法,這種方式實際上是main執行緒執行之後調用的方法,即要想使用裡面的各種內容,得要以執行緒為單位,執行相應的方法才行。

那一個執行緒執行的狀態如何維護?一個執行緒可以執行多少個方法?這樣的關係怎麼維護呢?

虛擬機棧是一個執行緒執行的區域,保存著一個執行緒中方法的調用狀態。換句話說,一個Java執行緒的運行狀態,由一個虛擬機棧來保存,所以虛擬機棧肯定是執行緒私有的,獨有的,隨著執行緒的創建而創建。

每一個被執行緒執行的方法,為該棧中的棧幀,即每個方法對應一個棧幀。

調用一個方法,就會向棧中壓入一個棧幀;一個方法調用完成,就會把該棧幀從棧中彈出。

Each Java Virtual Machine thread has a private Java Virtual Machine stack, created at the same time as the thread. A Java Virtual Machine stack stores frames (§2.6).

畫圖理解棧和棧幀

棧幀:每個棧幀對應一個被調用的方法,可以理解為一個方法的運行空間。

A frame is used to store data and partial results, as well as to perform dynamic linking, return values for methods, and dispatch exceptions.

A new frame is created each time a method is invoked. A frame is destroyed when its method invocation completes, whether that completion is normal or abrupt (it throws an uncaught exception).

Note that a frame created by a thread is local to that thread and cannot be referenced by any other thread.

每個棧幀中包括局部變數表(Local Variables)、操作數棧(Operand Stack)、指向運行時常量池的引用(A reference to the run-time constant pool)、方法返回地址(Return Address)和附加資訊。

局部變數表:方法中定義的局部變數以及方法的參數存放在這張表中局部變數表中的變數不可直接使用,如需要使用的話,必須通過相關指令將其載入至操作數棧中作為操作數使用。

操作數棧:以壓棧和出棧的方式存儲操作數的

動態鏈接:每個棧幀都包含一個指向運行時常量池中該棧幀所屬方法的引用,持有這個引用是為了支援方法調用過程中的動態連接(Dynamic Linking)。

方法返回地址:當一個方法開始執行後,只有兩種方式可以退出,一種是遇到方法返回的位元組碼指令;一種是遇見異常,並且這個異常沒有在方法體內得到處理。

5.3.4 The pc Register(程式計數器)

我們都知道一個JVM進程中有多個執行緒在執行,而執行緒中的內容是否能夠擁有執行權,是根據CPU調度來的。

假如執行緒A正在執行到某個地方,突然失去了CPU的執行權,切換到執行緒B了,然後當執行緒A再獲得CPU執行權的時候,怎麼能繼續執行呢?這就是需要在執行緒中維護一個變數,記錄執行緒執行到的位置。

程式計數器佔用的記憶體空間很小,由於Java虛擬機的多執行緒是通過執行緒輪流切換,並分配處理器執行時間的方式來實現的,在任意時刻,一個處理器只會執行一條執行緒中的指令。因此,為了執行緒切換後能夠恢復到正確的執行位置,每條執行緒需要有一個獨立的程式計數器(執行緒私有)。

如果執行緒正在執行Java方法,則計數器記錄的是正在執行的虛擬機位元組碼指令的地址;

如果正在執行的是Native方法,則這個計數器為空。

The Java Virtual Machine can support many threads of execution at once (JLS §17). Each Java Virtual Machine thread has its own pc (program counter) register. At any point, each Java Virtual Machine thread is executing the code of a single method, namely the current method (§2.6) for that thread. If that method is not native, the pc register contains the address of the Java Virtual Machine instruction currently being executed. If the method currently being executed by the thread is native, the value of the Java Virtual Machine's pc register is undefined. The Java Virtual Machine's pc register is wide enough to hold a returnAddress or a native pointer on the specific platform.

5.3.5 Native Method Stacks(本地方法棧)

如果當前執行緒執行的方法是Native類型的,這些方法就會在本地方法棧中執行。

5.4 結合位元組碼指令理解虛擬機棧

```  1 java   2 class Person{   3     private String name="Jack";   4     private int age;   5   private final double salary=100;   6     private static String address;   7     private final static String hobby="Programming";   8   private Object obj=new Object();   9     public void say(){  10         System.out.println("person say...");  11     }  12     public static int calc(int op1,int op2){  13         op1=3;  14         int result=op1+op2;  15         Object o=obj;  16         return result;  17     }  18   public static void main(String[] args){  19     System.out.println(calc(1,2));  20   }  21 }  ```

此時你需要一個能夠看懂反編譯指令的寶典

比如我給大家準備了一個

```   1 java   2 Compiled from "Person.java"   3 class Person {   4   Person();   5     Code:   6        0: aload_0   7        1: invokespecial #1   // Method java/lang/Object."<init>":()V   8        4: aload_0   9        5: ldc           #2   // String Jack  10        7: putfield      #3   // Field name:Ljava/lang/String;  11       10: aload_0  12       11: ldc2_w        #4   // double 100.0d  13       14: putfield      #6   // Field salary:D  14       17: aload_0  15       18: new           #7   // class java/lang/Object  16       21: dup  17       22: invokespecial #1   // Method java/lang/Object."<init>":()V  18       25: putfield      #8   // Field obj:Ljava/lang/Object;  19       28: return  ```  ```  1 public void say();  2     Code:  3        0: aload_0  4        1: getfield      #8   // Field obj:Ljava/lang/Object;  5        4: astore_1  6        5: getstatic     #9   // Field java/lang/System.out:Ljava/io/PrintStream;  7        8: ldc           #10  // String person say...  8       10: invokevirtual #11  // Method java/io/PrintStream.println:(Ljava/lang/String;)V  9       13: return  ```  ```   1 public static int calc(int, int);   2     Code:   3        0: iconst_3     //將int類型常量3壓入操作數棧   4        1: istore_0     //將int類型值存入局部變數0   5        2: iload_0      //從局部變數0中裝載int類型值   6        3: iload_1      //從局部變數1中裝載int類型值   7        4: iadd         //執行int類型的加法   8        5: istore_2     //將int類型值存入局部變數2   9        6: iload_2      //從局部變數2中裝載int類型值  10        7: ireturn      //從方法中返回int類型的數據  ```  ```  1   public static void main(java.lang.String[]);  2     Code:  3        0: getstatic     #9  // Field java/lang/System.out:Ljava/io/PrintStream;  4        3: iconst_1  5        4: iconst_2  6        5: invokestatic  #12 // Method calc:(II)I  7        8: invokevirtual #13 // Method java/io/PrintStream.println:(I)V  8       11: return  9 }  ```

5.5 結合類載入機制理解運行時數據區

5.5.1 裝載

通過一個類的全限定名獲取定義此類的二進位位元組流

將這個位元組流所代表的靜態存儲結構轉化為方法區的運行時數據結構

在Java堆中生成一個代表這個類的java.lang.Class對象,作為對方法區中這些數據的訪問入口

值得探討的兩個方向:(1)類的裝載方式有哪些?(2)類裝載到底做了什麼?

類的裝載方式有哪些?

(1)本地系統載入

(2)網路下載.class文件

(3)從zip,jar等歸檔文件中載入.class文件

(4)從資料庫中提取.class文件

(5)由java源文件動態編譯成.class文件

(6)Class.forName()載入

(7)ClassLoader.loadClass()載入

類裝載到底做了什麼?

(1)通過一個類的全限定名獲取定義此類的二進位位元組流

這個階段是可控性比較強的階段,既可以用系統提供的類載入器進行載入,又可以自定義類載入器進行載入。

(2)將這個位元組流所代表的靜態存儲結構轉化為方法區的運行時數據結構

方法區用於存儲已被虛擬機載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼等數據。

說明:類資訊

類的版本、欄位、方法、構造方法、介面定義等

(3)類載入的最終產品是位於堆區中的Class對象。

Class對象封裝了類在方法區內的數據結構,並且向Java程式設計師提供了訪問方法區內的數據結構的介面。

在Java堆中生成一個代表這個類的java.lang.Class對象,作為對方法區中這些數據的訪問入口

Java對象實例以及數組都在堆上分配Class類

```  public final class Class<T> implements java.io.Serializable,                                 GenericDeclaration,                                 Type,                                 AnnotatedElement {  ```

5.5.2.1 驗證

保證被載入類的正確性

文件格式驗證

驗證位元組流是否符合Class文件格式規範,比如是否以0xCAFEBABE開頭,主次版本號是否在當前虛擬機的處理範圍之內,常量池中的常量是否有不被支援的類型。

元數據驗證

對位元組碼描述的資訊進行語義分析,保證其符合Java語言規範的要求。

位元組碼驗證

通過數據流和控制流分析,確定程式語義是合法的、符合邏輯的。

符號引用驗證

確保解析動作能正確執行。

小結:驗證階段很重要,但不是必須的。若所引用的類經過反覆驗證沒問題,可以使用-Xverifynone參數關閉大部分類驗證措施,從而縮短虛擬機類載入的時間。

5.5.2.2 準備

為類的靜態變數分配記憶體,並將其初始化為默認值

在方法區中,為類變數分配內容並設置初始值

(1)記憶體分配僅僅是類變數,也就是static類型的變數。不包含實例變數,實例變數會在對象實例化時隨對象分配在堆中。

(2)這裡的默認值是根據類型賦值,不是在程式碼中顯示賦予的值。

5.5.2.3 解析

把類中的符號引用轉換為直接引用

Run-Time Constant Pool

Class文件中除了有類的版本、欄位、方法、介面等描述 資訊外,還有一項資訊就是常量池,用於存放編譯時期生

成的各種字面量和符號引用,這部分內容將在類載入後進 入方法區的運行時常量池中存放。

解析階段是虛擬機將常量池內的符號引用替換為直接引用的過程。

解析動作主要針對類或介面、欄位、類方法、介面方法、方法類型、方法句柄和調用限定符7類符號引用進行。

符號引用就是一組符號來描述目標,可以是任何字面量。

直接引用就是直接指向目標的指針、相對偏移量或一個間接定位到目標的句柄。

5.5.3 初始化

執行類構造器<clinit>,為類的靜態變數賦予正確的初始值,有兩種方式

(1)直接給類變數指定初始值

(2)通過靜態程式碼塊為類變數指定初始值

類的初始化步驟

(1)如果這個類還沒有被載入和鏈接,那先進行載入和鏈接

(2)假如這個類存在直接父類,並且這個類還沒有被初始化(在一個類載入器中,類只能初始化一次),那就初始化直接的父類(不適用於介面)

(3)假如類中存在初始化語句(如static變數和static塊),那就依次執行這些初始化語句。

類什麼時候才會被初始化?

(1)創建類的實例

(2)訪問某個類或介面的靜態變數,或者對該靜態變數進行賦值

(3)調用類的靜態方法

(4)反射[Class.forName("com.XXX")]

(5)初始化一個類的子類(因為會先初始化父類)

(6)JVM啟動時表明的啟動類