從Java文件到字節碼文件

本文涉及的javac編譯器來自openjdk.

javac的目錄地址爲:解壓目錄/langtools/src/share/classes/com/sun/tools/javac/

javac編譯器將Java編譯成爲一個有效的字節碼文件會經歷4個步驟:

  • 詞法解析:將Java關鍵字排序,使得程序能有序運行。
  • 語法解析:詞法解析後的Token序列整合爲一顆抽象的語法樹。
  • 語義解析:將抽象語法樹擴展地更加完善。
  • 字節碼解析:將字節碼解析成完整的類。

詞法解析


詞法解析是編譯器執行的字節碼編譯的第一步。這個步驟中,將Java源碼中關鍵字和標識符等轉換成符合規範的Token序列。

詞法解析器的接口是com.sun.tools.javac.parser.Lexer ,它直接派生於同包下面的Scanner類,它的主要任務是按照單個字符的方式讀取Java源文件中的關鍵字標識符等,然後將其轉換爲符合Java規範的Token序列。而負責詞法解析工作的是com.sun.tools.javac.parser.JavacParser類,該類的對象實例由ParseFactory負責創建,JavacParser負責詞法解析的具體細節

當我們在命令行敲入javac的時候,Java首先會調用com.sun.tools.javac.main.Main類的compile()方法。compile()方法接着就會調用JavaCompiler類的parseFile()方法,parseFile()的主要功能就是調用自己的parse()方法獲得JavacParser實例對象,然後調用JavacParser類的parseCompilationUnit()進行詞法解析。

這個過程如下圖所示:

Token序列

Token其實就是一個枚舉類型,其內部定了許多符合Java語法規範並與源碼字符集相對應的枚舉常量

所有的枚舉常量都在 com.sun.tools.javac.parser.Token類中。

編譯器在執行詞法解析的過程中,只會對Token進行匹配校驗。

源碼字符集是如何轉換成Token的:

Name對象和Token對象建立的是一種一對一的關係。當詞法解析器中需要將一個源碼字符集合解析成一個Token時,它會通過Names類調用Name類的fromChars()方法獲得一個Name對象,然後使用Keyswords類的key(Name name)方法獲得傳入相對應的Token對象。

詞法解析器如何保存源碼字符集和Token之間的對應關係:

詞法解析器在將源碼轉字符集合轉換爲Token之前,會先將每一個字符集合都轉換成一個對應的Name對象。接着再由com.sun.tools.javac.parser.Keywords類負責實際的Token轉換任務(將Token常量全部轉換爲Name對象),然後轉換好的這些Name對象全部存到Name類的內部類Table中,Keywords類中的數組key用於保存源碼字符集合和Token之間的對應關係。

以上兩個問題略微有些複雜。畫了一個圖來表示一下:

調用nextToken()計算Token的獲取順序

Keywords類的key()方法僅僅是根據Name對象獲得對應的Token,而詞法解析器是通過Scanner類的nextToken()方法保證Token的讀取順序規則。

調用parseCompilationUnit()方法執行詞法解析

詞法解析的核心是校驗Token是否匹配com.sun.tools.javac.parser.JavacParser類在parseCompilationUnit()方法定義的匹配規則。parseCompilationUnit()方法會按照Token的匹配順序依次解析出package、import等關鍵字,當這些Token匹配之後,詞法解析器會開始解析class主題信息,直到詞法解析全部結束,parseCompilationUnit()方法會將Token轉換爲一棵結構化的抽象語法樹。

語法解析


之前提過,語法解析的目的就是將經過詞法解析得到的Token整合爲一棵結構化的抽象語法樹。

詞法解析完成的Token序列依舊還不完善,它們還沒有被整合起來,語法解析的主要任務是把這些零散的Token按照指定的Java語法規範整合起來形成一個有機的整體。

在語法解析階段,語法樹上每個節點都直接或者間接地繼承了JCTree類。

調用qualident()方法解析package語法節點

