註:本文主要記錄自《深入分析java web技術內幕》"第四章 javac編譯原理" 1、javac作用 將*.java源代碼文件轉化為*.class文件 2、編譯流程 流程: 詞法分析器:將源碼轉換為Token流 將源代碼劃分成一個個Token(Token包含的元素類型看3.2) 語法分析器:將T
註:本文主要記錄自《深入分析java web技術內幕》"第四章 javac編譯原理"
1、javac作用
- 將*.java源代碼文件轉化為*.class文件
2、編譯流程
流程:
- 詞法分析器:將源碼轉換為Token流
- 將源代碼劃分成一個個Token(Token包含的元素類型看3.2)
- 語法分析器:將Token流轉化為語法樹
- 將上述的一個個Token組成一句句話(或者說成一句句代碼塊),檢查這一句句話是不是符合Java語言規範
- 語義分析器:將語法樹轉化為註解語法樹
- 將複雜的語法轉化成簡單的語法(eg.註解、foreach轉化為for迴圈)並做一些檢查,添加一些代碼
- 代碼生成器:將註解語法樹轉化為位元組碼
3、詞法分析
3.1、作用
- 將源碼轉換為Token流。
3.2、流程
一個位元組一個位元組的讀取源代碼,形成規範化的Token流。規範化的Token包含:
- java關鍵詞:package、import、public、class、int等
- 自定義單詞:包名、類名、變數名、方法名
- 符號:=、;、+、-、*、/、%、{、}等
3.3、示例
代碼:
1 package compile; 2 3 /** 4 * 詞法 5 */ 6 public class Cifa { 7 int a; 8 int c = a + 1; 9 }View Code
以上代碼轉化為的Token流:
說明:完成以上示例的是JavacParser的parseCompilationUnit()方法,源代碼見文章開頭的書籍。
註意:上邊的token流符合java語言規範。
3.4、疑問
- 怎樣判斷package是java關鍵詞還是自定義變數?
- JavacParser會根據java語言規範來控制什麼順序、什麼地方出現什麼Token(這個查看parseCompilationUnit()源碼就知道了),所以package在文件的最開頭出現,我們會知道是一個Token.PACKAGE類型,而非自定義的Token.IDENTIFIER類型。
- 一條實踐:在編寫程式的時候,不要用java關鍵詞來定義變數名、類名、包名、方法名,而是採取一定有意義的單詞來定義,當然,你再eclipse中編寫代碼的時候,如果使用了java關鍵詞來定義變數,eclipse會提醒你這是一個錯誤的定義。
- 怎樣確定package是一個Token,而packa不是?
- 我的理解是,主要看空格和符號(符號見3.2),對於package是一個單詞,中間沒有空格也沒有符號,所以是一個Token
- 一條實踐:在編寫代碼時,例如:int a = b + c;//a與=中間有一個空格、=與b之間有一個空格、b與+之間有一個空格、+與c之間有一個空格,當然,這裡沒有空格也行,因為每一個變數之間正好都是由符號來隔開的,但是之前看了一個視頻說,如果上邊這句話沒有這些空格的話,可能編譯不通過,所以我們最好還是加上空格,當然加上空格後顯得整個代碼也清晰。
4、語法分析
4.1、作用
- 將進行詞法分析後形成的Token流中的一個個Token組成一句句話,檢查這一句句話是不是符合Java語言規範。
4.2、語法分析三部分:
- package
- import
- 類(包含class、interface、enum),一下提到的類泛指這三類,並不單單是指class
4.3、示例
代碼:
1 package compile; 2 3 /** 4 * 語法 5 */ 6 public class Yufa { 7 int a; 8 private int c = a + 1; 9 10 //getter 11 public int getC() { 12 return c; 13 } 14 //setter 15 public void setC(int c) { 16 this.c = c; 17 } 18 }View Code
最終語法樹:
說明:
- 每一個包package下的所有類都會放在一個JCCompilationUnit節點下,在該節點下包含:package語法樹(作為pid)、各個類的語法樹
- 每一個從JCClassDecl發出的分支都是一個完整的代碼塊,上述是四個分支,對應我們代碼中的兩行屬性操作語句和兩個方法塊代碼塊,這樣其實就完成了語法分析器的作用:將一個個Token單片語成了一句句話(或者說成一句句代碼塊)
- 在上述的語法樹部分,對於屬性操作部分是完整的,但是對於兩個方法塊,省略了一些語法節點,例如:方法修飾符public、方法返回類型、方法參數。
疑問:
import節點的語法樹與package的相似,但是import語法樹放在了哪一個地方?
5、語義分析
5.1、作用
- 將語法樹轉化為註解語法樹
5.2、步驟
- 添加預設的無參構造器(在沒有指定任何有參構造器的情況下)
- 處理註解
- 標註:檢查語義合法性、進行邏輯判斷
- 檢查語法樹中的變數類型是否匹配(eg.String s = 1 + 2;//這樣"="兩端的類型就不匹配)
- 檢查變數、方法或者類的訪問是否合法(eg.一個類無法訪問另一個類的private方法)
- 變數在使用前是否已經聲明、是否初始化
- 常量摺疊(eg.代碼中:String s = "hello" + "world",語義分析後String s = "helloworld")
- 推導泛型方法的參數類型
- 數據流分析
- 變數的確定性賦值(eg.有返回值的方法必須確定有返回值)
- final變數只能賦一次值,在編譯的時候再賦值的話會報錯
- 所有的檢查型異常是否拋出或捕獲
- 所有的語句都要被執行到(return後邊的語句就不會被執行到,除了finally塊兒)
- 進一步語義分析
- 去掉永假代碼(eg.if(false))
- 變數自動轉換(eg.int和Integer)
- 去掉語法糖(eg.foreach轉化為for迴圈,assert轉化為if,內部類解析成一個與外部類相關聯的外部類)
- 最後,將經過上述處理的語法樹轉化為最後的註解語法樹
6、生成位元組碼
6.1、作用
- 將註解語法樹轉化成位元組碼,並將位元組碼寫入*.class文件。
6.2、步驟
- 將java的代碼塊轉化為符合JVM語法的命令形式,這就是位元組碼
- 按照JVM的文件組織格式將位元組碼輸出到*.class文件中
具體的源代碼與步驟查看com.sun.tools.javac.jvm.Gen類與《分散式Java應用:基礎與實踐》P42
6.3、class文件包含的內容
在生成的*.class文件中不只包含位元組碼信息,具體包含:
- 結構信息
- class文件格式版本號
- 各部分的數量與大小
- 元數據
- 類、父類、實現介面的聲明信息
- 屬性聲明信息
- 方法聲明信息
- 常量池
- 方法信息
- 位元組碼
- 異常處理器表
- 局部變數區的大小
- 操作數棧的大小
- 操作數棧的類型記錄
- 調試用符號信息
這裡提到的局部變數區和操作數棧組成了了方法棧,可以參看第一章 JVM記憶體結構
總結:
對於編譯這一塊兒,我們在實際操作中不會直接去操作這些代碼,不像類載入器機制,我們可能需要自己編寫類載入工具,也不像Java記憶體管理那樣,我們會直接在伺服器配置堆棧方法區空間、配置GC收集器等,但是瞭解javac編譯,對於我們瞭解以後的類文件結構、類載入機制有一定的幫助,也有利於我們掌握整個Java代碼的執行流程,對於我們瞭解編譯期間編譯器做的一些檢查工作也有很大幫助,瞭解這些檢查工作有利於我們在寫代碼的時候更加小心,例如,檢查型異常都需要捕獲或拋出,每一條語句都要被執行到(即可達)等。雖然,這些工作eclipse會在我們寫代碼的時候為我們自動去檢查,包括檢查語句是否可達,但是瞭解這些還是有好處的。