軟件開發(fā)的環(huán)境需要什么?一個IDE,一個OS,一個硬件設(shè)備,沒錯,這個實質(zhì)是軟件進(jìn)展的三個層集。在很久很久以前(幾十年),軟件就是直接開發(fā)在硬件設(shè)備上的,用紙帶有無孔標(biāo)識二進(jìn)制位,此時的開發(fā)語言是機(jī)器碼,軟件直接對接硬件設(shè)備;后來很不方便,尤其不方便復(fù)用,然后,有了匯編,有了簡單的編譯環(huán)境,然后逐漸發(fā)展成為OS內(nèi)核;時代會進(jìn)步,軟件要處理越來越多復(fù)雜的場景,然后有了高級語言:C等,為了更加高效友好的開發(fā),有了最初期的IDE,2000年左右的程序員,應(yīng)該用記得有一個Turbo C,這個也是我的入門IDE,這個至少要比記事本寫代碼方便了一些。軟件的規(guī)模越來越大,行業(yè)分工越來越細(xì),開發(fā)的效率也越發(fā)的重要,以Android為例,從Eclipse到google官方所推的Android Studio,IDE的功能越來越強(qiáng)大,這個解放了程序員,但也控制了程序員,有誰還會知道,寫在這些IDE中的代碼行,為什么可以執(zhí)行在手機(jī)上?中間有什么過程,是什么模塊在控制這些過程?是完全由IDE實現(xiàn)的嗎?我相信能答出這個問題的人是少數(shù)。古人說過,要知其然,還要知其所以然,只會使用IDE去寫固定的功能模塊,只是一個普通碼農(nóng),要想用好一個工具(包括IDE及語言),還要了解其實質(zhì),要明白,我們寫好的代碼,是怎么運(yùn)行到硬件環(huán)境上的。這個話題很大,包括編譯原理、OS結(jié)構(gòu)、硬件驅(qū)動、各種語言等。大不是不去了解的理由,技術(shù)的提交需要反復(fù)的打磨,好,先來揭個蓋子,看一下C/C++及Java由代碼到機(jī)器碼的過程,有了機(jī)器碼之后,再后面的運(yùn)行細(xì)節(jié),離軟件開發(fā)離得較遠(yuǎn),和開發(fā)優(yōu)質(zhì)軟件平臺(除OS外)關(guān)系也不太大。
由源碼到機(jī)器碼,C/C++與Java的實現(xiàn)并不相同,為什么要放在一起呢,三個原因吧:(一)從語言代來講,C為二代面向過程語言、C++為三代面向?qū)ο笳Z言,Java為參考C++所設(shè)計的三代面向?qū)ο笳Z言,其本身是有傳承的,語言會傳承,編譯運(yùn)行環(huán)境同樣是傳承的,對比著看,可以看出優(yōu)化方向;(二)我目前主要是在做Android開發(fā)的相關(guān)方面,這個是與自身最切身相關(guān)的語言,Android的內(nèi)核是C/C++環(huán)境、應(yīng)用層和framework層是Java環(huán)境,對這兩門語言有迫切的項目需求。(三)個人認(rèn)為,C/C++、Java作為靜態(tài)語言,其應(yīng)用范圍、語言特性,編譯運(yùn)行原理非常有代表性,尤其是Java在做跨平臺,之后JVM上可以運(yùn)行其他的動態(tài)語言,可以說Java的編譯運(yùn)行可以代表一部分希望跨平臺的動態(tài)語言,比如Kotlin。
先來看一下C/C++的源碼到機(jī)器語言過程發(fā)生了什么,分為四個大步驟:預(yù)編譯、編譯、匯編、鏈接,在C/C++中統(tǒng)稱為編譯。
(一)預(yù)編譯所處理的過程包括:
a.展開宏定義#define
b.處理條件預(yù)編譯指令#if等
c.處理#include
d.刪除注釋
e.為Debug及日志填加行號
f.保留#pragma
(二)編譯所處理的過程包括:
a.詞法分析:使用掃描器將源碼分隔為一系列的記號(Token),即源碼中的不可分隔項,較為容易理解,比如下面的:
int index = (2 + 8) * c 會被折分為 :int index = ( 2 + 8 ) * c 10個token,左右半括號各為一個
b.語法分析
將a中分出來的Token,映射為語法樹
c.語義分析
在b中語法樹的基礎(chǔ)上,分析是否有錯誤語義,編譯器所能分析的語義為靜態(tài)語義,包括:聲明是否正確、類型是否匹配、類型的轉(zhuǎn)換是否符合要求
d.經(jīng)過前面三個過程后,將源代碼轉(zhuǎn)換為中間語言,可以理解為將c中通過的語法樹通過一定的規(guī)則拍平,變?yōu)轭惣儆谀繕?biāo)代碼的結(jié)構(gòu),為什么要引入中間語言呢,目標(biāo)語言很多時候是硬件相關(guān)的,而中間語言與硬件無關(guān)。
e.生成目標(biāo)代碼并進(jìn)行相應(yīng)優(yōu)化
目標(biāo)代碼的文件組織格式為.o文件,其內(nèi)部的格式與具體設(shè)備有很大關(guān)系,比如在Android中,對arm7和x86CPU要編譯出不同的so文件,此處同理,不同的硬件環(huán)境生成不同的目標(biāo)代碼。目標(biāo)代碼的優(yōu)化也是針對不同情況有不同處理,在此不再展開,感興趣的同學(xué)可以參考編譯原理相關(guān)書籍。
(三)匯編:匯編的目的是把匯編語言轉(zhuǎn)為機(jī)器語言,基本是一條轉(zhuǎn)一條,沒啥特殊的
(四)鏈接:鏈接是要解決目標(biāo)文件之間的互相依賴關(guān)系,當(dāng)a文件中的aa方法中調(diào)用了b文件的bb方法時,在匯編完成后,a文件的bb方法并沒有準(zhǔn)確的內(nèi)存地址,鏈接后會轉(zhuǎn)換為虛擬地址,虛擬地址可以依據(jù)一定的規(guī)則轉(zhuǎn)換為實際地址,即可以運(yùn)行時找到該方法。鏈接過程包括:地址和空間分配、符號決議和重定位,之后的文檔中可以再總結(jié)下。
Java語言從源代碼到機(jī)器碼的過程要比C/C++復(fù)雜,Java追求的是一次編譯,多次運(yùn)行的跨平臺特性,因而在分層上更為徹底。可以分為編譯期和運(yùn)行期兩個周期,編譯期的輸入為源代碼.java文件、輸出為字節(jié)碼.class文件、運(yùn)行期輸入為字節(jié)碼.class文件,輸出為機(jī)器碼。為何這么做可以跨平臺呢,JVM定義了嚴(yán)格的.class文件的格式,Java文件需要嚴(yán)格按照定義編譯為.class文件,然后可以拿到各個平臺各個版本的虛擬機(jī)上運(yùn)行。如果其他的語言編譯完畢后也遵循.class文件格式,也可以在JVM上和Java一樣運(yùn)行。
先來看Java的編譯期,有以下幾個過程:
(一)生成語法樹及符號表的過程:
a.首先進(jìn)行詞法分析,將源碼的字符流分解為token的集合,token的含義可以參考上面所提到的,即不可折分的個體
b.語法分析,根據(jù)token集合構(gòu)建抽象語法樹
c.生成符號表:符號表是一個類key-value結(jié)構(gòu)的集合,記錄符號的類型、結(jié)構(gòu)、定義,支持增加與刪除
(二)處理Java語言中的注解,修正(一)中生成的語法樹
(三)語義分析,與C/C++的語義分析類似,進(jìn)行一些語義正確性檢測,具體包括:
a.標(biāo)注檢查:類型聲明及賦值是否合適
b.常量折疊:將 1 + 2 直接用3替代
c.上下文的語法檢測,如變量使用前是否賦值等
d.解語法糖,為了提升開發(fā)效率,Java提供了許多的語法糖,比如:泛型、變長參數(shù)、自動裝箱等,在此過程中,會將這些語法糖轉(zhuǎn)為JVM所規(guī)定的格式。具體可以查看Java虛擬機(jī)中相關(guān)語法編譯期的處理
(四)生成字節(jié)碼,生成字節(jié)碼階段主要是兩個事情:
a.按照抽象語法樹及符號表生成相應(yīng)的字節(jié)碼
b.針對Java語言的特點,進(jìn)行些必要的填充,比如:字符串的+轉(zhuǎn)為StringBuilder等
經(jīng)過上述四個過程,Java中的編譯期即進(jìn)行完畢了,可以獲取到.class文件,如果對.class文件格式感興趣,可以看我之前的文章:https://blog.csdn.net/kcstrong/article/details/79460262
繼續(xù)分析下Java的運(yùn)行期,先看下運(yùn)行期有什么特點。首先,運(yùn)行期離Java語言的使用者,離得較遠(yuǎn),運(yùn)行期處理的是字節(jié)碼到機(jī)器碼之間的轉(zhuǎn)換,Java語言的使用者不需要了解這些細(xì)節(jié),也可以進(jìn)行高效的開發(fā)。理解這些細(xì)節(jié),以我當(dāng)前的體驗,對于開發(fā)者來講,主要是增長知識面,對開發(fā)的影響也不大。那運(yùn)行期和什么樣的開發(fā)者關(guān)系較大呢,沒錯,就是虛擬機(jī)開發(fā)人員或者需要優(yōu)化虛擬機(jī)的人員,比如,服務(wù)器的運(yùn)維人員。
Java運(yùn)行期的工作并非是固定的,最基本的,也是最簡單的,當(dāng)然,也可以理解為保底的,是使用解釋器將字節(jié)碼轉(zhuǎn)換為機(jī)器碼,該原理相當(dāng)?shù)那逦鞔_,有以下步驟:
a.按一定的規(guī)則尋找相關(guān)的類,首先,根據(jù)環(huán)境變量找到j(luò)ava類庫的位置,然后根據(jù)雙親委托模式找到準(zhǔn)確的類位置
b.然后就很簡單了,JVM字節(jié)碼可以直接逐條解釋,只是JVM是基本于棧的指令集,條數(shù)較多,解釋較為耗時。
另一種Java運(yùn)行期的處理為JIT,是一種Java動態(tài)編譯方法,也是當(dāng)前Java虛擬機(jī)效率逐漸追上C/C++的主要因素,分為兩種:Client Compiler和Server Compiler
Client Compiler較為簡單快速,其過程用下面一幅圖來說明:
Server Compiler是一個經(jīng)過充分優(yōu)化的高級編譯器,包括但不限于:無用代碼擦除、基本塊重排序等,其技術(shù)極為復(fù)雜,感興趣的同學(xué)可以查閱專門的資料研究。選用上述那種Compiler進(jìn)行處理,要視環(huán)境的需要而定。
JIT并非完全取代解釋器,只是在需要優(yōu)化的高頻方法或高頻模塊(比如多次for循環(huán))才會介入,其他時候仍采用的是解釋器的處理方式。
針對Java的源碼到機(jī)器碼的處理,上面提到了兩種編譯運(yùn)行方式分別為:編譯為字節(jié)碼+解釋器、編譯為字節(jié)碼+JIT+解析器。目前還是另一種方式,為AOT,一種靜態(tài)編譯方式,在編譯期,即將源碼編譯為機(jī)器碼,其好處也是顯然的,運(yùn)行器無JVM處理時延,效率最高,但缺點也很明顯:(1)犧牲了Java的跨平臺特性,機(jī)器碼一定是平臺相關(guān)的(2)編譯算法復(fù)雜度相當(dāng)高,JIT在運(yùn)行期,通過運(yùn)行時數(shù)據(jù)收集,制定是否編譯機(jī)器碼,但AOT在編譯期就要收集到這些。
好,總結(jié)一下,總共提到的Java的源碼到機(jī)器碼的處理為三種:
a.編譯為字節(jié)碼+解釋器
b.編譯為字節(jié)碼+JIT+解析器
c.AOT直接編譯為機(jī)器碼
Java目前應(yīng)用范圍要大于C/C++,場景也更為豐富,采用那種方式,要看那些場景更為合適,比如,Android采用的變異后的b方式(編譯后的輸出為dex文件,運(yùn)行前要將dex文件轉(zhuǎn)化為oat格式,運(yùn)行時要處理的格式為oat格式,運(yùn)行時的指令集為基于寄存器的)
從上面對C/C++及Java編譯運(yùn)行的分析,可以看出,編譯的基本原理及流程是相近的,然后加入了后期的優(yōu)化,尤其是Java,為改善編譯及運(yùn)行效率(JIT、AOT、字節(jié)碼等),做了大量的工作,至Android虛擬機(jī)(當(dāng)前文中沒細(xì)說),又根據(jù)平臺特點,在Java的基礎(chǔ)上做了大量的改進(jìn)。C/C++將編譯運(yùn)行封裝起來,對外黑盒,代碼的開發(fā)人員是難以干擾其過程的。而JVM,只是將抽象樹之前的處理封裝起來,后續(xù)的處理可以人為干預(yù)(比如JIT就是對解釋器的一種優(yōu)化)。因而JVM從復(fù)雜度及涉及的技術(shù)面、調(diào)優(yōu)人員來講規(guī)模都要遠(yuǎn)大于C/C++編譯運(yùn)行環(huán)境。