java官方编译器_JAVA 编译器
javac做了些什么?
說白了,javac就是一個編譯器;編譯器就是把一種語言規(guī)矩轉(zhuǎn)換成另一種語言規(guī)矩,也就是將對人友好的語言轉(zhuǎn)換成對機(jī)器友好的語言。因此,javac是把Java源代碼編譯成Java字節(jié)碼,即JVM可以識別的二進(jìn)制;表面上就是將.java文件轉(zhuǎn)成.class文件。
javac做了些什么?
主要由4個模塊組成。
詞法剖析器:識別Java中的if、else、for、while等關(guān)鍵字及其語句的合法性,構(gòu)成符合標(biāo)準(zhǔn)的Token流。
語法剖析器:對構(gòu)成的Token流進(jìn)行語法剖析,檢查它們構(gòu)成的組合是否符合Java語法標(biāo)準(zhǔn),構(gòu)成抽象的語法樹。
語義剖析器:將foreach、注解等雜亂的語法轉(zhuǎn)換成最簡略的語法,構(gòu)成注解往后的語法樹。
字節(jié)碼生成器:將注解往后的語法樹翻譯器字節(jié)碼。
在之前的面試解說中我們經(jīng)剖析了由常量表達(dá)式計(jì)算出的字符串(字面量+字面量),為 什么并不會發(fā)生多個對象?
因?yàn)閖avac在編譯期間,已經(jīng)對這些字符串進(jìn)行了合并操作。
JIT做了些什么
JIT 是 just in time 的縮寫, 也就是即時編譯編譯器。使用即時編譯器技術(shù),能夠加速 Java 程序的執(zhí)行速度。下面,就對該編譯器技術(shù)做個簡單的講解。
首先,我們大家都知道,通常通過 javac 將程序源代碼編譯,轉(zhuǎn)換成 java 字節(jié)碼,JVM 通過解釋字節(jié)碼將其翻譯成對應(yīng)的機(jī)器指令,逐條讀入,逐條解釋翻譯。很顯然,經(jīng)過解釋執(zhí)行,其執(zhí)行速度必然會比可執(zhí)行的二進(jìn)制字節(jié)碼程序慢很多。為了提高執(zhí)行速度,引入了 JIT 技術(shù)。
在運(yùn)行時 JIT 會把翻譯過的機(jī)器碼保存起來,以備下次使用,因此從理論上來說,采用該 JIT 技術(shù)可以接近以前純編譯技術(shù)。下面我們看看,JIT 的工作過程。
image.png
初級調(diào)優(yōu):客戶模式或服務(wù)器模式
JIT 編譯器在運(yùn)行程序時有兩種編譯模式可以選擇,并且其會在運(yùn)行時決定使用哪一種以達(dá)到最優(yōu)性能。這兩種編譯模式的命名源自于命令行參數(shù)(eg: -client 或者 -server)。JVM Server 模式與 client 模式啟動,最主要的差別在于:-server 模式啟動時,速度較慢,但是一旦運(yùn)行起來后,性能將會有很大的提升。原因是:當(dāng)虛擬機(jī)運(yùn)行在-client 模式的時候,使用的是一個代號為 C1 的輕量級編譯器,而-server 模式啟動的虛擬機(jī)采用相對重量級代號為 C2 的編譯器。C2 比 C1 編譯器編譯的相對徹底,服務(wù)起來之后,性能更高。
通過 java -version 命令行可以直接查看當(dāng)前系統(tǒng)使用的是 client 還是 server 模式。例如:
圖 2. 查看編譯模式
圖 2. 查看編譯模式
中級編譯器調(diào)優(yōu)
大多數(shù)情況下,優(yōu)化編譯器其實(shí)只是選擇合適的 JVM 以及為目標(biāo)主機(jī)選擇合適的編譯器(-cient,-server 或是-xx:+TieredCompilation)。多層編譯經(jīng)常是長時運(yùn)行應(yīng)用程序的最佳選擇,短暫應(yīng)用程序則選擇毫秒級性能的 client 編譯器。
優(yōu)化代碼緩存
當(dāng) JVM 編譯代碼時,它會將匯編指令集保存在代碼緩存。代碼緩存具有固定的大小,并且一旦它被填滿,JVM 則不能再編譯更多的代碼。
我們可以很容易地看到如果代碼緩存很小所具有的潛在問題。有些熱點(diǎn)代碼將會被編譯,而其他的則不會被編譯,這個應(yīng)用程序?qū)赃\(yùn)行大量的解釋代碼來結(jié)束。
這是當(dāng)使用 client 編譯器模式或分層編譯時很頻繁的一個問題。當(dāng)使用普通 server 編譯器模式時,編譯合格的類的數(shù)量將被填入代碼緩存,通常只有少量的類會被編譯。但是當(dāng)使用 client 編譯器模式時,編譯合格的類的數(shù)量將會高很多。
在 Java 7 版本,分層編譯默認(rèn)的代碼緩存大小經(jīng)常是不夠的,需要經(jīng)常提高代碼緩存大小。大型項(xiàng)目若使用 client 編譯器模式,則也需要提高代碼緩存大小。
現(xiàn)在并沒有一個好的機(jī)制可以確定一個特定的應(yīng)用到底需要多大的代碼緩存。因此,當(dāng)需要提高代碼緩存時,這將是一種湊巧的操作,一個通常的做法是將代碼緩存變成默認(rèn)大小的兩倍或四倍。
可以通過 –XX:ReservedCodeCacheSize=Nflag(N 就是之前提到的默認(rèn)大小)來最大化代碼緩存大小。代碼緩存的管理類似于 JVM 中的內(nèi)存管理:有一個初始大小(用-XX:InitialCodeCacheSize=N 來聲明)。代碼緩存的大小從初始大小開始,隨著緩存被填滿而逐漸擴(kuò)大。代碼緩存的初始大小是基于芯片架構(gòu)(例如 Intel 系列機(jī)器,client 編譯器模式下代碼緩存大小起始于 160KB,server 編譯器模式下代碼緩存大小則起始于 2496KB)以及使用的編譯器的。重定義代碼緩存的大小并不會真正影響性能,所以設(shè)置 ReservedCodeCacheSize 的大小一般是必要的。
再者,如果 JVM 是 32 位的,那么運(yùn)行過程大小不能超過 4GB。這包括了 Java 堆,JVM 自身所有的代碼空間(包括其本身的庫和線程棧),應(yīng)用程序分配的任何的本地內(nèi)存,當(dāng)然還有代碼緩存。
所以說代碼緩存并不是無限的,很多時候需要為大型應(yīng)用程序來調(diào)優(yōu)(或者甚至是使用分層編譯的中型應(yīng)用程序)。比如 64 位機(jī)器,為代碼緩存設(shè)置一個很大的值并不會對應(yīng)用程序本身造成影響,應(yīng)用程序并不會內(nèi)存溢出,這些額外的內(nèi)存預(yù)定一般都是被操作系統(tǒng)所接受的。
編譯閾值
在 JVM 中,編譯是基于兩個計(jì)數(shù)器的:一個是方法被調(diào)用的次數(shù),另一個是方法中循環(huán)被回彈執(zhí)行的次數(shù)。回彈可以有效的被認(rèn)為是循環(huán)被執(zhí)行完成的次數(shù),不僅因?yàn)樗茄h(huán)的結(jié)尾,也可能是因?yàn)樗鼒?zhí)行到了一個分支語句,例如 continue。
當(dāng) JVM 執(zhí)行一個 Java 方法,它會檢查這兩個計(jì)數(shù)器的總和以決定這個方法是否有資格被編譯。如果有,則這個方法將排隊(duì)等待編譯。這種編譯形式并沒有一個官方的名字,但是一般被叫做標(biāo)準(zhǔn)編譯。
但是如果方法里有一個很長的循環(huán)或者是一個永遠(yuǎn)都不會退出并提供了所有邏輯的程序會怎么樣呢?這種情況下,JVM 需要編譯循環(huán)而并不等待方法被調(diào)用。所以每執(zhí)行完一次循環(huán),分支計(jì)數(shù)器都會自增和自檢。如果分支計(jì)數(shù)器計(jì)數(shù)超出其自身閾值,那么這個循環(huán)(并不是整個方法)將具有被編譯資格。
這種編譯叫做棧上替換(OSR),因?yàn)榧词寡h(huán)被編譯了,這也是不夠的:JVM 必須有能力當(dāng)循環(huán)正在運(yùn)行時,開始執(zhí)行此循環(huán)已被編譯的版本。換句話說,當(dāng)循環(huán)的代碼被編譯完成,若 JVM 替換了代碼(前棧),那么循環(huán)的下個迭代執(zhí)行最新的被編譯版本則會更加快。
標(biāo)準(zhǔn)編譯是被-XX:CompileThreshold=Nflag 的值所觸發(fā)。Client 編譯器模式下,N 默認(rèn)的值 1500,而 Server 編譯器模式下,N 默認(rèn)的值則是 10000。改變 CompileThreshold 標(biāo)志的值將會使編譯器相對正常情況下提前(或推遲)編譯代碼。在性能領(lǐng)域,改變 CompileThreshold 標(biāo)志是很被推薦且流行的方法。事實(shí)上,您可能知道 Java 基準(zhǔn)經(jīng)常使用此標(biāo)志(比如:對于很多 server 編譯器來說,經(jīng)常在經(jīng)過 8000 次迭代后改變次標(biāo)志)。
我們已經(jīng)知道 client 編譯器和 server 編譯器在最終的性能上有很大的差別,很大程度上是因?yàn)榫幾g器在編譯一個特定的方法時,對于兩種編譯器可用的信息并不一樣。降低編譯閾值,尤其是對于 server 編譯器,承擔(dān)著不能使應(yīng)用程序運(yùn)行達(dá)到最佳性能的風(fēng)險(xiǎn),但是經(jīng)過測試應(yīng)用程序我們也發(fā)現(xiàn),將閾值從 8000 變成 10000,其實(shí)有著非常小的區(qū)別和影響。
檢查編譯過程
中級優(yōu)化的最后一點(diǎn)其實(shí)并不是優(yōu)化本身,而是它們并不能提高應(yīng)用程序的性能。它們是 JVM(以及其他工具)的各個標(biāo)志,并可以給出編譯工作的可見性。它們中最重要的就是--XX:+PrintCompilation(默認(rèn)狀態(tài)下是 false)。
如果 PrintCompilation 被啟用,每次一個方法(或循環(huán))被編譯,JVM 都會打印出剛剛編譯過的相關(guān)信息。不同的 Java 版本輸出形式不一樣,我們這里所說的是基于 Java 7 版本的。
編譯日志中大部分的行信息都是下面的形式:
清單 2. 日志形式
|
1
|
timestamp compilation_id attributes (tiered_level) method_name size depot
|
這里 timestamp 是編譯完成時的時間戳,compilation_id 是一個內(nèi)部的任務(wù) ID,且通常情況下這個數(shù)字是單調(diào)遞增的,但有時候?qū)τ?server 編譯器(或任何增加編譯閾值的時候),您可能會看到失序的編譯 ID。這表明編譯線程之間有些快有些慢,但請不要隨意推斷認(rèn)為是某個編譯器任務(wù)莫名其妙的非常慢。
用 jstat 命令檢查編譯
要想看到編譯日志,則需要程序以-XX:+PrintCompilation flag 啟動。如果程序啟動時沒有 flag,您可以通過 jstat 命令得到有限的可見性信息。
Jstat 有兩個選項(xiàng)可以提供編譯器信息。其中,-compile 選項(xiàng)提供總共有多少方法被編譯的總結(jié)信息(下面 6006 是要被檢查的程序的進(jìn)程 ID):
清單 3 進(jìn)程詳情
|
1
2
3
|
% jstat -compiler 6006
CompiledFailedInvalid TimeFailedTypeFailedMethod
206 0 0 1.97 0
|
注意,這里也列出了編譯失敗的方法的個數(shù)信息,以及編譯失敗的最后一個方法的名稱。
另一種選擇,您可以使用-printcompilation 選項(xiàng)得到最后一個被編譯的方法的編譯信息。因?yàn)?jstat 命令有一個參數(shù)選項(xiàng)用來重復(fù)其操作,您可以觀察每一次方法被編譯的情況。舉個例子:
Jstat 對 6006 號 ID 進(jìn)程每 1000 毫秒執(zhí)行一次: %jstat –printcompilation 6006 1000,具體的輸出信息在此不再描述。
高級編譯器調(diào)優(yōu)
這一節(jié)我們將介紹編譯工作剩下的細(xì)節(jié),并且過程中我們會探討一些額外的調(diào)優(yōu)策略。調(diào)優(yōu)的存在很大程度上幫助了 JVM 工程師診斷 JVM 自身的行為。如果您對編譯器的工作原理很感興趣,這一節(jié)您一定會喜歡。
編譯線程
從前文中我們知道,當(dāng)一個方法(或循環(huán))擁有編譯資格時,它就會排隊(duì)并等待編譯。這個隊(duì)列是由一個或很多個后臺線程組成。這也就是說編譯是一個異步的過程。它允許程序在代碼正在編譯時被繼續(xù)執(zhí)行。如果一個方法被標(biāo)準(zhǔn)編譯方式所編譯,那么下一個方法調(diào)用則會執(zhí)行已編譯的方法。如果一個循環(huán)被棧上替換方式所編譯,那么下一次循環(huán)迭代則會執(zhí)行新編譯的代碼。
這些隊(duì)列并不會嚴(yán)格的遵守先進(jìn)先出原則:哪一個方法的調(diào)用計(jì)數(shù)器計(jì)數(shù)更高,哪一個就擁有優(yōu)先權(quán)。所以即使當(dāng)一個程序開始執(zhí)行,并且有大量的代碼需要編譯,這個優(yōu)先權(quán)順序?qū)椭⒈WC最重要的代碼被優(yōu)先編譯(這也是為什么編譯 ID 在 PrintComilation 的輸出結(jié)果中有時會失序的另一個原因)。
當(dāng)使用 client 編譯器時,JVM 啟動一個編譯線程,而 server 編譯器有兩個這樣的線程。當(dāng)分層編譯生效時,JVM 會基于某些復(fù)雜方程式默認(rèn)啟動多個 client 和 server 線程,涉及雙日志在目標(biāo)平臺上的 CPU 數(shù)量。如下圖所示:
分層編譯下 C1 和 C2 編譯器線程默認(rèn)數(shù)量:
圖 3. C1 和 C2 編譯器默認(rèn)數(shù)量
圖 3. C1 C2 編譯器默認(rèn)數(shù)量
編譯器線程的數(shù)量可以通過-XX:CICompilerCount=N flag 進(jìn)行調(diào)節(jié)設(shè)置。這個數(shù)量是 JVM 將要執(zhí)行隊(duì)列所用的線程總數(shù)。對于分層編譯,三分之一的(至少一個)線程被用于執(zhí)行 client 編譯器隊(duì)列,剩下的(也是至少一個)被用來執(zhí)行 server 編譯器隊(duì)列。
在何時我們應(yīng)該考慮調(diào)整這個值呢?如果一個程序被運(yùn)行在單 CPU 機(jī)器上,那么只有一個編譯線程會更好一些:因?yàn)閷τ谀硞€線程來說,其對 CPU 的使用是有限的,并且在很多情況下越少的線程競爭資源會使其運(yùn)行性能更高。然而,這個優(yōu)勢僅僅局限于初始預(yù)熱階段,之后,這些具有編譯資格的方法并不會真的引起 CPU 爭用。當(dāng)一個股票批處理應(yīng)用程序運(yùn)行在單 CPU 機(jī)器上并且編譯器線程被限制成只有一個,那么最初的計(jì)算過程將比一般情況下快 10%(因?yàn)樗鼪]有被其他線程進(jìn)行 CPU 爭用)。迭代運(yùn)行的次數(shù)越多,最初的性能收益就相對越少,直到所有的熱點(diǎn)方法被編譯完性能收益也隨之終止。
總結(jié)
以上是生活随笔為你收集整理的java官方编译器_JAVA 编译器的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 服务器mt核心bug修复,RHSA-20
- 下一篇: 火力发电行业三大知识图谱应用场景,助力火