-
- 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等,認真寫好每一篇文章,不喜歡標題黨,不喜歡花裡胡哨,大多寫系列文章,不能保證我寫的都完全正確,但是我保證所寫的均經過實踐或者查詢資料。遺漏或者錯誤之處,還望指正。