Java 編譯和運行時期, 分別進行的操作


不知大家有沒有思考過,當我們使用IDE寫了一個Demo類,並執行main函數打印 hello world時都經歷了哪些流程么?

 

1. 基本流程如下:

編譯期:檢查是否有語法錯誤,如果沒有就將其翻譯成字節碼文件。即.class文件。
運行期:java虛擬機分配內存,解釋執行字節碼文件。

 

 

  例如下面的的代碼

public class MyApp {
    public static void main(String[] args) {
        System.out.println("hello world");
    }
}

 

假如我們寫了一個MyApp.java,並要打印‘hello world’ 那它需要經過哪些步驟?

 

第一步compile

通過編譯器進行編譯,從Java源碼 ---> Java 字節碼

這個編譯器則是jdk 里的javac 編譯器,我們只需 javac MyApp.java 即可以編譯該源碼,javac 編譯器位於jdk --> bin -->javac

  

 

 

 

第二步load and execute

可以通過jdk 里的java命令運行java字節碼,我們只需 java MyApp.class 即可加載並執行該字節碼,當運行java命令時,JRE將與您指定的類一起加載。然后,執行該類的主要方法。

java命令位於jdk --> bin -->java。如上圖的方框內.

 

===============================================分割線===============================================

2. 編譯期間都做了什么?

編譯期都做了什么?從我們使用者角度看無非就是把源代碼編譯成了可被虛擬機執行的字節碼,但是從平台(編譯器)角度看,它所經歷的流程還不少

畢竟總不能給你什么以.java為后綴的文件都進行編譯吧,需要有各種校驗解析步驟

 

2.1 解析與填充符號表

詞法語法分析

詞法分析是指把源代碼的字符流轉為標記(Token)集合,標記(Token)是編譯階段的最小單元,字符則是編程階段源碼的最小單元。

比如,int i = 0由4個標記構成分別是「int,i,=,0」編譯器只認識這些標記,詞法分析過程就是識別一個個標記的過程。

然而,語法分析則是把生成的標記集合 構成一個語法樹,每個節點代表程序代碼中的語法結構,如包,類型,修飾符,運算符等等。

填充符號表

通過了上面的詞義語義分析之后我們需要把數據存起來,以供后續流程使用,編譯器會以key-value的形式存儲數據,以符號地址為key符號信息為value,具體形式沒做限制可以是樹狀符號表或者有序符號表等。

在語義分析中,根據符號表所登記的內容 語義檢查和產生中間代碼,在目標代碼生成階段,當對符號表進行地址分配時,該符號表是檢查的依據。

注: 說白了就是編譯器做的一些優化操作,比如

 

                      

 

       

 

 

 

 

再比如: 

 

                                         

 

 

 

可以思考下,第一行跟第二行在編譯時期有什么區別?

 

java編譯時會做一些優化操作。第一行,因為是兩個常量做運算,那么他們的結果就是確定的,即num1的值是確定的。所以在編譯時,編譯器就會直接算出num1的值。第二行則不會,java在運行時期才為變量分配內存空間。

 

所以Eclipse編譯得到.class文件,打開class反編譯后可以得到如下代碼:

                                        

 

 

 

2.2 注解處理器

注解與普通的Java代碼一樣,是在運行期間發揮作用的。我們可以把它看做是一組編譯器的插件,在這些插件里面,可以讀取、修改、添加抽象語法樹中的任意元素。

如果這些插件在處理注解期間對語法樹進行了修改,編譯器將回到解析及填充符號表的過程重新處理,直到所有插入式注解處理器都沒有再對語法樹進行修改為止。

換句話說當我們處理注解時如果修改了語法樹的話會重新執行分析以及符號填充過程,把注解也填充進來,直到處理完所有注解。

2.3 語義分析  ( 語法不等於語義, 語法就是寫代碼的規范,而語義就是代碼中的一個個Java關鍵字或者保留字等構成的結點, 最終形成一個語法樹, 可以參考數據結構中的二叉樹等知識)

語法分析以及處理注解之后,編譯器獲得了程序代碼的抽象語法樹,語法樹能表示一個結構正確的源程序的抽象,但無法保證源程序是符合邏輯的

說白了,語法樹上的內容單個來說是合法的, 但是結合到上下文語義則未必是合法的。

比如定義了兩個變量

int a = 1;  //定義int類型就要使用int, 不能使用String來定義, 這才符合Java語義, 語義分析也就是這個意思  <可以參考編譯原理這本書> boolean b = false;
int c = a + b

 

以上 都能構成結構正確的語法樹,但是根據語義分析之后編譯是通不過,Java語言中是不合乎邏輯的。

2.4 解語法糖

Java 中最常用的語法糖主要有泛型、變長參數、條件編譯、自動拆裝箱、內部類等。虛擬機並不支持這些語法,它們在編譯階段就被還原回了簡單的基礎語法結構,這個過程成為解語法糖。

