自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

爆爆:Java代碼編譯流程是怎樣的?

開發(fā) 后端
寫了這么多年的代碼,對(duì)于java代碼運(yùn)行的全流程你心里有清晰的脈絡(luò)嗎?今天就讓我們花點(diǎn)篇幅,來好好聊聊,Java代碼RUN起來的背后,那些默默付出的大功臣們。

前言

寫了這么多年的代碼,對(duì)于java代碼運(yùn)行的全流程你心里有清晰的脈絡(luò)嗎?

大家會(huì)不會(huì)跟我最開始一樣,覺得在IDE里點(diǎn)一下RUN按鈕,我們寫的代碼就直接直接跑起來了吧?

俗話說的好,你覺得生活靜好,其實(shí)只是因?yàn)橛腥嗽跒槟阖?fù)重前行,編譯器和虛擬機(jī)默默的承受了這一切。

小小的一個(gè)RUN,背后卻是很多組件共同努力的結(jié)果,它們必須非常努力,才能看起來毫不費(fèi)力。

今天就讓我們花點(diǎn)篇幅,來好好聊聊,Java代碼RUN起來的背后,那些默默付出的大功臣們。

當(dāng)我們寫下一行代碼時(shí),我們到底在寫什么?

夜深了,我們?cè)谄聊簧洗蛳乱欢蝺?yōu)雅的代碼,一邊擰開泡著枸杞的保溫杯抿了一口熱水,一邊欣賞自己詩一樣的代碼,心里默默地夸了一波自己:不愧是我!

第一個(gè)問題來了,計(jì)算機(jī)真的能看到我們寫的”詩“嗎?

眾所周知,Java是一門"一次編寫,到處運(yùn)行"的語言,也就是所謂的平臺(tái)無關(guān)性,不管在哪個(gè)平臺(tái)都能夠運(yùn)行,且保證運(yùn)行的結(jié)果與期待的一致。(這是大學(xué)老師反復(fù)強(qiáng)調(diào)的)

Java實(shí)現(xiàn)”平臺(tái)無關(guān)性“的原理也非常簡(jiǎn)單,就是利用中間格式來進(jìn)行過渡,也就是我們常說的字節(jié)碼,通過將Java源代碼轉(zhuǎn)換成字節(jié)碼,保證JVM(Java虛擬機(jī))讀取到的一定是自己能夠識(shí)別的字節(jié)碼格式。

一個(gè)通俗的解釋:你不會(huì)說法語,法國人不會(huì)講中文,但是你們或多或少都會(huì)點(diǎn)英語,把英語作為你們的中間格式,保證雙方都能明白對(duì)方的意思,這就是所謂的跨平臺(tái)。

Java源碼首先被編譯成字節(jié)碼,而這個(gè)字節(jié)碼就是實(shí)現(xiàn)平臺(tái)無關(guān)性的關(guān)鍵,無論你是什么類型的平臺(tái),只要你安裝了能夠識(shí)別字節(jié)碼的JVM(Java虛擬機(jī)),通過JVM對(duì)字節(jié)碼文件進(jìn)行解析,把字節(jié)碼轉(zhuǎn)換成具體平臺(tái)上的機(jī)器指令,就可以實(shí)現(xiàn)跨平臺(tái)的運(yùn)行了。

因此別說讓計(jì)算機(jī)底層讀到我們寫的”代碼詩“了,就連Java虛擬機(jī)都拿不到我們?cè)兜拇a,在編譯器的努力下,Java源代碼已經(jīng)變成大白話的class文件了。

所以啊寶,操作系統(tǒng)欣賞不到我們”詩一樣的代碼“,我們所寫的每一行代碼,都會(huì)變成一條條指令,對(duì)操作系統(tǒng)來說,它看到的不是編程的藝術(shù),只是自己需要完成的一條條KPI罷了。

文本即代碼?

如果我們寫了具有同樣內(nèi)容的Java文件和txt文本,他們?cè)谖谋揪庉嬈髦虚L(zhǎng)得是沒有區(qū)別的。

有一句名言是:世界上最好的IDE是txt文本編輯器?,F(xiàn)在我們可能用IDE都用順手了,很多的操作我們都習(xí)慣于讓IDE給我們提示,依賴于IDE的代碼補(bǔ)全和快捷鍵。

