Java 編譯器


javac

javac 就是一個編譯器;編譯器就是把一種語言轉換成另一種語言,也就是將對人友好的語言轉換成對機器友好的語言。因此,javac 是把 *.java 源代碼編譯成 *.class 字節碼,JVM 可以識別的二進制。

主要由4個模塊組成:

詞法剖析器:識別 java 中的 if、else、for、while 等關鍵字及其語句的合法性,構成符合標准的 token 流。
語法剖析器:對構成的 token 流進行語法剖析,檢查它們構成的組合是否符合 java 語法標准,構成抽象的語法樹。
語義剖析器:將 foreach、注解等雜亂的語法轉換成最簡略的語法,構成注解往后的語法樹。
字節碼生成器:將注解往后的語法樹翻譯器字節碼。

Jit

Jit 是 just in time 的縮寫, 也就是即時編譯編譯器。使用即時編譯器技術,能夠加速 Java 程序的執行速度。下面,就對該編譯器技術做個簡單的講解。

首先,我們大家都知道,通常通過 javac 將程序源代碼編譯,轉換成 java 字節碼,JVM 通過解釋字節碼將其翻譯成對應的機器指令,逐條讀入,逐條解釋翻譯。很顯然,經過解釋執行,其執行速度必然會比可執行的二進制字節碼程序慢很多。為了提高執行速度,引入了 JIT 技術。
在運行時 JIT 會把翻譯過的機器碼保存起來,以備下次使用,因此從理論上來說,采用該 JIT 技術可以接近以前純編譯技術。下面我們看看,JIT 的工作過程。

 

 

初級調優:客戶模式或服務器模式

JIT 編譯器在運行程序時有兩種編譯模式可以選擇,並且其會在運行時決定使用哪一種以達到最優性能。這兩種編譯模式的命名源自於命令行參數(eg: -client 或者 -server)。JVM Server 模式與 client 模式啟動,最主要的差別在於:

-server 模式啟動時,速度較慢,但是一旦運行起來后,性能將會有很大的提升。原因是:當虛擬機運行在-client 模式的時候,使用的是一個代號為 C1 的輕量級編譯器,而-server 模式啟動的虛擬機采用相對重量級代號為 C2 的編譯器。C2 比 C1 編譯器編譯的相對徹底,服務起來之后,性能更高。

通過 java -version 命令行可以直接查看當前系統使用的是 client 還是 server 模式。例如:

 

 

中級編譯器調優

大多數情況下,優化編譯器其實只是選擇合適的 JVM 以及為目標主機選擇合適的編譯器(-client,-server 或是 -XX:+TieredCompilation)。多層編譯經常是長時運行應用程序的最佳選擇,短暫應用程序則選擇毫秒級性能的 client 編譯器。

 

優化代碼緩存

當 JVM 編譯代碼時,它會將匯編指令集保存在代碼緩存。代碼緩存具有固定的大小,並且一旦它被填滿,JVM 則不能再編譯更多的代碼。我們可以很容易地看到如果代碼緩存很小所具有的潛在問題。有些熱點代碼將會被編譯,而其他的則不會被編譯,這個應用程序將會以運行大量的解釋代碼來結束。

這是當使用 client 編譯器模式或分層編譯時很頻繁的一個問題。當使用普通 server 編譯器模式時,編譯合格的類的數量將被填入代碼緩存,通常只有少量的類會被編譯。但是當使用 client 編譯器模式時,編譯合格的類的數量將會高很多。

在 Java 7 版本,分層編譯默認的代碼緩存大小經常是不夠的,需要經常提高代碼緩存大小。大型項目若使用 client 編譯器模式,則也需要提高代碼緩存大小。

現在並沒有一個好的機制可以確定一個特定的應用到底需要多大的代碼緩存。因此,當需要提高代碼緩存時,這將是一種湊巧的操作,一個通常的做法是將代碼緩存變成默認大小的兩倍或四倍。

可以通過 –XX:ReservedCodeCacheSize=Nflag(N 就是之前提到的默認大小)來最大化代碼緩存大小。

代碼緩存的管理類似於 JVM 中的內存管理:有一個初始大小(用-XX:InitialCodeCacheSize=N 來聲明)。代碼緩存的大小從初始大小開始,隨着緩存被填滿而逐漸擴大。代碼緩存的初始大小是基於芯片架構(例如 Intel 系列機器,client 編譯器模式下代碼緩存大小起始於 160KB,server 編譯器模式下代碼緩存大小則起始於 2496KB)以及使用的編譯器的。重定義代碼緩存的大小並不會真正影響性能,所以設置 ReservedCodeCacheSize 的大小一般是必要的。

再者,如果 JVM 是 32 位的,那么運行過程大小不能超過 4GB。這包括了 Java 堆,JVM 自身所有的代碼空間(包括其本身的庫和線程棧),應用程序分配的任何的本地內存,當然還有代碼緩存。
所以說代碼緩存並不是無限的,很多時候需要為大型應用程序來調優(或者甚至是使用分層編譯的中型應用程序)。比如 64 位機器,為代碼緩存設置一個很大的值並不會對應用程序本身造成影響,應用程序並不會內存溢出,這些額外的內存預定一般都是被操作系統所接受的。

 

編譯閾值

在 JVM 中,編譯是基於兩個計數器的:一個是方法被調用的次數,另一個是方法中循環被回彈執行的次數。回彈可以有效的被認為是循環被執行完成的次數,不僅因為它是循環的結尾,也可能是因為它執行到了一個分支語句,例如 continue。
當 JVM 執行一個 Java 方法,它會檢查這兩個計數器的總和以決定這個方法是否有資格被編譯。如果有,則這個方法將排隊等待編譯。這種編譯形式並沒有一個官方的名字,但是一般被叫做標准編譯。
但是如果方法里有一個很長的循環或者是一個永遠都不會退出並提供了所有邏輯的程序會怎么樣呢?這種情況下,JVM 需要編譯循環而並不等待方法被調用。所以每執行完一次循環,分支計數器都會自增和自檢。如果分支計數器計數超出其自身閾值,那么這個循環(並不是整個方法)將具有被編譯資格。
這種編譯叫做棧上替換(OSR),因為即使循環被編譯了,這也是不夠的:JVM 必須有能力當循環正在運行時,開始執行此循環已被編譯的版本。換句話說,當循環的代碼被編譯完成,若 JVM 替換了代碼(前棧),那么循環的下個迭代執行最新的被編譯版本則會更加快。

標准編譯是被-XX:CompileThreshold=Nflag 的值所觸發。Client 編譯器模式下,N 默認的值 1500,而 Server 編譯器模式下,N 默認的值則是 10000。改變 CompileThreshold 標志的值將會使編譯器相對正常情況下提前(或推遲)編譯代碼。在性能領域,改變 CompileThreshold 標志是很被推薦且流行的方法。事實上,您可能知道 Java 基准經常使用此標志(比如:對於很多 server 編譯器來說,經常在經過 8000 次迭代后改變次標志)。

我們已經知道 client 編譯器和 server 編譯器在最終的性能上有很大的差別,很大程度上是因為編譯器在編譯一個特定的方法時,對於兩種編譯器可用的信息並不一樣。降低編譯閾值,尤其是對於 server 編譯器,承擔着不能使應用程序運行達到最佳性能的風險,但是經過測試應用程序我們也發現,將閾值從 8000 變成 10000,其實有着非常小的區別和影響。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM