1. 程式人生 > >JVM效能優化系列-(5) 早期編譯優化

JVM效能優化系列-(5) 早期編譯優化

5. 早期編譯優化

早起編譯優化主要指編譯期進行的優化。

java的編譯期可能指的以下三種:

  1. 前端編譯器:將.java檔案變成.class檔案,例如Sun的Javac、Eclipse JDT中的增量式編譯器(ECJ)
  2. JIT編譯器(Just In Time Compiler):將位元組碼變成機器碼,例如HotSpot VM的C1、C2編譯器
  3. AOT編譯器(Ahead Of Time Compiler):直接把*.java檔案編譯成本地機器碼,例如GNU Compiler for the Java(GCJ)、Excelsior JET

本文中涉及到的編譯器都僅限於第一類,第二類編譯器跟java語言的關係不大。javac這類編譯器對程式碼的執行效率幾乎沒有任何優化措施,但javac做了許多針對java語言程式碼過程的優化措施來改善程式設計師的編碼風格和提高編碼效率,java許多的語法特性都是靠編譯器的語法糖來實現的。

5.1 javac編譯器工作流程

Sun javac編譯器的編譯過程可以分為3個過程:

  • 解析與填充符號表過程
  • 插入式註解處理器的註解處理過程
  • 分析與位元組碼生成過程

1. 解析與填充符號表

解析步驟包括了經典程式編譯原理中的詞法分析與語法分析兩個過程。

詞法、語法分析:詞法分析是將原始碼的字元流轉變為標記(Token)集合,單個字元是程式編寫過程的最小元素,而標記則是編譯過程的最小元素,關鍵字、變數名、字面量、運算子都可以成為標記
語法分析是根據Token序列構造抽象語法樹的過程,抽象語法樹(Abstract Syntax Tree,AST)是一種用來描述程式程式碼語法結構的樹形表示方式,語法樹的每一個節點都代表著程式程式碼中的一個語法結構(Construct),例如包、型別、修飾符、運算子、介面、返回值甚至程式碼註釋等都可以是一個語法結構。

填充符號表:符號表(Symbol Table)是由一組符號地址和符號資訊構成的表格,可以想象成K-V的形式。符號表中所登記的資訊在編譯的不同階段都要用到。在語義分析中,符號表所登記的內容將用於語義檢查和產生中間程式碼。在目的碼生成階段,當對符號名進行地址分配時,符號表是地址分配的依據

2. 註解處理器

註解處理器是用於提供對註解的支援,可以將其看成一組編譯器的外掛。

3. 語義分析與位元組碼生成

語法分析後,編譯器獲得了程式程式碼的抽象語法樹表示,語法樹能表示一個結構正確的源程式的抽象,但無法保證源程式是符合邏輯的。

這部分主要分如下幾步,完成語義分析與位元組碼生成:

  1. 標註檢查

標註檢查檢查的內容包括變數使用前是否已被宣告、變數與賦值之間的資料型別是否能夠匹配等。在標註檢查中,還有一個重要的動作稱為常量摺疊,這使得a=1+2比起a=3不會增加任何運算量

  1. 資料及控制流分析

資料及控制流分析是對程式上下文邏輯更進一步的驗證,可以檢查出諸如程式區域性變數在使用前是否賦值、方法的每條路徑是否都有返回值、是否所有的受查異常都被正確處理等

  1. 解語法糖

語法糖(Syntactic Sugar),也稱糖衣語法,指在計算機語言中新增的某種語法,這種語法對語言的功能並沒有影響,但方便使用。java在現代程式語言中屬於低糖語言,java中的主要語法糖包括泛型、可變引數、自動裝箱/拆箱等,虛擬機器執行時不支援這些語法,它們在編譯階段還原回簡單的基礎語法結構,這個過程稱為解語法糖

  1. 位元組碼生成

位元組碼生成階段不僅僅時把前面各個步驟所生成的資訊(語法樹、符號表)轉化成位元組碼寫到磁碟中,編譯器還進行了少量的程式碼新增和轉換工作

5.2 Java語法糖

語法糖主要是為了方便程式設計師的程式碼開發,這些語法糖並不會提供實質性的功能改進,但是他們能提高效率。

以下介紹了Java中常用的語法糖。

泛型與型別擦除

Java中的引數化型別只在原始碼中存在,在編譯後的位元組碼中,已經被替換為原來的原生型別了,並且在相應的地方插入了強制轉換程式碼。對於執行期的Java 語言來說,ArrayList

以下兩個方法,在編譯時,由於型別擦除,變成了一樣的原生型別List

void method(List<Integer> list);
void method (List<String> list);

但是如果兩者的返回值不一致,在JDK1.6中則可以編譯通過,並不是因為返回值不同,所以過載成功。只是因為加入返回值後,兩個方法的位元組碼特徵簽名不一樣了,所以可以共存。但是在JDK1.7和1.8中,依然無法通過,會報兩個方法在型別擦除後具有相同的特徵簽名。

Java程式碼中的方法特徵簽名只包含方法名稱、引數順序和引數型別,而在位元組碼中的特徵簽名還包括方法返回值及受查異常表。
方法過載要求方法具備不同的特徵簽名,返回值並不包含在方法的特徵簽名中,所以返回值不參與過載選擇。但是在Class位元組碼檔案中,只要描述符不是完全一致的兩個方法就可以共存。

自動裝箱和拆箱

自動裝箱和拆箱實現了基本資料型別與物件資料型別之間的隱式轉換。

public void autobox() {
    Integer one = 1;
    if (one == 1) {
        System.out.println(one);
    }
}

下面對自動裝箱和自動拆箱進行詳細介紹:

自動裝箱就是Java自動將原始型別值轉換成對應的物件,比如將int的變數轉換成Integer物件,這個過程叫做裝箱,反之將Integer物件轉換成int型別值,這個過程叫做拆箱。因為這裡的裝箱和拆箱是自動進行的非人為轉換,所以就稱作為自動裝箱和拆箱。原始型別byte,short,char,int,long,float,double和boolean對應的封裝類為Byte,Short,Character,Integer,Long,Float,Double,Boolean。

何時發生自動裝箱和拆箱,

  1. 賦值:
Integer iObject = 3; //autobxing - primitive to wrapper conversion
int iPrimitive = iObject; //unboxing - object to primitive conversion
  1. 方法呼叫:當我們在方法呼叫時,我們可以傳入原始資料值或者物件,同樣編譯器會幫我們進行轉換。
public static Integer show(Integer iParam){
   System.out.println("autoboxing example - method invocation i: " + iParam);
   return iParam;
}

//autoboxing and unboxing in method invocation
show(3); //autoboxing
int result = show(3); //unboxing because return type of method is Integer

自動裝箱的弊端,

自動裝箱有一個問題,那就是在一個迴圈中進行自動裝箱操作的時候,如下面的例子就會建立多餘的物件,影響程式的效能。

Integer sum = 0;
 for(int i=1000; i<5000; i++){
   sum+=i;
}

自動裝箱與比較:

下面程式的輸出結果是什麼?

public class Main {
    public static void main(String[] args) {
         
        Integer a = 1;
        Integer b = 2;
        Integer c = 3;
        Integer d = 3;
        Integer e = 321;
        Integer f = 321;
        Long g = 3L;
        Long h = 2L;
         
        System.out.println(c==d);
        System.out.println(e==f);
        System.out.println(c==(a+b));
        System.out.println(c.equals(a+b));
        System.out.println(g==(a+b));
        System.out.println(g.equals(a+b));
        System.out.println(g.equals(a+h));
    }
}

在解釋具體的結果時,首先必須明白如下兩點:

  • 當"=="運算子的兩個運算元都是 包裝器型別的引用,則是比較指向的是否是同一個物件,而如果其中有一個運算元是表示式(即包含算術運算)則比較的是數值(即會觸發自動拆箱的過程)。
  • 對於包裝器型別,equals方法並不會進行型別轉換。

下面是程式的具體輸出結果:

true
false
true
true
true
false
true

注意到對於Integer和Long,Java中,會對-128到127的物件進行快取,當建立新的物件時,如果符合這個這個範圍,並且已有存在的相同值的物件,則返回這個物件,否則建立新的Integer物件。

對於上面的結果:

c==d:指向相同的快取物件,所以返回true;
e==f:不存在快取,是不同的物件,所以返回false;
c==(a+b):直接比較的數值,因此為true;
c.equals(a+b):比較的物件,由於存在快取,所以兩個物件一樣,返回true;
g==(a+b):直接比較的數值,因此為true;
g.equals(a+b):比較物件,由於equals也不會進行型別轉換,a+b為Integer,g為Long,因此為false;
g.equals(a+h):和上面不一樣,a+h時,a會進行型別轉換,轉成Long,接著比較兩個物件,由於Long存在快取,所以兩個物件一致,返回true。