但在傳說中,有一群用記事本就能打出優(yōu)美代碼的大佬,到了這個(gè)境界時(shí),已經(jīng)是人碼合一,無需語法高亮,無需補(bǔ)全提示,所有的正確語法都了然于心,打出來的每一行代碼都是可以直接編譯run起來且零BUG的好代碼(doge)。

扯得有點(diǎn)遠(yuǎn)了,但用記事本確實(shí)是可以實(shí)現(xiàn)開發(fā)功能,只要你自己打的代碼邏輯正確,且沒有語法錯(cuò)誤,最后保存的后綴是.java,就能作為代碼去運(yùn)行了。

因此,從本質(zhì)來說,我們所打出來的txt文本和Java代碼在一開始是沒有多大區(qū)別的,用普通的文本編輯器也能打開我們的.java后綴的文件。但是文本編輯器能做到的也僅僅限于看到.java文件里面的代碼文本而已了。

Java編譯器才是最終,能夠識(shí)別并理解.java文件的存在。

Java代碼想要運(yùn)行起來,第一步就是得到編譯器的認(rèn)可。編譯器的任務(wù)很簡(jiǎn)單,就是將符合Java語言源碼編譯為符合 Java虛擬機(jī)規(guī)范的Class文件,如果輸入的Java源碼不符合規(guī)范則需要報(bào)告錯(cuò)誤。

可以說,編譯的過程是Java開發(fā)的第一小步,但也是程序的一大步。

接下來我們先介紹一下編譯器在Java體系中的位置。

JDK與JRE的愛恨情仇

在我們初學(xué)java時(shí),一定安裝過所謂的java環(huán)境,當(dāng)我們自信滿滿地點(diǎn)進(jìn)了Oracle的Java官網(wǎng),映入眼簾的是兩個(gè)看起來很像的安裝包:

這我就蒙蔽了呀,我就想裝個(gè)Java環(huán)境,怎么有兩個(gè)奇奇怪怪的安裝包,一個(gè)叫JDK,一個(gè)叫JRE,這兩個(gè)安裝包跟俗稱的”Java“又有什么關(guān)系?

先理清楚所謂的JDK和JRE到底有什么區(qū)別吧,來看一張Java 8的體系架構(gòu)圖(https://docs.oracle.com/javase/8/docs/):

jdk8體系架構(gòu)圖

JDK全稱是Java開發(fā)工具包(Java Development Kit),它包含了Java從開發(fā)到運(yùn)行的各種工具。

JRE指的則是Java運(yùn)行環(huán)境(Java Runtime Environment),它包含了基礎(chǔ)類庫和JVM虛擬機(jī)。

上圖展示的是Java 8的體系結(jié)構(gòu),最左邊的一欄很清晰的表明了JDK和JRE各自的范圍,我們也很容易發(fā)現(xiàn):

JRE是JDK的子集。

既然你要搞開發(fā),肯定得保證自己寫的代碼能運(yùn)行起來吧,所以當(dāng)開發(fā)人員安裝好JDK之后里面已經(jīng)包含了一個(gè)運(yùn)行環(huán)境JRE,保證自己的代碼能夠得到運(yùn)行和驗(yàn)證,這就是為什么JRE被包含在JDK中。

但如果我們是普通用戶,并不關(guān)心開發(fā),甚至根本不懂代碼,我只想要代碼跑起來的結(jié)果,那只需要本地有JRE運(yùn)行環(huán)境就行了。

如果用過零幾年的按鍵手機(jī),你就會(huì)深有體會(huì),那時(shí)候很多的手機(jī)軟件都是用Java編寫的,只需要一個(gè)JAR包,你就能收獲快樂。

手機(jī)Java應(yīng)用

反向思維一下,既然安裝JRE就能運(yùn)行JAVA代碼,但要需要完整的JDK才能完成開發(fā),那他們之間的差集肯定跟開發(fā)的過程有關(guān)。

所以接下來,我們來探討一下為什么缺少這一塊內(nèi)容就只能成為運(yùn)行環(huán)境,而不能承擔(dān)開發(fā)功能呢?

 

JDK和JRE的差集

這一塊里我們可以看到幾個(gè)很熟悉的命令:

  • javac:用于編譯java源代碼,生成class文件;
  • javap:用于反編譯,根據(jù)class文件,反解析出其中的匯編指令和其他信息;
  • javadoc:用于生成java文檔的命令。

其中,我們最常用的、最重要的就是javac命令。這是JDK中內(nèi)嵌的編譯器,通過這個(gè)命令,可以將java源文件轉(zhuǎn)換成class文件。這個(gè)javac編譯器就是JRE相比于JDK少了開發(fā)功能的決定性元素!!

我們用一個(gè)簡(jiǎn)單的例子看看,開發(fā)者編寫好的java代碼在完整的JDK架構(gòu)下,經(jīng)過JDK、JRE以及JVM的運(yùn)行過程。

java代碼運(yùn)行的簡(jiǎn)單示例

可以看到,通過JDK中的javac命令,我們才能將java源代碼編譯成class文件,而前面也提到了,這個(gè)class文件才是最終放到JVM中運(yùn)行的文件。

我們把java源碼到class文件的過程稱之為編譯階段,把class文件到JVM中運(yùn)行得到結(jié)果的階段稱為運(yùn)行階段。

因此,如果只有JRE而沒有完整的JDK的話,相當(dāng)于就少了編譯源代碼的關(guān)鍵工具,你只能依賴人家傳遞的,已經(jīng)編譯好的class代碼,將程序運(yùn)行起來,而不具備修改、開發(fā)的能力。

聰明的你很快就能發(fā)現(xiàn),既然虛擬機(jī)運(yùn)行需要的其實(shí)是class文件,因此它對(duì)于最前面用的是什么語言其實(shí)并不關(guān)心,只要支持生成JVM能夠識(shí)別的字節(jié)碼就行了。

難道說……

沒錯(cuò),恭喜你發(fā)現(xiàn)了JVM虛擬機(jī)**”跨語言“的特性**。

很多語言依賴了這種特性,將自己本身的源代碼,編譯生成class文件,并基于JVM虛擬機(jī)運(yùn)行。比較常用的有Scala和Kotlin等,它們甚至可以跟Java語言相互調(diào)用,因?yàn)樽罱K都是要編譯成class文件到虛擬機(jī)中運(yùn)行嘛,所以即使在源代碼階段是不同的語言,經(jīng)過編譯器之后,大家都變成了一樣的字節(jié)碼。

多語言轉(zhuǎn)換為字節(jié)碼

當(dāng)然,要是再極端一點(diǎn),由于class文件本質(zhì)上也是一個(gè)二進(jìn)制的文件,因此只要你足夠強(qiáng),能夠徒手寫出自己需要的二進(jìn)制文件,你也就不再需要編譯器了(狗頭保命)。

很多讀者就要說了:”我們是來學(xué)技術(shù)的,不是來學(xué)仙術(shù)的“。

先別笑,直接改字節(jié)碼并不是什么天上飛的仙術(shù),而是實(shí)打?qū)嵉募夹g(shù)。像我們熟悉的lombok,就能夠根據(jù)我們編寫的注解生成字節(jié)碼,實(shí)現(xiàn)字節(jié)碼的修改增強(qiáng)(但lombok也是利用了編譯器的一些特性,是在編譯階段觸發(fā)操作的)。

類似的還有諸如ASM等一些字節(jié)碼增強(qiáng)技術(shù),也是通過直接操作字節(jié)碼來實(shí)現(xiàn)的。

通過字節(jié)碼增強(qiáng)技術(shù)可以實(shí)現(xiàn)熱部署等操作,讓你修改代碼之后無需重啟服務(wù)就能生效;也可以實(shí)現(xiàn)日志注入等功能,在不需要改變客戶端調(diào)用方式情況下完成對(duì)指定方法增加緩存或日志的功能。

但對(duì)于大部分的普通開發(fā)者來說,編譯器還是必不可少的。

編譯階段

當(dāng)調(diào)用javac命令,觸發(fā)java代碼的編譯過程,將.java文件編譯成了.class二進(jìn)制文件。

那么,在編譯器中,源代碼到底是怎么一步步變化的呢。

注意:javac是javac編譯器的自帶的命令,但市面上可用的并不只有javac這一種編譯器,有一些其他的廠商也根據(jù)java的標(biāo)準(zhǔn)開發(fā)了自己的編譯器。例如Eclipse的ecj(the Eclipse Compiler for Java)等。

只是大部分人用的都是JDK自帶的javac的編譯器,因此下文的討論都是基于javac編譯器展開的。

可以這樣理解,編譯的過程就是”編“和”譯“。

編:將java源代碼的結(jié)構(gòu)組織成合適的格式,包括編譯過程中的抽象語法樹和符號(hào)表等,并在最終將源碼編碼成為class文件。

譯:對(duì)源代碼中的語義進(jìn)行解析,并準(zhǔn)確地翻譯成另一種形式(字節(jié)碼)。這一步既要確保原格式正確(Java源代碼中的語法正確),又要確保翻譯后的字節(jié)碼跟源代碼表達(dá)的意思一致。

也就是說,編譯的過程要保證 輸入的格式符合Java語言規(guī)范,輸出的格式符合Java虛擬機(jī)規(guī)范。

這個(gè)過程說起來復(fù)雜,但是讀者可以回憶一下自己經(jīng)歷過的代碼編譯失敗的場(chǎng)景,每一次編譯失敗都是編譯器在默默工作的結(jié)果,不同的錯(cuò)誤可能是在編譯過程的不同階段被發(fā)現(xiàn)并拋出的。

接下來,我們循序漸進(jìn)地告訴大家編譯的具體步驟,以及編譯過程的各個(gè)階段拋出的不同編譯異常。

編譯過程調(diào)用圖

東西看起來很多哈,總結(jié)起來大概可以分為下面幾個(gè)步驟:

1. 詞法分析&語法分析

詞法分析是最開始的一步,主要的作用就是把源代碼的字符流轉(zhuǎn)換成Token集合,Token是指代碼中具有獨(dú)立語義且不可再分的標(biāo)記。

這里要注意,一個(gè)Token指的并不是單個(gè)的字符,而是具有實(shí)義的詞。而且,編譯器還會(huì)識(shí)別不同的詞法類型,為它分配對(duì)應(yīng)的Token類型,比如,int就會(huì)被識(shí)別為Token.INT ,運(yùn)算符也會(huì)被分配為對(duì)應(yīng)的Token類型,例如+就是Token.PLUS:

詞法分析

當(dāng)代碼被解析為一系列的Token集合之后,下一步是進(jìn)行語法分析。

語法分析是根據(jù)解析后的Token集合,解析出抽象語法樹(Abstract Syntax Tree, AST),AST中包含了java代碼中的層級(jí)結(jié)構(gòu)。

小知識(shí):在NLP等領(lǐng)域的研究中,語法樹也是用來分析語法規(guī)則及原理的重要手段,在這里不過多闡述。

語法分析1

根據(jù)這個(gè)結(jié)構(gòu),可以層級(jí)地展示代碼中所有的變量、方法甚至是注釋等各種信息。

構(gòu)建AST的過程會(huì)判斷Token的類型與其在樹中的位置是否匹配,這一步我們很好理解哈,你用關(guān)鍵字作為變量名稱的時(shí)候編譯會(huì)不通過,就是在這一步被逮到的。

例如,你用這樣一段代碼去編譯:

  1. public class Hello { 
  2.     public static void main(String[] args) { 
  3.         String enum = "world"
  4.         System.out.println("Hello world"); 
  5.     } 

會(huì)報(bào)如下的錯(cuò)誤:

  • error: as of release 5, 'enum' is a keyword, and may not be used as an identifier

因?yàn)閑num是關(guān)鍵字,構(gòu)建語法樹的時(shí)候發(fā)現(xiàn)堂堂一個(gè)關(guān)鍵字居然出現(xiàn)在了標(biāo)識(shí)符的位置,這可使不得啊!

因此AST樹構(gòu)建失敗,編譯報(bào)錯(cuò)。

詞法分析&語法分析是對(duì)源代碼中文本的抽象,將.java源代碼中的文本結(jié)構(gòu)按照編譯器特定的規(guī)則拆分、解析,為后續(xù)的編譯工作鋪平了道路,后面的操作都離不開這個(gè)AST。

2. 填充符號(hào)表符號(hào)表

就是由符號(hào)地址(位置)和符號(hào)信息構(gòu)成的”表格“,它存儲(chǔ)的是標(biāo)識(shí)所對(duì)應(yīng)的類型、作用域等。

這里說它是”表格“可能會(huì)對(duì)讀者產(chǎn)生一定的誤解,實(shí)際上它不是像我們想象的那種二維的表格,而是更接近hashTable那樣的鍵值對(duì)結(jié)構(gòu),符號(hào)表可以由數(shù)組、樹狀結(jié)構(gòu)或者棧等各種結(jié)構(gòu)來實(shí)現(xiàn)。