parseCompilationUnit()這個方法實際上,跨越了詞法解析和語法解析兩個階段。當詞法解析器成功將package關鍵字聲明轉換爲Token並完成詞法解析之後,會調用qualident()方法根據Token.PACKAGE解析爲package語法節點。

語法解析步驟中,com.sun.tools.javac.tree.TreeMaker負責創建JCTree類的所有語法節點對象實例。所以TreeMaker本身就是一個語法解析器,不過具體的細節由parseCompilationUnit()方法來控制。

語法解析器實質上還是使用Token對應的Name對象,來作爲轉換語法節點的素材。所以在解析語法樹之前,首先需要將Token轉換成對應的Name對象。語法解析器就可以根據Name對象解析出一個JCIdent語法節點。

當一個package關鍵字聲明中定義了多級目錄時,qualident()方法就會循環迭代調用語法解析器將package關鍵字聲明解析爲嵌套的JCFieldAccess語法節點。

調用importDeclaration()方法解析import語法樹

當成功解析出package語法節點之後,parseCompilationUnit()這個節點會調用importDeclaration()方法來解析得到import語法樹。

importDeclaration()首先匹配Token.Static來檢測import語句中是否包含了靜態導入。接着importDeclaration()就會調用語法解析器的Ident()方法解析出一個JCIdent語法節點,如果import語句中包含多級目錄的時候,語法解析器就會調用Select()方法解析爲嵌套的JCFieldAccess語法節點。

當語法解析器成功解析出JCIdent和JCFieldAccess節點之後,importDeclaration()方法會調用import()方法,將之前解析的語法節點,整合成爲一棵JCImport節點。

實際開發中,通常會有多個import關鍵字聲明,那麼importDeclaration()方法內部會通過迭代循環方法解析出多個JCImport語法樹,然後將其存儲在一個集合中。

調用classDeclaration()方法解析class語法樹

語法解析的最後一步就是解析class的主體信息,當語法解析器成功將import關鍵字解析聲明爲JCIdent和JCFieldAccess語法節點並整合爲一顆JCImport語法樹之後,parseCompilationUnit()方法內部通過typeDeclaration()方法調用classOrInterfaceOrEnumDeclaration方法將class主體信息解析爲一棵JCClassDecl語法樹。

當以上這個過程結束之後,parseCompilationUnit()方法會調用TopLevel()方法將之前解析好過的package語法節點,import語法節點和class語法樹等內容內容信息全部整合成一棵JCCompilationUnit語法節點樹。

JCCompilationUnit類會成爲整個語法樹的Root,持有整個語法樹的所有節點。這個時候,語法樹的雛形已經建成。

語義解析


經過了以上兩個步驟,解析完成的語法樹依舊不能進入字節碼的編譯,它還不夠完善。語義解析的任務就是將這個這顆不夠完善的語法樹擴充地更加完善。

語義解析步驟中經歷的操作:

  • 爲沒有構造方法的類型添加默認的無參構造函數
  • 檢查任何變量是否在使用之前都被初始化
  • 檢查變量類型和值是不是匹配
  • 將String類型和常量進行合併處理
  • 檢查代碼中的操作是不是都可達
  • 異常檢查
  • 將語法糖的內容正常化

常量摺疊操作

如果一個String類型的數據是由多個常量通過『+』組成的,它其實只會創建一個String對象,編譯器在語義解析的時候,會將多個常量信息合併爲一個對象。

//源代碼中的寫法
String str="Hello"+" "+"World!"
//經過編譯器編譯之後
String str="Hello World!"

生成字節碼


在經歷了一系列的語義解析之後,所解析出來的語法樹就足夠完善了。這個時候編譯器最後的任務就是調用com.sun.tools.javac.jvm.Gen類,將這棵語法樹編譯爲Java字節碼文件。

這個時候,符合Java規範的Java代碼就轉換成符合Java規範的字節碼文件了。

發佈了251 篇原創文章 · 獲贊 274 · 訪問量 73萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章