-
- 2011年,
JDK7
發布,1.7u4中,開始啟用新的垃圾回收器G1
(但是不是默認)。
- 2011年,
-
- 2017年,發布
JDK9
,G1
成為默認GC
,代替CMS
。(一般公司使用jdk8
的時候,會通過參數,指定GC
為G1
)
- 2017年,發布
-
- 2018年,發布
JDK11
,帶來了革命性ZGC
,性能比較強。
- 2018年,發布
虛擬機介紹
虛擬機,就是虛擬的計算機,可以執行一系列虛擬計算機指令,大體上可以分為系統虛擬機和程序虛擬機。它們運行時,都會受到虛擬機提供的資源的限制。
- 系統虛擬機:仿真模擬系統的,比如
Visual Box
,VMware
。 - 程序虛擬機:為執行單個計算機程序設計的,比如
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
,不會在運行的時候才去計算,這個是因為2
和3
都是常量。
這個現象稱之為編譯期的常量折疊。
但是如果我們把代碼成下面這種情況呢?
int i = 2;
int j = 3;
int k = i + j;
反編譯出來的指令:
const
意思是constant
(常量),store
是storeage
寄存器。
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等,認真寫好每一篇文章,不喜歡標題黨,不喜歡花里胡哨,大多寫系列文章,不能保證我寫的都完全正確,但是我保證所寫的均經過實踐或者查找資料。遺漏或者錯誤之處,還望指正。