這個(gè)符號(hào)表在后續(xù)的很多步驟都能發(fā)揮作用,例如:

  1. static char x;   
  2. int foo() {      
  3.   int x;      
  4.   {         
  5.     float x;      
  6.   }   

這段代碼有三個(gè)同名變量,聰明的讀者肯定能夠分辨它們各自的作用域,但是笨笨的計(jì)算機(jī)沒辦法那么快分清它們的區(qū)別。

為了在解析符號(hào)和類型的時(shí)候分清它們的作用域而不產(chǎn)生使用沖突,就需要通過符號(hào)表來記錄關(guān)系。

填充符號(hào)表的過程可以描述為:

  1. 將每個(gè)AST的頂層節(jié)點(diǎn)都放到待處理的列表中,并逐個(gè)處理;
  2. 將所有的類符號(hào)(類的聲明,名稱)都輸出到外層的作用域的符號(hào)表中;
  3. 如果發(fā)現(xiàn)有package-info.java文件(描述整個(gè)包的信息和包內(nèi)的常量),將其頂層節(jié)點(diǎn)放到待處理的列表中;
  4. 明確泛型類型的真實(shí)類型;
  5. 如果類中沒有任何構(gòu)造器,則添加默認(rèn)的無參構(gòu)造器;
  6. 將類中符號(hào)輸入到類自身的符號(hào)表中。

這一步有點(diǎn)抽象了,大家也不用太糾結(jié)于細(xì)節(jié),能夠明白大概的流程和目的就行了,只需要理解,這一步就是為了生成記錄了類中符號(hào)的類型、屬性等信息的符號(hào)表,方便后續(xù)流程中的應(yīng)用。

強(qiáng)調(diào)一下5,學(xué)過java基礎(chǔ)的都知道,如果一個(gè)類沒有定義構(gòu)造器,則會(huì)默認(rèn)一個(gè)默認(rèn)構(gòu)建無參構(gòu)造器,添加默認(rèn)構(gòu)造器的操作也是在填充符號(hào)表時(shí)完成的。

為什么呢?

很簡(jiǎn)單,因?yàn)轭惖臉?gòu)造方法也是需要放到符號(hào)表里記錄的,而且不能為空,既然你沒有指定,那我就給你放一個(gè)默認(rèn)的空參構(gòu)造器,然后記錄到符號(hào)表咯。

相關(guān)的源碼就放著這里了,大家有興趣可以深挖一下。http://hg.openjdk.java.net/jdk8u/jdk8u/langtools/file/2baeb96fa198/src/share/classes/com/sun/tools/javac/comp/Enter.java

3. 注解處理

自從JDK 5以來,Java提供了對(duì)注解的支持,現(xiàn)在程序中使用注解已經(jīng)是非常常規(guī)的操作。

然而要注意的是,并不是所有的注解都是在編譯期起作用的,我們平時(shí)用反射處理的注解主要是指運(yùn)行時(shí)注解,運(yùn)行時(shí)注解在編譯期不受影響,在編譯之后的class文件中還是會(huì)保留,最終要在class文件到JVM運(yùn)行的過程中才生效。

而編譯期注解是指以@Retention(RetentionPolicy.SOURCE)定義的,在編譯期就處理了的注解,這一類注解不會(huì)保留到class文件中。

聽起來很懵,但其實(shí)編譯過程中這一步注解處理其實(shí)大家在無意中已經(jīng)接觸過很多次了,比如大家常用的lombok,就是在這一步起作用的。

lombok采用的就是編譯期注解處理的方法,因此當(dāng)我們編譯好用了lombok注解的.java文件后,打開生成的class文件就可以看到lombok相關(guān)的注解已經(jīng)消失,而相應(yīng)的getter、setter方法則已經(jīng)被注入到class文件中。

上圖中右圖展示的并不是class文件,而是與添加lombok注解等效的源代碼,左右兩側(cè)的代碼生成的字節(jié)碼是一致的。

在這一步,lombok的注解處理器生效,并對(duì)我們前面所說的抽象語法樹AST進(jìn)行增強(qiáng)處理。

首先找到@Data注解所在類對(duì)應(yīng)的語法樹(AST),然后修改該語法樹(AST),增加getter和setter方法定義的相應(yīng)樹節(jié)點(diǎn),實(shí)現(xiàn)我們所需的功能。

這一步也是為數(shù)不多的,編譯器留給程序員自己編寫代碼來影響源代碼編譯過程的機(jī)會(huì)。