關於equals和==:

  • .equals(...) will only compare what it is written to compare, no more, no less.
  • If a class does not override the equals method, then it defaults to the equals(Object o) method of the closest parent class that has overridden this method.
  • If no parent classes have provided an override, then it defaults to the method from the ultimate parent class, Object, and so you're left with the Object#equals(Object o) method. Per the Object API this is the same as ==; that is, it returns true if and only if both variables refer to the same object, if their references are one and the same. Thus you will be testing for object equality and not functional equality.
  • Always remember to override hashCode if you override equals so as not to "break the contract". As per the API, the result returned from the hashCode() method for two objects must be the same if their equals methods show that they are equivalent. The converse is not necessarily true.

遍歷迴圈

遍歷迴圈語句是java5的新特徵之一,在遍歷陣列、集合方面,為開發人員提供了極大的方便。

public void circle() {
    Integer[] array = { 1, 2, 3, 4, 5 };

    for (Integer i : array) {

    System.out.println(i);

    }
}

在編譯後的版本中,程式碼還原成了迭代器的實現,這也是為遍歷迴圈需要被遍歷的類實現Iterable介面的原因。

變長引數

Arrays.asList(1, 2, 3, 4, 5);

條件編譯

條件編譯也是java語言的一種語法糖,根據布林常量值的真假,編譯器將會把分支中不成立的程式碼塊消除掉。

public void ifdef() {if (true) {

System.out.println("true");

} else {//此處有警告--DeadCode

System.out.println("false");

}

}

對列舉和字串的switch支援

public void enumStringSwitch() {

String str = "fans";

switch (str) {

case "fans":

break;case "leiwen":

break;default:

break;

}

}

try-with-resources

在try語句中定義和關閉資源 jdk7提供了try-with-resources,可以自動關閉相關的資源(只要該資源實現了AutoCloseable介面,jdk7為絕大部分資源物件都實現了這個介面)。

staticStringreadFirstLineFromFile(Stringpath)throwsIOException{

try(BufferedReaderbr=newBufferedReader(newFileReader(path))){

returnbr.readLine();
}
}

本文由『後端精進之路』原創,首發於部落格 http://teckee.github.io/ , 轉載請註明出處

搜尋『後端精進之路』關注公眾號,立刻獲取最新文章和價值2000元的BATJ精品面試課程。

相關推薦

JVM效能優化系列-(5) 早期編譯優化

5. 早期編譯優化 早起編譯優化主要指編譯期進行的優化。 java的編譯期可能指的以下三種: 前端編譯器:將.java檔案變成.class檔案,例如Sun的Javac、Eclipse JDT中的增量式編譯器(ECJ) JIT編譯器(Just In Time Compiler):將位元組碼變成機器碼,例如

小師妹學JVM之:深入理解JIT和編譯優化-你看不懂系列

[toc] # 簡介 小師妹已經學完JVM的簡單部分了,接下來要進入的是JVM中比較晦澀難懂的概念,這些概念是那麼的枯燥乏味,甚至還有點惹人討厭,但是要想深入理解JVM,這些概念是必須的,我將會盡量嘗試用簡單的例子來解釋它們,但一定會有人看不懂,沒關係,這個系列本不是給所有人看的。 更多精彩內容且看:

APP效能優化系列:apk體積優化

一.APK檔案格式 /assets /lib /armeabi /armeabi-v7a /x86 /mips /META-INF MANIFEST.MF CERT.RSA CERT.SF /res AndroidManifes

Android效能優化系列之Bitmap圖片優化

在Android開發過程中,Bitmap往往會給開發者帶來一些困擾,因為對Bitmap操作不慎,就容易造成OOM(Java.lang.OutofMemoryError - 記憶體溢位),本篇部落格,我們將一起探討Bitmap的效能優化。 為什麼Bitmap

性能優化系列七:SQL優化

定義 最好的 針對 增加 分享圖片 xxx ebs ora 資源 一、SQL在數據庫中的執行過程 二、執行計劃 1. ACID 原子性:一個事務(transaction)中的所有操作,要麽全部完成,要麽全部不完成,不會結束在中間某個環節。事務在執行過程中發生錯誤,會被

深入理解JVM虛擬機器讀書筆記【第十章】早期(編譯期)優化

10.1 概述 10.2 Javac編譯器 10.2.1 Javac的原始碼與除錯 10.2.2 解析與填充符號表 10.2.3 註解處理器 10.2.4 語義分析與

JVM效能優化, Part 5:Java的伸縮性

感謝朋友【吳傑】投遞本文。 JVM效能優化系列文章由Eva Andearsson在javaworld上發表共計5篇文章,ImportNew上有前4篇譯文。本文(第5篇)由吳傑翻譯自:javaworld 。 很多程式設計師在解決JVM效能問題的時候,花開了很多時間去調優應用程式級別的效能瓶頸,當

Java效能優化系列二(jvm記憶體調優)

