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


    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年我寫了什么?

開源編程筆記


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM