1. 程式人生 > >Java程式碼是怎麼執行的

Java程式碼是怎麼執行的

Java程式碼有很多執行方式。

  1. 在開發工具中執行
  2. 雙擊jar檔案執行
  3. 在命令列中執行
  4. 在網頁中執行

當然,上述執行方式都離不開JRE, 也就是Java執行時環境。

JRE僅包含Java程式的必須元件,包括Java虛擬機器以及Java核心類庫等。

而我們Java程式設計師經常接觸到的JDK(Java開發工具包)同樣包含了JRE, 並且還附帶了一系列開發、診斷工具。

然而,執行C++程式則無需額外的執行時環境,C++編譯器往往把C++程式碼編譯成CPU能夠理解的機器碼。

那麼,既然C++的執行方式如此成熟,我們為什麼要在JVM裡執行Java程式碼呢?

為什麼Java要在虛擬機器裡執行?

Java作為一門高階程式語言,它的語法複雜,抽象程度也很高。因此在硬體上執行Java程式碼並不現實,所以執行Java程式之前,我們需要對其進行一番轉換。

當前進行轉換的主要思路是:設計一個面向Java語言特性的虛擬機器,並通過編譯器將Java程式轉換層該虛擬機器所能識別的指令序列(Java位元組碼)。之所以這麼取名,是因為Java位元組碼指令的操作碼被固定成一個位元組。

Java虛擬機器可以由硬體實現

當然,更多時候還是在各個現有平臺(Windows_x64,Linux_aarch64)上提供軟體實現。這麼做的目的在於,一旦一個程式被編譯成Java位元組碼,那麼它變可以在不同平臺上的虛擬機器實現裡執行。這也就是平時我們所說的Java的跨平臺特性。

虛擬機器的另外一個好處是它帶來了一個託管環境(Managed Runtime)。這個託管環境能夠代替我們處理一些程式碼中冗長而且容易出錯的部分。其中最廣為人知的當屬自動記憶體管理與垃圾回收,這部分內容甚至催生了一波垃圾回收調優的業務。

除此之外,託管環境還提供了諸如陣列越界,動態型別、安全許可權等等的動態監測,使我們免於書寫這些無關業務邏輯的程式碼。

Java虛擬機器具體是怎麼執行Java位元組碼的?

以標準JDK中的HotSpot虛擬機器為例,從虛擬機器和底層硬體兩個角度,剖析該問題。

從虛擬機器的角度來看,執行Java程式碼首先需要將它編譯而成的class檔案載入到Java虛擬機器中。載入後的Java類會被存放於方法區(Method Area)中。

實際執行時,虛擬機器會執行方法區內的程式碼。

如果你熟悉X86的話,你會發現這和段式儲存管理中的程式碼段類似。而且,Java虛擬機器同樣也會在記憶體中劃分出堆和棧來儲存執行時的資料。不同的是,Java虛擬機器會將棧細分為面向Java方法的Java方法棧面向本地方法(用C++寫的native方法)的本地方法棧,以及存放各個執行緒執行位置的PC暫存器

在執行過程中,每當呼叫進入一個Java方法,Java虛擬機器會在當前執行緒的Java方法棧中生成一個棧幀,用以存放區域性變數以及位元組碼的運算元。這個棧幀的大小是提前計算好的,而且Java虛擬機器不要求棧幀在記憶體空間裡連續分佈

當退出當前執行的方法時,不管是正常返回還是異常返回,Java虛擬機器均會彈出當前執行緒的棧幀,並將之捨棄。

在HotSpot裡面,上述翻譯過程有兩種形式

  1. 解釋執行,即逐條將位元組碼翻譯成機器碼並執行。
  2. 即時編譯(Just-In-Time compilation, JIT), 即將一個方法中包含的所有位元組碼編譯成機器碼後再執行。

前者的優勢是無需等待編譯,而後者的優勢在於實際的執行速度更快。

HotSpot預設採用混合模式,綜合瞭解釋執行和即時編譯兩者的優點。

它會首先解釋位元組碼。然後將其中反覆執行的熱點程式碼,以方法為單位即時編譯

Java虛擬機器的執行效率究竟是怎麼樣的?

HotSpot採用了多種技術來提升峰值效能,上文提到的即時編譯技術便是其中最重要的技術之一。

即時編譯建立在程式符合二八定律的假設上。

二八定律:20%的程式碼佔用了程式執行過程中80%的資源。

對於佔據大部分的不常用的程式碼,我們無需耗費時間將其編譯成機器碼,而是採用解釋執行的方式。

另一方面,對於僅佔據小部分的熱點程式碼,我們則可以將其編譯成機器碼,打到理想的執行速度。

理論上講,即時編譯後的Java程式的執行效率,是可以超過C++程式的。這是因為與靜態編譯相比,即時編譯擁有程式的執行時資訊,並且能夠根據這個資訊做出相應的優化。(實際上,編譯時會插入一些有關jvm的程式碼)

舉個例子,我們知道虛方法是用來實現面嚮物件語言多型性的。對於一個虛方法呼叫,儘管它有很多個目標方法,但在實際執行過程中他可能只調用了其中的一個,這個資訊便可以被即時編譯器所利用,來規避虛方法呼叫的開銷從而達到比靜態編譯的C++程式更高的效能。

為了滿足不同使用者場景的需要,HotSpot內建了多個即時編譯器:C1、C2和Graal。 Graal是Java 10正式引入的實驗性即時編譯器。