注解處理完成后,可能又會(huì)產(chǎn)生新的符號(hào),因此如果執(zhí)行了注解處理,需要再執(zhí)行一次解析和填充符號(hào)表的操作(回到第2步)。

4. 語義分析

語義分析聽起來跟第一步詞法分析&語法分析看起來很像,但其實(shí)是有很大區(qū)別。

我們類比成語文來解釋:

敖丙說:”吃你飯今天了嗎?“。

詞法分析的步驟相當(dāng)于把這一句話拆成了你、吃、今天、飯、了、嗎、?,這幾個(gè)詞語。每個(gè)詞都沒問題。

可是到了語義分析階段,我們?cè)俑鶕?jù)規(guī)則檢查這句話的語義,發(fā)現(xiàn)這句話其實(shí)是不通順的。

回到編譯過程中來解釋,語義分析的功能就是從結(jié)構(gòu)和規(guī)則上對(duì)源代碼進(jìn)行檢查,包括聲明檢查和類型檢查等等。

這里我們用周志明老師書中的一個(gè)例子來說明:

假設(shè)有如下3個(gè)變量定義的語句:

  1. int a = 1;  
  2. boolean b = false;  
  3. char c= 2;  
  4.  
  5. int d =a + c;  
  6. int e = b + c;  
  7. char f = a + c;  

這一段代碼能夠通過第一步的詞法分析和語法分析,并構(gòu)成正確的AST,但是在語義分析中會(huì)報(bào)錯(cuò)。因?yàn)榫幾g器發(fā)現(xiàn)變量e和f的運(yùn)算都是不符合規(guī)范的,參與運(yùn)算的兩個(gè)值的類型不匹配該運(yùn)算符的邏輯。

語義分析更進(jìn)一步檢查上下文中變量的規(guī)范性,例如變量是否已經(jīng)聲明,變量的數(shù)據(jù)類型與其參與的運(yùn)算是否匹配等等。

如果要對(duì)語義分析做細(xì)分的話,可以分為以下幾個(gè)小階段:

4.1 標(biāo)注檢查

這就是剛才說的,檢查變量是否事先聲明以及運(yùn)算類型是否匹配的步驟,而且這一步的處理會(huì)影響到AST的結(jié)構(gòu):

注意圖中所示,我**們首先需要檢查變量a有沒有聲明(聲明檢查),并檢查a的類型(類型檢查),這兩個(gè)檢查都需要用上我們前文已經(jīng)填充完成的符號(hào)表,從符號(hào)表中查詢變量的作用域和類型,**完成語義分析的檢查。

然后判斷運(yùn)算符和另一個(gè)運(yùn)算值的類型,檢查左右運(yùn)算值的類型是否匹配,能否參與運(yùn)算。

看到了嗎,在這里AST和符號(hào)表就共同發(fā)揮作用啦。

此外,標(biāo)注檢查步驟還有兩個(gè)很重要的操作:

泛型方法類型的推導(dǎo):

在這一步就需要明確泛型方法傳遞的真實(shí)類型是什么了;

常量折疊(Constant Folding):

這是一個(gè)很有意思的操作,它會(huì)進(jìn)行一些簡(jiǎn)單的常量計(jì)算,例如:int a = 1 + 2;在這一步就會(huì)被優(yōu)化為a = 3,優(yōu)化之后在AST中還是能夠看到int、a、1、+、2、;這幾個(gè)標(biāo)記,但是這個(gè)表達(dá)式的值已經(jīng)被計(jì)算出來了,并在AST上進(jìn)行了標(biāo)注。也就是說,現(xiàn)在的AST既保留了表達(dá)式的結(jié)構(gòu),也記錄了表達(dá)式的結(jié)果。

當(dāng)后續(xù)到虛擬機(jī)中去執(zhí)行字節(jié)碼的時(shí)候,由于編譯期常量折疊的優(yōu)化,int a = 3和int a = 1 + 2的運(yùn)行效率其實(shí)是一樣的,因?yàn)檫@一個(gè)常量的運(yùn)算在編譯期已經(jīng)做完,不會(huì)再額外消耗運(yùn)行期的處理時(shí)間。

一般的代碼優(yōu)化都是要到生成字節(jié)碼之后,等到運(yùn)行期在虛擬機(jī)的解釋器中再進(jìn)行的。而常量折疊是javac編譯器對(duì)源代碼做的極少量的優(yōu)化措施之一,也是為數(shù)不多的編譯期對(duì)代碼進(jìn)行優(yōu)化的操作。

