Java的編譯原理


概述

java語言的"編譯期"分為前端編譯和后端編譯兩個階段。前端編譯是指把*.java文件轉變成*.class文件的過程; 后端編譯(JIT, Just In Time Compiler)是指把字節碼轉變成機器碼的過程。

在編譯原理中, 將源代碼編譯成機器碼, 主要經過下面幾個步驟:

Java中的前端編譯

java的前端編譯(即javac編譯)可分為解析與填充符號表、插入式注解處理器的注解處理、分析與字節碼生成等三個過程。

解析與填充符號表

解析步驟包括詞法分析和語法分析兩個階段。

詞法分析是將源代碼的字符流轉變為標記(Token)集合, 單個字符是程序編寫過程的最小單位, 而標記則是編譯過程的最小單位, 關鍵字、變量名、字面量、運算符都可以成為標記。

語法分析是根據Token序列構造抽象語法樹的過程, 抽象語法樹(AST)是一種用來描述程序代碼語法結構的樹形表示方式, 語法樹的每一個節點都代表着程序代碼中的一個語法結構, 如包、類型、修飾符、運算符、接口、返回值都可以是一個語法結構。 

符號表是由一組符號地址和符號信息構成的表格。在語法分析中, 符號表所登記的內容將用於語義檢查和產生中間代碼。在目標代碼生成階段, 符號表是當對符號名進行地址分配時的依據。

插入式注解處理器

插入式注解處理器可以看做是一組編譯器的插件, 在這些插件里面, 可以讀取、修改、添加抽象語法樹中的任意元素。如果這些插件在處理注解期間對語法數進行了修改, 編譯器將回到解析與填充符號表的過程重新處理, 直到所有插入式注解處理器都沒有再對語法數進行修改為止, 每一次循環稱為一個Round。

語義分析與字節碼生成

語法分析后, 編譯器獲得了程序代碼的抽象語法樹表示, 語法數能表示一個結構正確的源程序的抽象, 但無法保證源程序是符合邏輯的。而語義分析的主要任務是對結構正確的源程序進行上下文有關性質的審查。

Javac的編譯過程中, 語義分析過程分為標注檢查、數據及控制流分析兩個步驟。

標注檢查的內容包括諸如變量使用前是否已被聲明、變量與賦值之間的數據類型是否能夠匹配等。另外在標注檢查步驟中, 還有一個重要的動作稱為常量折疊

數據及控制流分析是對程序上下文邏輯更進一步的驗證, 他可以檢查出諸如程序局部變量在使用前是否有賦值、方法的每條路徑是否都有返回值、是否所有的受查異常都被正確處理等問題。

Java中常用的語法糖有泛型、變長參數、自動裝箱/拆箱、遍歷循環、條件編譯等等。虛擬機運行時並不支持這些語法, 它們在編譯階段還原回簡單的基礎語法結構, 這個過程稱為解語法糖

字節碼生成是Javac編譯過程的最后一個階段, 它將前面各個步驟所生成的信息(語法數、符號表)轉化成字節碼寫到磁盤中, 另外還進行少量的代碼添加(如實例構造器)和轉換工作。

Java中的后端編譯

在部分商用虛擬機中, Java程序最初是通過解釋器進行解釋執行的, 當虛擬機發現某個方法或代碼塊的運行特別頻繁時, 就會把這些代碼認定為"熱點代碼"。為了提高熱點代碼的執行效率, 在運行時, 虛擬機將會把這些代碼編譯成與本地平台相關的機器碼, 並進行各種層析的優化, 完成這個任務的編譯器稱為即時編譯器(JIT編譯器)。

編譯器與解釋器

HotSpot虛擬機中內置了兩個即時編譯器, 分別稱為Client Compiler(C1編譯器)和Server Compiler(C2編譯器)。在HotSpot虛擬機中, 默認采用解釋器與其中一個編譯器直接配合的方式工作, 程序使用哪個編譯器, 取決於虛擬機運行的模式, HotSpot虛擬機會根據自身版本與宿主機器的硬件性能自動選擇運行模式, 這種解釋器與編譯器搭配使用的方式在虛擬機中稱為"混合模式"(Mixed Mode)。在個人機器上, 通過java -version命令可查看自己安裝的JDK中是哪種模式。

