有沒有人和我一樣好奇自己寫的java程式是經過了哪些過程,最後以可視化的介面呈現給我們的呢。如果你現在也不知道,或者不是很清楚,那麼我們可以一起學習一下。
一、副檔名
記得剛開始學習敲代碼的時候,時常有人告訴我們寫完了代碼要Control+S(儲存一下),然後當我們查看所寫的內容所在的檔案的格式時,會發現是.java的副檔名,不同於我們平時見到的.txt或者.exe等,為什麼檔案的副檔名不同於平時接觸到的呢,還有.java檔案編譯後的.class副檔名。
其實:
.java:未編譯的程式語言,提供給程式員查看修改的,簡單說就是一個普通的文本,尾碼名僅僅只是為了提供給javac編譯識別的
.class:編譯後的Java檔案,以位元組碼的形式儲存,以供JVM(java虛擬機器)讀取調用執行程式,可以說.class才是jdk認識的語言。
二、編譯過程
程式源碼並不能被電腦直接識別,需要通過編譯轉化為目標代碼,即.class的二進位檔案,如圖1-1.
javac 編譯
在 Java 中提到“編譯”,自然很容易想到 javac 編譯器將*.java檔案編譯成為*.class檔案的過程,這裡的 javac 編譯器稱為前端編譯器,其他的前端編譯器還有諸如 Eclipse JDT 中的增量式編譯器 ECJ 等。相對應的還有後端編譯器,它在程式運行期間將位元組碼轉變成機器碼(現在的 Java 程式在運行時基本都是解釋執行加編譯執行),如 HotSpot 虛擬機器內建的 JIT(Just In Time Compiler)編譯器(分 Client 端和 Server 端)。另外,有時候還有可能會碰到靜態提前編譯器(AOT,Ahead Of Time Compiler)直接把*.java檔案編譯成本地機器代碼,如 GCJ、Excelsior JET 等,這類編譯器我們應該比較少遇到。
下面簡要說下 javac 編譯(前端編譯)的過程。 詞法、文法分析
詞法分析是將原始碼的字元流轉變為標記(Token)集合。單個字元是程式編寫過程中的的最小元素,而標記則是編譯過程的最小元素,關鍵字、變數名、字面量、運算子等都可以成為標記,比如整型標誌 int 由三個字元構成,但是它只是一個標記,不可拆分。
文法分析是根據Token序列來構造抽象文法樹的過程。抽象文法樹是一種用來描述程式碼文法結構的樹形表示方式,文法樹的每一個節點都代表著程式碼中的一個文法結構,如 bao、類型、修飾符、運算子等。經過這個步驟後,編譯器就基本不會再對源碼檔案進行操作了,後續的操作都建立在抽象文法樹之上。 填充符號表
完成了文法分析和詞法分析之後,下一步就是填充符號表的過程。符號表是由一組符號地址和符號資訊構成的表格。符號表中所登記的資訊在編譯的不同階段都要用到,在語義分析(後面的步驟)中,符號表所登記的內容將用於語義檢查和產生中間代碼,在目標代碼產生階段,黨對符號名進行地址分配時,符號表是地址分配的依據。 語義分析
文法樹能表示一個結構正確的來源程式的抽象,但無法保證來源程式是符合邏輯的。而語義分析的主要任務是讀結構上正確的來源程式進行上下文有關性質的審查。語義分析過程分為標註檢查和資料及控制流程分析兩個步驟: 標註檢查步驟檢查的內容包括諸如變數使用前是否已被聲明、變數和賦值之間的資料類型是否匹配等。 資料及控制流程分析是對程式上下文邏輯更進一步的驗證,它可以檢查出諸如程式局部變數在使用前是否有賦值、方法的每條路徑是否都有傳回值、是否所有的受查異常都被正確處理了等問題。 位元組碼產生
位元組碼產生是 javac 編譯過程的最後一個階段。位元組碼產生階段不僅僅是把前面各個步驟所產生的資訊轉化成位元組碼寫到磁碟中,編譯器還進行了少量的代碼添加和轉換工作。 執行個體構造器()方法和類構造器()方法就是在這個階段添加到文法樹之中的(這裡的執行個體構造器並不是指預設的建構函式,而是指我們自己重載的建構函式,如果使用者代碼中沒有提供任何建構函式,那編譯器會自動添加一個沒有參數、存取權限與當前類一致的預設建構函式,這個工作在填充符號表階段就已經完成了)。 JIT 編譯
Java 程式最初是僅僅通過解譯器解釋執行的,即對位元組碼逐條解釋執行,這種方式的執行速度相對會比較慢,尤其當某個方法或代碼塊啟動並執行特別頻繁時,這種方式的執行效率就顯得很低。於是後來在虛擬機器中引入了 JIT 編譯器(即時編譯器),當虛擬機器發現某個方法或代碼塊運行特別頻繁時,就會把這些代碼認定為“Hot Spot Code”(熱點代碼),為了提高熱點代碼的執行效率,在運行時,虛擬機器將會把這些代碼編譯成與本地平台相關的機器碼,並進行各層次的最佳化,完成這項任務的正是 JIT 編譯器。
現在主流的商用虛擬機器(如Sun HotSpot、IBM J9)中幾乎都同時包含解譯器和編譯器(三大商用虛擬機器之一的 JRockit 是個例外,它內部沒有解譯器,因此會有啟動相應時間長之類的缺點,但它主要是面向服務端的應用,這類應用一般不會重點關注啟動時間)。二者各有優勢:當程式需要迅速啟動和執行時,解譯器可以首先發揮作用,省去編譯的時間,立即執行;當程式運行後,隨著時間的推移,編譯器逐漸會返回作用,把越來越多的代碼編譯成本地代碼後,可以擷取更高的執行效率。解釋執行可以節約記憶體,而編譯執行可以提升效率。
HotSpot 虛擬機器中內建了兩個JIT編譯器:Client Complier 和 Server Complier,分別用在用戶端和服務端,目前主流的 HotSpot 虛擬機器中預設是採用解譯器與其中一個編譯器直接配合的方式工作。
運行過程中會被即時編譯器編譯的“熱點代碼”有兩類: 被多次調用的方法。 被多次調用的迴圈體。
兩種情況,編譯器都是以整個方法作為編譯對象,這種編譯也是虛擬機器中標準的編譯方式。要知道一段代碼或方法是不是熱點代碼,是不是需要觸發即時編譯,需要進行 Hot Spot Detection(熱點探測)。目前主要的熱點 判定方式有以下兩種: 基於採樣的熱點探測:採用這種方法的虛擬機器會周期性地檢查各個線程的棧頂,如果發現某些方法經常出現在棧頂,那這段方法代碼就是“熱點代碼”。這種探測方法的好處是實現簡單高效,還可以很容易地擷取方法調用關係,缺點是很難精確地確認一個方法的熱度,容易因為受到線程阻塞或別的外界因素的影響而擾亂熱點探測。 基於計數器的熱點探測:採用這種方法的虛擬機器會為每個方法,甚至是代碼塊建立計數器,統計方法的執行次數,如果執行次數超過一定的閥值,就認為它是“熱點方法”。這種統計方法實現複雜一些,需要為每個方法建立並維護計數器,而且不能直接擷取到方法的調用關係,但是它的統計結果相對更加精確嚴謹。
在 HotSpot 虛擬機器中使用的是第二種——基於計數器的熱點探測方法,因此它為每個方法準備了兩個計數器:方法調用計數器和回邊計數器。
方法調用計數器用來統計方法調用的次數,在預設設定下,方法調用計數器統計的並不是方法被調用的絕對次數,而是一個相對的執行頻率,即一段時間內方法被調用的次數。
回邊計數器用於統計一個方法中迴圈體代碼執行的次數(準確地說,應該是回邊的次數,因為並非所有的迴圈都是回邊),在位元組碼中遇到控制流程向後跳轉的指令就稱為“回邊”。
在確定虛擬機器運行參數的前提下,這兩個計數器都有一個確定的閥值,當計數器的值超過了閥值,就會觸發JIT編譯。觸發了 JIT 編譯後,在預設設定下,執行引擎並不會同步等待編譯請求完成,而是繼續進入解譯器按照解釋方式執行位元組碼,直到提交的請求被編譯器編譯完成為止(編譯工作在後台線程中進行)。當編譯工作完成後,下一次調用該方法或代碼時,就會使用已編譯的版本。
由於方法計數器觸發即時編譯的過程與回邊計數器觸發即時編譯的過程類似,因此這裡僅給出方法調用計數器觸發即時編譯的流程:
javac 位元組碼編譯器與虛擬機器內的 JIT 編譯器的執行過程合起來其實就等同於一個傳統的編譯器所執行的編譯過程
轉載自:http://wiki.jikexueyuan.com/project/java-vm/javac-jit.html