4.2 數(shù)據(jù)流分析

數(shù)據(jù)流分析是在標(biāo)注檢查之后的進(jìn)一步檢驗(yàn),主要檢驗(yàn)是局部變量在使用前是否確定性賦值、聲明有返回值的方法是否有確定性的返回值等。

值得注意的是,final變量不可重復(fù)賦值的性質(zhì)也是在這一步檢查,如果一個(gè)final變量被重復(fù)賦值,編譯器會(huì)發(fā)現(xiàn)并報(bào)錯(cuò)的。也正是因?yàn)檫@個(gè)特性,用final關(guān)鍵字局部變量只會(huì)在編譯期去校驗(yàn),不會(huì)對(duì)在運(yùn)行期產(chǎn)生任何作用 。

有如下的例子:

  1. // 方法1 
  2. public void aobingTest(final int nezha){ 
  3.   final int a = 0; 
  4.  
  5. // 方法2 
  6. public void aobingTest(int nezha){ 
  7.   int a = 0; 

這兩個(gè)方法產(chǎn)生的字節(jié)碼是一模一樣的,沒有任何的差別。因此所有的final不可重復(fù)賦值的限制,都在編譯期得到了檢驗(yàn),如果聲明為final的局部變量被重復(fù)賦值,在編譯期就會(huì)報(bào)錯(cuò),如果沒有發(fā)現(xiàn)有final重復(fù)賦值的錯(cuò)誤,才會(huì)成功生成字節(jié)碼。

因此對(duì)于運(yùn)行期來說,局部變量是否聲明為final,不會(huì)有任何校驗(yàn)的步驟(因?yàn)榫植孔兞坎还苡袥]有用final限制,生成的字節(jié)碼都是一樣的,字節(jié)碼中不會(huì)保留局部變量是否聲明為final的信息)。

5. 解語法糖

簡(jiǎn)單地來說,語法糖就是方便程序員編寫的便捷寫法,這種語法不會(huì)對(duì)最終的結(jié)果產(chǎn)生實(shí)際影響,但能夠減少程序編寫者的工作量。

例如,java中的自動(dòng)拆箱裝箱功能、foreach循環(huán)功能等,都是為了程序員能夠更寫出更簡(jiǎn)潔流程的代碼而封裝的語法糖。

但是到了程序運(yùn)行階段,這樣的語法糖對(duì)計(jì)算機(jī)來說是不可識(shí)別的。因此需要在編譯階段先解語法糖,將語法還原為它本來”笨拙“的樣子。

例如,將包裝類型拆成普通類型,將增強(qiáng)for循環(huán)替換為普通的for循環(huán)。

6. 生成Class文件

終于到了生成最終需要的class文件的一步了,前面所構(gòu)建的語法樹、符號(hào)表等信息,在這一步被轉(zhuǎn)換成字節(jié)碼指令寫到class文件中,除此之外,還有兩個(gè)非常重要的方法被添加到語法樹中,他們分別是和方法。

注意,這兩個(gè)長(zhǎng)得像init的方法指的并不是類中的構(gòu)造函數(shù)。

方法是一個(gè)類的構(gòu)造器,它的作用是初試化所有的靜態(tài)變量并執(zhí)行用static {}包裹的代碼塊,而且該方法的收集是有順序的:

將這些與類相關(guān)的初始化代碼按順序收集在一起生成了函數(shù),在類加載的時(shí)候按順序運(yùn)行,所以方法相當(dāng)于是把靜態(tài)的代碼打包在一起,等待后續(xù)統(tǒng)一執(zhí)行。

  • 父類靜態(tài)變量初始化
  • 父類靜態(tài)語句塊
  • 子類靜態(tài)變量初始化
  • 子類靜態(tài)語句塊

方法其實(shí)是一個(gè)實(shí)例構(gòu)造器,它的作用是初始化類中的成員變量,例如成員變量的賦值操作,以及被{}符號(hào)包裹的代碼塊,這些方法都會(huì)被收斂到方法中成為一個(gè)跟對(duì)象初始化相關(guān)的方法。該方法的收集也是有順序的:

  • 父類代碼塊
  • 父類構(gòu)造函數(shù)
  • 子類變量初始化
  • 子類代碼塊
  • 子類構(gòu)造函數(shù)
  • 父類變量初始化