在JDK 1.7的Server模式虛擬機中, 默認開啟分層編譯的策略。分層編譯根據編譯器編譯、優化的規模與耗時, 划分出不同的編譯層次:

  • 第0層, 程序解釋執行, 解釋器不開啟性能監控功能, 可觸發第1層編譯。
  • 第1層, 也稱為C1編譯, 將字節碼編譯為本地代碼, 進行簡單可靠的優化, 如有必要將加入性能性能監控的邏輯。
  • 第2層(或2層以上), 也稱為C2編譯, 也是將字節碼編譯為本地代碼, 但是會啟用一些編譯耗時較長的優化, 甚至會根據性能監控信息進行一些不可靠的激進優化。

實施分層編譯后, C1編譯器和C2編譯器將會同時工作, 用C1編譯器獲取更高的編譯速度, 用C2編譯器獲取更好的編譯質量。

編譯對象與觸發條件

在運行過程中會被即時編譯器編譯的"熱點代碼"有如下兩類:

  • 被多次調用的方法。
  • 被多次執行的循環體。

對於第一種情況, 編譯器會以整個方法作為編譯對象, 這種編譯也是虛擬機中標准的JIT編譯方式。而對於第二種, 盡管編譯動作是由循環體所觸發的, 但編譯器依然會以整個方法(而不是單獨的循環體)作為編譯對象, 這種編譯方式因為編譯發生在方法執行過程之中, 因此形象的稱之為棧上替換(即OSR編譯)。

判斷是否需要觸發即時編譯, 需要先識別出熱點代碼, 這個行為稱之為熱點探測。目前主要的熱點探測判定方式有以下兩種:

  • 基於采樣的熱點探測: 虛擬機周期性地檢查各個線程的棧頂, 如發現某個方法經常出現在棧頂, 它就是"熱點方法"。好處是簡單高效, 還可以獲取方法調用關系; 缺點是很難精確的確認一個方法的熱點, 容易受到線程阻塞或別的外界因素干擾。
  • 基於計數器的熱點探測: 虛擬機會為每個方法(甚至是代碼塊)建立計數器, 統計方法的執行次數, 如果執行次數超過一定的閾值就認為是"熱點方法"。

在HotSpot虛擬機中使用的是第二種————基於計數器的熱點探測, 它為每個方法准備了兩類計數器: 方法調用計數器和回邊計數器。在確定虛擬機運行參數的前提下, 這兩個計數器都有一個的確定的閾值, 當計數器超過閾值溢出, 就會觸發JIT編譯。

方法調用計數器用於統計方法被調用的次數; 回邊計數器用於統計一個方法中循環體代碼執行的次數, 在字節碼中遇到控制流向后跳轉的指令稱為"回邊"。關於這兩種計數器, 讀者可參閱<<深入理解Java虛擬機>>, 這里不多做深入分析。

編譯過程

在默認設置下, 無論是方法調用產生的標准JIT編譯請求, 還是OSR編譯請求, 虛擬機在代碼編譯器還未完成之前, 都仍然將按照解釋方式繼續執行, 而編譯動作則在后台的編譯線程中進行。

Java的后端編譯優化技術

公共子表達式消除

如果一個表達式E已經計算過了,並且從先前的計算到現在E中所有變量的值都沒有發生變化,那E的這次出現就成為了公共子表達式。對於這種表達式, 沒必要花時間再對它進行計算, 只需要直接用前面計算過的表達式結果替代E就可以了。

數組邊界檢查消除

顧名思義就是如果編譯器根據數據流分析, 訪問數組的下標沒有越界, 那么就可以消除數組的邊界檢查, 這樣能節省很多的條件判斷操作, 提升程序性能。

方法內聯

內聯函數就是在程序編譯時,編譯器將程序中出現的內聯函數的調用表達式用內聯函數的函數體來直接進行替換。

逃逸分析

逃逸分析的基本行為就是分析對象動態作用域:當一個對象在方法中被定義后,它可能被外部方法所引用,例如作為調用參數傳遞到其他地方中,稱為方法逃逸。甚至還有可能被外部線程訪問到,譬如賦值給類變量或可以在其他線程中訪問的實例變量,稱為線程逃逸。

如果能證明一個對象不會逃逸到方法或線程外,則可能為這個變量進行一些高效的優化, 如棧上替換、同步消除、標量替換。

參考資料

《深入理解Java虛擬機》

深入淺出 JIT 編譯器

什么是即時編譯(JIT)!?OpenJDK HotSpot VM剖析

深入分析Java的編譯原理-HollisChuang's Blog

對象和數組並不是都在堆上分配內存的。-HollisChuang's Blog

作者:張小凡
出處:https://www.cnblogs.com/qingshanli/
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。如果覺得還有幫助的話,可以點一下右下角的【推薦】。


免責聲明!

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



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