Java源碼的前端編譯
歡迎來到我的博客:TWind的博客
我的CSDN::Thanwind-CSDN博客
我的掘金:Thanwinde 的個人主頁
0.前言
當一份Java代碼寫好時,將其進行編譯,運行,并不是簡單把這個Java源碼從頭到尾執行,一般來說會經歷前端編譯和后端編譯兩個階段,前端編譯會把Java源碼進行分析,拆解,填充并進行一些優化來變成字節碼
后端編譯是發生在JVM已經在解釋執行字節碼時的JIT,會進行一些分析來將熱點代碼直接替換成大部分情況下效率更高的本地機器碼,并進行許多優化,從而提高效率
除去前后編譯,還有提前編譯,后面都會寫文章一一介紹
1.前端編譯階段
前端編譯是一個Java源碼的開始,在這里會將其轉化為可以執行(效率不談)的字節碼
Javac
Javac是收錄于JDK中的Java編譯器。該工具可以將后綴名為.java的源文件編譯為后綴名為.class的可以運行于JVM的字節碼,Javac是由Java寫的,運行javac的實質便是命令行的調用:javac hello.java
Java對于如何把.java文件轉化為.class文件規范得很寬松,可能會導致class在一些JDK上能編譯,一些卻不行的情況
具體來說,Javac的編譯過程可分為四個階段
- 初始化插入式注解處理器
- 解析與填充符號表
- 把源代碼轉化為標記集合來構造抽象語法樹
- 填充符號表
- 注解處理器處理注解
- 語義分析與生成字節碼
- 標注檢查
- 數據流與控制流分析
- 解語法糖
- 字節碼生成
下面,讓我們一個個來看:
初始化插入式注解處理器
JDK5之后,Java提供了對注解的支持,原本的方案是把注解作為運行時才發揮作用的,但是到了JDK6,引入了插入式注解處理器:讓注解處理可以發生在編譯階段,一般來說,運行時的注解只能完成一些反射,自動化類的操作,但注解處理時可以動態的向其中加入代碼:譬如lombok之類的動態生成代碼,使注解能在代碼層面影響代碼
SPI
許多的框架其實都是依靠于這套機制完成的,而這些是基于一種名為 SPI 的技術
和SPI對應的是API,API(Application Programming Interface)
API就是上方:實現方已經實現了接口,要求調用方去實現它
SPI反過來:接口由調用方決定,實現方要根據調用方的接口要求去實現這個接口來提供服務
API最典型的實現就是OAuth2服務:你需要按照OAuth2提供商的要求去實現登錄過程
SPI最典型的也就是JDBC,日志:各種數據庫的JDBC驅動都是按照統一的接口規范來編寫的,完全符合調用者制定的規范,眾多的日志實現類也同理
但這只是SPI的概念,和Java中修改源代碼的需求好像并無交集
具體來說,Java設計了許多接口,來供其他包作者去適配
這其中就包括注解的解釋接口:javax.annotation.processing.AbstractProcessor
,或是javax.annotation.processing.Processor
(插入式注解處理器會放在META-INF/services之中)
在編譯的時候,也就是一開始編譯,Javac會先加載所有實現了這個接口的類,也就是所有插入式注解處理器(包括你自定義的),這實際上破壞了雙親委派模型
因為對于javac(對一般的java程序也一樣)來說,這些插入式注解處理的實現(SPI)都屬于“應用程序級”的,應該由最低級的應用程序類加載器加載
但實際上,他們的接口都位于“高層”,其接口是由啟動類加載器加載的,但是這些高層的接口要去加載自己的實現類,就屬于高級類加載器加載低級類加載器了
這里,java引入了線程上下文類加載器,這個加載器可以跨界進行加載,這樣就能幫助高層的加載器加載底層的類
至于這里為什么強調“線程上下文”,我們可以以tomcat舉例:通常里面有多個線程來處理請求,相互應該隔離:對于java,不同類加載器加載的同一個類,視為不同類,不相同也不兼容,這樣就能隔離開不同線程
也不用擔心每個線程都要去加載一個SPI,只會進行一次類加載,剩下的都會從緩存之中直接加載,盡管是其他線程
總而言之
通過SPI,我們就能解決Javac在準備編譯時去加載插入式注解處理器的問題了
解析與填充符號表
詞法語法分析
這里會把代碼中的字符流轉化為標記(Token),Token是編譯時的最小元素,比如int a = b + 2,會拆分成6個Token:int ,a,=,b,+,2
接著會把這些Token按照順序構造成抽象語法樹(AST),其代表了一個程序的語法結構,你可以理解為是一個能保存一個程序的所有信息的數據結構
之后,Javac就不會再操作字符流了,一切都會圍繞著語法樹來進行
填充符號表
符號表類似于一個哈希表,用來標識每一個符號的地址和其信息,用人話來說的話,就是會標記一些Token,記錄下這些Token的名字,類型,作用域等等,譬如方法名,變量名這類的,會用來進行各種檢測,優化:比如語法檢查,分配地址子類的,符號表就像是電話簿一樣的角色
注解處理器處理注解
這里相對的簡單:會調用每一個注解處理器進行處理,如果其對語法樹進行了更改:就會回退到解析與填充符號表 重新處理,因為可能改變了語法以及符號表,這個操作稱之為輪(Round),可以抽象成下面這個圖
當所有注解處理器都處理完成后,便會進入下一個階段:
語義分析與生成字節碼
這里已經具有了一個完整的語法樹和符號表,最后一步就是把這些轉化成字節碼了
標注檢查
這里會進行類型賦值是否互相兼容,變量使用前是否聲明,還會進行常量折疊,這是前端編譯中為數不多的優化:int a = 1 + 2,這里會把1 + 2 在語法樹上直接變成3
數據及控制流分析
這里會對各種邏輯進行進一步的驗證,比如,對于局部變量用之前有無賦值,是否有返回值,異常是否會處理這種更復雜的控制
值得注意的是,這里的分析是和類加載期間的分析(運行時)基本相同,但存在一些東西只能在編譯期或者運行時檢測
就比如說局部變量的final,JVM對于字節碼的要求是越短越好,對于局部變量的final關鍵字會直接被消除,那如何保證其值不會變?那就輪到編譯器來判斷了
為什么不會去掉成員變量的final?因為JVM 運行時有可能用到(比如常量折疊、只讀約束等)
解語法糖
語法糖是一些用來幫助程序員進行編程的特殊語法,或許其不嚴謹或不規范但是其能大幅度提升程序員的幸福度
包括泛型(是的,Java匪夷所思沒有真正的實現泛型,詳見類型擦除),自動拆裝箱,自動變長參數等,這些語法會在這個階段被替換成最基礎的語法
字節碼生成
這里是最后一個階段:把語法樹,符號表轉化成字節碼
字節碼的格式極其嚴格,這里會把其嚴格的轉化成字節碼,并向其中添加一些其他的代碼,比如類構造器,實例構造器
變量初始化,調用父類構造等等
并還會添加一些優化,比如把字符串的+替換成StringBuffer的append之類的
到此,編譯結束
現在已經生成了一份詳盡且嚴謹的字節碼,接下來一步就是開始解釋執行,并開始最大的舞臺:后端編譯及優化