通俗來說,這兩個(gè)方法就是將源代碼中的代碼塊和變量初始化的步驟按照靜態(tài)與非靜態(tài)分為了兩類,并按一定順序打包好,等待合適的時(shí)機(jī)執(zhí)行。

對(duì)方法來說,這個(gè)合適的執(zhí)行時(shí)機(jī)就是在類被加載的時(shí)候;

而對(duì)方法來說,執(zhí)行的時(shí)機(jī)就是在該類new一個(gè)對(duì)象的時(shí)候。

由于類加載過程優(yōu)先于對(duì)象實(shí)例化過程,所以方法一定比方法先執(zhí)行。因此它們完整的執(zhí)行順序就是:

  1. 父類靜態(tài)變量初始化
  2. 父類靜態(tài)語句塊
  3. 子類靜態(tài)變量初始化
  4. 子類靜態(tài)語句塊
  5. 父類變量初始化
  6. 父類語句塊
  7. 父類構(gòu)造函數(shù)
  8. 子類變量初始化
  9. 子類語句塊
  10. 子類構(gòu)造函數(shù)

發(fā)現(xiàn)了嗎,這就是常見的面試題:”java代碼的加載順序“的標(biāo)準(zhǔn)答案。

這個(gè)問題的本質(zhì)其實(shí)在于:Java代碼能夠保持加載順序的原因就是在生成class文件時(shí),將按順序拼接好的和方法添加到了class文件中,在后續(xù)的運(yùn)行過程中再按順序執(zhí)行。

以后面試遇到這個(gè)問題知道怎么答了嗎。

除了生成構(gòu)造器之外,生成class文件時(shí)還會(huì)優(yōu)化某些代碼邏輯的實(shí)現(xiàn)方式,比如,將字符串的+運(yùn)算操作,替換為StringBuffer或者StringBuilder的append()方法。

到此為止,java源代碼到class文件的編譯過程進(jìn)入了尾聲。

由于篇幅原因,今天暫時(shí)講到Java代碼編譯為class文件的過程,后續(xù)我們?cè)倮^續(xù)鉆研class文件中的細(xì)節(jié)以及字節(jié)碼最終在JVM中運(yùn)行的流程。

一些思考

對(duì)了,還有一個(gè)問題可能是大家理解上的誤區(qū)。

很多人會(huì)認(rèn)為class文件 = 字節(jié)碼,這是不對(duì)的,class文件并不等于字節(jié)碼。我們從class文件的結(jié)構(gòu)中可以窺見端倪,class文件中記錄了如下的一些信息:

  1. 結(jié)構(gòu)信息:class文件格式版本號(hào);
  2. 元數(shù)據(jù):主要對(duì)應(yīng)的是Java源代碼中”聲明“和”常量“對(duì)應(yīng)的信息,包括類的聲明信息、類中屬性域與方法的聲明信息、常量池等;
  3. 方法信息:主要對(duì)應(yīng)Java源代碼中”語句“和”表達(dá)式“對(duì)應(yīng)的信息,包括 字節(jié)碼、異常處理器表、操作數(shù)棧和局部變量區(qū)的大小等;

這下就很清晰了,字節(jié)碼是Class文件的一個(gè)子集,只是class文件中眾多組成部分的其中之一。

乖,以后別再以為Class文件就是字節(jié)碼了。

 

責(zé)任編輯:姜華 來源: 敖丙
相關(guān)推薦

2018-09-05 10:14:32

小程序

2025-02-12 10:06:25

2009-04-30 17:12:50

2016-09-19 15:45:35

戴爾

2024-02-22 10:17:39

AI模型

2017-11-10 17:16:53

2020-12-29 05:39:44

日志服務(wù)環(huán)境

2022-08-10 12:01:50

DrayTek路由器漏洞

2013-05-22 10:28:19

2013-03-05 14:35:37

2024-03-04 09:00:00

2021-02-11 09:14:36

內(nèi)存虛擬機(jī)數(shù)據(jù)

2015-11-10 09:09:23

代碼程序員成長(zhǎng)

2022-08-28 21:41:19

低代碼/無代碼

2020-11-12 07:49:18

MySQL

2009-12-04 19:29:33

2024-12-30 20:32:36

2020-11-11 08:55:32

SparkJava磁盤

2025-01-21 11:35:45

2009-02-17 13:44:57

短信漏洞N73短信門
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)