JVM筆記 -- JVM的發展以及基於棧的指令集架構

第十六封發表於2021-03-07
    1. 2011年,JDK7釋出,1.7u4中,開始啟用新的垃圾回收器G1(但是不是預設)。
    1. 2017年,釋出JDK9G1成為預設GC,代替CMS。(一般公司使用jdk8的時候,會通過引數,指定GCG1)
    1. 2018年,釋出JDK11,帶來了革命性ZGC,效能比較強。

虛擬機器介紹

虛擬機器,就是虛擬的計算機,可以執行一系列虛擬計算機指令,大體上可以分為系統虛擬機器和程式虛擬機器。它們執行時,都會受到虛擬機器提供的資源的限制。

  • 系統虛擬機器:模擬模擬系統的,比如Visual BoxVMware
  • 程式虛擬機器:為執行單個計算機程式設計的,比如Java虛擬機器。

JAVA虛擬機器

Java虛擬機器是一臺執行位元組碼的虛擬機器計算機,但是位元組碼不一定是由Java語言編譯而成。但是隻要使用這一套虛擬機器規則的語言,就可以享受到跨平臺,垃圾收集以及可靠的即時編譯器。JVM和硬體之間沒有直接的互動。

  • 一次編譯,到處執行。
  • 自動記憶體管理
  • 自動垃圾回收

下面是ava平臺文件中Java概念圖的描述,可以看出javac命令在JDK中,也就是將.java檔案編譯成為.class檔案,這個就是前端編譯器,將原始檔編譯成為位元組碼。這個編譯器不在JRE中,也說明了JRE不包括編譯環境。

JRE和JDK都包括了JVM虛擬機器。JRE是執行時環境,而JDK包含了開發環境。

JDK7 中java家族的結構組成 : https://docs.oracle.com/javase/7/docs/

JDK7 中java家族的結構組成 : https://docs.oracle.com/javase/8/docs/

JVM結構

上面的圖主要包括三部分:類載入器,執行時資料區,執行引擎。

類載入器,主要是將Class檔案(已經經過前端編譯器編譯後的位元組碼檔案),載入到執行時資料區,生成Class物件,這個過程會設計載入,連結,初始化等過程。

執行時區域主要分為:

  • 執行緒私有(每個執行緒有一份):
    • 程式計數器:Program Count Register,執行緒私有,沒有垃圾回收
    • 虛擬機器棧:VM Stack,執行緒私有,沒有垃圾回收
    • 本地方法棧:Native Method Stack,執行緒私有,沒有垃圾回收
  • 執行緒共享:
    • 方法區:Method Area,以HotSpot為例,JDK1.8後元空間取代方法區,有垃圾回收。
    • 堆:Heap,垃圾回收最重要的地方。

執行引擎主要包括直譯器和即時編譯器(熱點程式碼提前編譯好,這是後端編譯器),垃圾回收器。位元組碼檔案不能直接被機器識別,所以需要執行引擎來做轉換。

Java程式碼執行流程

Java程式碼變成位元組碼檔案的過程中,其實包含了詞法分析,語法分析,語法樹,語義分析等一系列操作。

在執行引擎中,有JIT編譯器,也就是第二次編譯的過程會發生在這裡,會將熱點程式碼編譯成為機器指令,是按照方法的維度,快取起來(放在方法區),也稱之為CodeCache

JVM架構模型

Java編譯器主要是基於棧的指令集架構,個人覺得主要原因是可移植性決定的,JVM需要跨平臺。指令集架構主要有兩種:

  • 基於棧的指令集架構:一個方法相當於一個入棧的操作,執行完相當於出棧操作。
  • 基於暫存器的指令集架構

基於棧的指令集架構的特點

主要特點:

  • 設計實現簡單,適用於資源受限的系統,比如機頂盒,小玩具上。
  • 避開暫存器分配難題:使用零地址指令方式分配。
  • 指令流中大部分都是零地址指令,執行過程依賴操作棧,指令集更小(零地址),編譯器容易實現。
  • 不需要硬體支援,可移植性強,容易實現跨平臺。