首先需要注意的是在對JVM記憶體調優的時候不能只看作業系統級別Java程序所佔用的記憶體,這個數值不能準確的反應堆記憶體的真實佔用情況,因為GC過後這個值是不會變化的,因此記憶體調優的時候要更多地使用JDK提供的記憶體檢視工具,比如JConsole和Java VisualVM(jvisua

JVM效能優化系列-(2) 垃圾收集器與記憶體分配策略

目前已經更新完《Java併發程式設計》和《Docker教程》,歡迎關注【後端精進之路】,輕鬆閱讀全部文章。 Java併發程式設計: Java併發程式設計系列-(1) 併發程式設計基礎 Java併發程式設計系列-(2) 執行緒的併發工具類 Java併發程式設計系列-(3) 原子操作與CAS Java

JVM效能優化系列-(3) 虛擬機器執行子系統

3. 虛擬機器執行子系統 3.1 Java跨平臺的基礎 Java剛誕生的宣傳口號:一次編寫,到處執行(Write Once, Run Anywhere),其中位元組碼是構成平臺無關的基石,也是語言無關性的基礎。 Java虛擬機器不和包括Java在內的任何語言繫結,它只與Class檔案這種特定的二進位制檔案

JVM效能優化系列-(4) 編寫高效Java程式

4. 編寫高效Java程式 4.1 面向物件 構造器引數太多怎麼辦? 正常情況下,如果構造器引數過多,可能會考慮重寫多個不同引數的建構函式,如下面的例子所示: public class FoodNormal { //required private final String foodNa

mysql 開發進階篇系列 5 SQL 優化

其它 目的 block 垂直拆分 例如 info 分析 設計 plain 一. 使用sql提示 sql 提示(sql hint)是優化數據庫的一個重要手段, 是在sql語句中加入一些人為的提示來達到優化操作的目的。   1.1 use index

JVM編譯優化

在部分的商用虛擬機器中,Java 程式最初是通過直譯器(Interpreter )進行解釋執行的,當虛擬機發現某個方法或程式碼塊的執行特別頻繁的時候,就會把這些程式碼認定為“熱點程式碼”。為了提高熱點程式碼的執行效率,在執行時,即時編譯器(Just In Time Compiler )會把這些程式碼

JAVA筆記 —— JVM 效能優化

JVM 引數檢視 java四類八種基本資料型別 第一類:整型 byte short int long 第二類:浮點型 float double 第三類:邏輯型 boolean(它只有兩個值可取true false) 第四類:字元型 char 在棧中可以直接分配記憶體的資料是基本資料型別。 引

提交訂單效能優化系列之002-引入自己編寫的資料庫連線池

資料庫連線池對效能的影響 總是在各種地方看到類似的說明:“資料庫連線池是非常昂貴的資源。” 那麼請問“非常昂貴”到底是有多昂貴呢? 在我的機器上的測試結果是:從資料庫獲取一個連線的平均耗時大

提交訂單效能優化系列之003-測試阿里巴巴的druid資料來源

概括總結 使用druid資料來源之後,相對於第002版自己隨手寫的資料庫,效能反而下降了8.49%。原因在於,002版是“隨手”寫的,因此功能非常簡陋,要什麼沒什麼,只能從記憶體中獲取連線,因此很快。

提交訂單效能優化系列之004-測試hikari資料來源

概括總結 使用hikari資料來源之後,相對於第003版的druid資料來源,從提交訂單這個複雜的操作上來說,效能提升了17.79%。而從獲取資料庫連線這一簡單的操作上來說,hikari比druid優秀幾百倍。 004版本更新說明 pom.xml檔案中

提交訂單效能優化系列之006-普通的Thread多執行緒改為Java8的parallelStream併發流

概括總結 Java8的parallelStream併發流能達到跟多執行緒類似的效果,但它也不是什麼善茬,為了得到跟上一版本的多執行緒類似的效果,一改再改,雖然最後改出來了,但是還是存在理解不了的地方。

【基礎+實戰】JVM原理及優化系列之八:如何檢視JVM引數配置?

1. 檢視JAVA版本資訊 2. 檢視JVM執行模式  在$JAVA_HOME/jre/bin下有client和server兩個目錄,分別代表JVM的兩種執行模式。   client執行模式,針對桌面應用,載入速度比server模式快10%,而執行速度為server模

提交訂單效能優化系列之009-對比整個方法同步與方法中的部分程式碼同步

概括總結 在用到synchronized關鍵字的時候,憑直覺就會加在方法上,比如public static synchronized void test(){},但是這種直覺不見得是對的,估計大部分時候是出圖方便,想偷懶,才直接加到方法上的。推薦的做法是:僅僅