之所以引入多個即時編譯器,是為了在編譯時間和生成程式碼的執行效率之間做取捨。 C1又叫做Client編譯器,面向的是對啟動效能有要求的客戶端GUI程式,採用的優化手段相對簡單,因此編譯時間較短。C2又叫做Server編譯器,面向的是對峰值效能有要求的服務端程式,採用的優化手段相對複雜,因此編譯時間較長,但同時生成程式碼的執行效率較高。

從Java 7開始,HotSpot預設採用分層編譯的方式:熱點方法首先被C1編譯,而後熱點方法中的熱點會進一步被C2編譯。

為了不干擾應用的正常執行,HotSpot的即時編譯是放在額外的編譯執行緒中進行的。HotSpot會根據CPU的數量設定編譯執行緒的數目,並且按1:2的比例配置給C1及C2編譯器。

在計算資源充足的情況下,位元組碼的解釋執行和即時編譯可同時進行。編譯完成後的機器碼後再下次呼叫時啟用,以替換原本的解釋執行。

我們來完成老師佈置的作業:瞭解Java語言和Java虛擬機器看待boolean型別的方式是否不同。

首先,撰寫程式碼Foo.java

  1. public class Foo {
  2. public static void main(String[] args){
  3. boolean flag = true;
  4. if(flag)
  5. System.out.println(“Hello, Java!!”);
  6. if(flag == true)
  7. System.out.println(“Hello, JVM!!!”);
  8. }
  9. }
  1. javac Foo.java
  2. java Foo

顯然,它的執行結果是:

Hello, Java!!
Hello, JVM!!!

我們使用asmtools.jar對其進行反彙編(此命令JDK7無法執行, 需要升級到JDK8)

java -cp ./asmtools.jar org.openjdk.asmtools.jdis.Main Foo.class > Foo.jasm.1

我們得到它的反彙編程式碼(在Foo.jasm.1 中)

  1. super public class Foo
  2. version 52:0
  3. {
  4. public Method "<init>":"()V"
  5. stack 1 locals 1
  6. {
  7. aload_0;
  8. invokespecial Method java/lang/Object."<init>":"()V";
  9. return;
  10. }
  11. public static Method main:"([Ljava/lang/String;)V"
  12. stack 2 locals 2
  13. {
  14. iconst_1;//看這裡
  15. istore_1;
  16. iload_1;
  17. ifeq L14;
  18. getstatic Field java/lang/System.out:"Ljava/io/PrintStream;";
  19. ldc String "Hello, Java!!";
  20. invokevirtual Method java/io/PrintStream.println:"(Ljava/lang/String;)V";
  21. L14: stack_frame_type append;
  22. locals_map int;
  23. iload_1;
  24. iconst_1;
  25. if_icmpne L27;
  26. getstatic Field java/lang/System.out:"Ljava/io/PrintStream;";
  27. ldc String "Hello, JVM!!!";
  28. invokevirtual Method java/io/PrintStream.println:"(Ljava/lang/String;)V";
  29. L27: stack_frame_type same;
  30. return;
  31. }
  32. } // end Class Foo

在執行指令

awk 'NR==1,/iconst_1/{sub(/iconst_1/, "iconst_2")} 1' Foo.jasm.1 > Foo.jasm

其作用是將Foo.jasm.1檔案中第一個iconst_1 替換為iconst_2, 輸出到檔案Foo.jasm中

  1. super public class Foo
  2. version 52:0
  3. {
  4. public Method "<init>":"()V"
  5. stack 1 locals 1
  6. {
  7. aload_0;
  8. invokespecial Method java/lang/Object."<init>":"()V";
  9. return;
  10. }
  11. public static Method main:"([Ljava/lang/String;)V"
  12. stack 2 locals 2
  13. {
  14. iconst_2; //看這裡
  15. istore_1;
  16. iload_1;
  17. ifeq L14;
  18. getstatic Field java/lang/System.out:"Ljava/io/PrintStream;";
  19. ldc String "Hello, Java!!";
  20. invokevirtual Method java/io/PrintStream.println:"(Ljava/lang/String;)V";
  21. L14: stack_frame_type append;
  22. locals_map int;
  23. iload_1;
  24. iconst_1;
  25. if_icmpne L27;
  26. getstatic Field java/lang/System.out:"Ljava/io/PrintStream;";
  27. ldc String "Hello, JVM!!!";
  28. invokevirtual Method java/io/PrintStream.println:"(Ljava/lang/String;)V";
  29. L27: stack_frame_type same;
  30. return;
  31. }
  32. } // end Class Foo

我們現在將flag的值由1改為了2, 將修改後的程式碼彙編到Foo.class檔案中

java -cp ./asmtools.jar org.openjdk.asmtools.jasm.Main Foo.jasm

再次執行Foo類

  1. java Foo
  2. Hello, Java!!

可見JVM將true視為1, 不等於修改為2的flag,使用if_icmpne指令判斷他們不相等,直接跳到L27執行,所以Hello, JVM!!!不會輸出。而第一次判斷是使用ifeq判斷flag的值是否為0,所以Hello,Java!!會輸出。

此文從極客時間專欄《深入理解Java虛擬機器》搬運而來,撰寫此文的目的:

  1. 對自己的學習總結歸納

  2. 此篇文章對想深入理解Java虛擬機器的人來說是非常不錯的文章,希望大家支援一下鄭老師。