Java的編譯原理

概述

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

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

Java中的前端編譯

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

解析與填充符號表

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

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

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

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

插入式註解處理器

插入式註解處理器能夠看作是一組編譯器的插件, 在這些插件裏面, 能夠讀取、修改、添加抽象語法樹中的任意元素。若是這些插件在處理註解期間對語法數進行了修改, 編譯器將回到解析與填充符號表的過程從新處理, 直到全部插入式註解處理器都沒有再對語法數進行修改成止, 每一次循環稱爲一個Round。優化

語義分析與字節碼生成

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

Javac的編譯過程當中, 語義分析過程分爲標註檢查、數據及控制流分析兩個步驟。.net

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

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

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/ 本文版權歸做者和博客園共有,歡迎轉載,但未經做者贊成必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接,不然保留追究法律責任的權利。若是以爲還有幫助的話,能夠點一下右下角的【推薦】。

相關文章
相關標籤/搜索