基於暫存器架構的特點

  • 典型應用是x86的二進位制指令集
  • 依賴於硬體,可移植性差
  • 效能好,執行效率高
  • 更少指令執行一項操作
  • 大部分情況下,暫存器的架構,一,二,三地址指令為主,而基於棧的指令集卻是以零地址指令為主。

說明:什麼叫零地址指令,一地址指令,二地址指令?
零地址指令只有操作碼,沒有運算元。這種指令有兩種情況:一是無需運算元,另一種是運算元為預設的(隱含的),預設為運算元在暫存器中,指令可直接訪問暫存器。

  • 三地址指令:一般地址域中A1、A2分別確定第一、第二運算元地址,A3確定結果地址。下一條指令的地址通常由程式計數器按順序給出。

  • 二地址指令:地址域中A1確定第一運算元地址,A2同時確定第二運算元地址和結果地址。

  • 單地址指令:地址域中A 確定第一運算元地址。固定使用某個暫存器存放第二運算元和操作結果。因而在指令中隱含了它們的地址。

  • 零地址指令:在堆疊型計算機中,運算元一般存放在下推堆疊頂的兩個單元中,結果又放入棧頂,地址均被隱含,因而大多數指令只有操作碼而沒有地址域。

棧資料結構,一般只有入棧和出棧,所以操作的地方只有棧頂元素,所以位置是確定的,不需要地址。

例子
執行2+3的操作,如果是基於棧的計算流程:

iconst_2 // 常量2入棧
istore_1 
iconst_3 // 常量3入棧
istore_2
iload_1
iload_2
iadd  // 常量2,3出棧,執行相加
istore_0  // 結果5入棧

基於暫存器的計算流程:

mov eax,2   //將eax暫存器的值設定為2
add eax,3   // 將eax暫存器的值加3

從上面的例子可以看出來,基於棧的暫存器的指令更小,但是基於暫存器的指令更少。

我們可以通過一個簡單程式看一下:

public class StackStructTest {
    public static void main(String[] args) {
        int i = 2 + 3;
    }
}

編譯後,切換到class目錄下,使用命令反編譯:

java -v StackStructTest.class

看到位元組碼的模組,可以看到前面有iconst_5,其實5就是2+3的結果,也就是編譯期間就會直接把2+3變成5,不會在執行的時候才去計算,這個是因為23都是常量。

這個現象稱之為編譯期的常量摺疊

但是如果我們把程式碼成下面這種情況呢?

        int i = 2;
        int j = 3;
        int k = i + j;

反編譯出來的指令:

const意思是constant(常量),storestoreage暫存器。

 stack=2, locals=4, args_size=1
         0: iconst_2  // 2是個常量
         1: istore_1  // 2載入到1號運算元棧
         2: iconst_3  // 3是一個產量
         3: istore_2  // 3載入到2號運算元棧
         4: iload_1   // 將1號運算元棧取出,載入進來
         5: iload_2   // 將2號運算元棧取出,載入進來
         6: iadd      // 兩者相加
         7: istore_3  // 結果儲存到索引為3號運算元棧中
         8: return

也就是棧架構的JVM,需要 8 條指令才能完成上面的變數相加計算。

棧架構總結

由於跨平臺特性,Java指令基於棧來設計,因為不同的CPU架構不同,優點是跨平臺,指令集小,編譯器容易實現。缺點是效能下降,實現同樣功能需要更多指令。

【作者簡介】
秦懷,公眾號【秦懷雜貨店】作者,技術之路不在一時,山高水長,縱使緩慢,馳而不息。個人寫作方向:Java原始碼解析,JDBC,Mybatis,Spring,redis,分散式,劍指Offer,LeetCode等,認真寫好每一篇文章,不喜歡標題黨,不喜歡花裡胡哨,大多寫系列文章,不能保證我寫的都完全正確,但是我保證所寫的均經過實踐或者查詢資料。遺漏或者錯誤之處,還望指正。

2020年我寫了什麼?

開源程式設計筆記

相關文章