換句話說,不論你是否使用Java的語法糖,最終到jvm哪里的時候都是一樣的,jvm不支持語法糖,所以需要編譯階段解語法糖,語法糖的初衷是用來提升開發效率,而不是代碼性能。

 另附: 

        1、方法重載是在編譯時執行的。因為在編譯的時候,如果調用了一個重載的方法,那么編譯時必須確定他調用的方法是哪個。如:當調用evaluate("hello")時候,我們在編譯時就可以確定他調用的method evaluate

 

        2、方法重寫是在運行時進行的。這個也常被稱為運行時多態的體現。編譯器是沒有辦法知道它調用的到底是那個方法,相反的,只有在jvm執行過程中,才知曉到底是父子類中的哪個方法被調用了。如下:

 

                                 

 

         3、泛型(類型檢測)
              這也正是泛型的好處之一,可以提前暴露問題,而不是等到運行時出現ClassCastException。編譯器會在編譯時對泛型類型進行檢測,並把它重寫成實際的對象類型(非泛型代碼),這樣就可以被JVM執行了。這個過程被稱為"類型擦除"。

 

              類型擦除的關鍵在於從泛型類型中清除類型參數的相關信息,並且再必要的時候添加類型檢查和類型轉換的方法。
              類型擦除可以簡單的理解為將泛型java代碼轉換為普通java代碼,只不過編譯器更直接點,將泛型java代碼直接轉換成普通java字節碼。類型擦除的主要過程如下:
                   1). 將所有的泛型參數用其最左邊界(最頂級的父類型)類型替換。
                   2). 移除所有的類型參數。
 

 

        4. 注解。注解即有可能是運行時也有可能是編譯時
             如java中的@Override注解就是典型的編譯時注解,他會在編譯時會檢查一些簡單的如拼寫的錯誤(與父類方法不相同)等
             同樣的@Test注解是junit框架的注解,他是一個運行時注解,他可以在運行時動態的配置相關信息如timeout等。

 

       5. 異常。異常即有可能是運行時異常,也有可能是編譯時異常
           RuntimeException是一個用於指示編譯器不需要檢查的異常。RuntimeException 是在jvm運行過程中拋出異常的父類。對於運行時異常是不需要再方法中顯示的捕獲或者處理的,如NullPointerException,ArrayIndexOutOfBoundsException
           已檢查的異常是被編譯器在編譯時候已經檢查過的異常,這些異常需要在try/catch塊中處理的異常。

 

      6. AOP. Aspects能夠在編譯時,預編譯時以及運行時使用
        1). 編譯時:當你擁有源碼的時候,AOP編譯器(AspectJ編譯器)能夠編譯源碼並生成編織后的class。這些編織進入的額外功能是在編譯時放進去的。
        2). 預編譯時:織入過程有時候也叫二進制織入,它是用來織入到哪些已經存在的class文件或者jar中的。
        3). 運行時:當被織入的對象已經被加載如jvm中后,可以動態的織入到這些類中一些信息。

 

      7、繼承:繼承是編譯時執行的,它是靜態的。這個過程編譯后就已經確定

 

     8、代理(delegate):也稱動態代理,是在運行時執行

 


        實際上在java中只支持編譯時繼承。Java語言原生是不支持運行時時繼承的。一般情況下所謂編譯時繼承如下:
        如上有兩個類,其中Child為Parent的子類。當我們創建一個Parent實例的時候(無論實際對象為Parent還是Child),編譯器在編譯期間會將其替換成實際類型。所以繼承實際上在編譯時就已經確定了。

 

        而在java中,可以設計通過組合模式來嘗試,模擬下所謂的運行時繼承。

 

                     

 

           在Child類中,其中有一個Parent實例。通過這種方式,我們動態的child類中代理了parent的相關功能。

 

 

2.5 字節碼生成

字節碼生成是Javac編譯過程的最后一個階段,在Javac源碼里面由com.sun.tools.javac. jvm.Gen類來完成。

字節碼生成階段前面各個步驟所生成的信息(語法樹、符號表)轉化成字節碼寫到磁盤中,主要工作就是把語法樹和符號表加工成字節碼文件。

 

因此, 編譯時期進行的操作也可以總結為

 

 ===============================================分割線===============================================

 

3. 運行期間都做了什么?

java的運行期主要是處理編譯器產生的字節碼,包括加載與執行

 

 

 

從jvm加載字節碼文件,到使用到最后的卸載過程,都是屬於運行期的范疇。

1. 加載

       ( 當字節碼還沒被類加載器加載之前它目前還處於虛擬機外部存儲空間里,要想執行它需要通過類加載器來加載到虛擬機的運行時內存空間里。加載器的任務就是把字節碼資源載入到虛擬機運行時環境里

  將類的.class文件中的二進制數據讀到內存中,將其放在運時數據區的方法區內,然后在堆區創建一個java.lang.Class對象,用來封裝類在方法區內的數據結構。

  加載.class文件的方式:

  • 從本地系統上直接加載。
  • 通過網絡下載.class文件。
  • 從zip,jar等歸檔文件中加載.class文件。
  • 將java源文件動態編譯為.class文件

2. 驗證  

  (當類加載器將新加載的字節碼呈現給虛擬機時,首先由驗證器來檢查驗證這些字節碼。驗證程序檢查指令是否無法執行明顯有害的操作。除系統類之外的所有類都需要經過驗證。也可以使用命令-noverify選項來停用驗證。)

  • 類文件的結構檢查 確保類文件總符合ava類文件的固定格式
  • 語義檢查 確保類本身符合java語言的語法規定
  • 字節碼驗證 確保字節碼流可以被java虛擬機安全的執行。(靜態方法,實例對象)
  • 二進制兼容性的驗證 引用類之間協調一致。

3. 准備

  java虛擬機對類的靜態變量分配內存,並設置初始值。如static int此時為0.

4. 初始化

  java虛擬機執行類的初始化語句,為類的靜態變量賦予初始值。

  兩種初始化方法:

  • 在靜態變量的聲明處進行初始化。
  • 在靜態代碼塊中進行初始化。

5. 使用

  程序運行過程。

6. 卸載

  垃圾回收機制相關。對無引用的對象進行回收。

 


免責聲明!

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



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