《深刻理解Java虛擬機》讀書筆記九

第十章 早期(編譯期)優化html

 一、Javac的源碼與調試前端

編譯期的分類:java

  • 前端編譯期:把*.java文件轉換爲*.class文件的過程。例如sun的javac、eclipseJDT中的增量編譯器。
  • JIT編譯期:後端運行期編譯器,把字節碼轉換成機器罵的過程。例如 HotSpot VM的C一、C2編譯器。
  • AOT編譯器:靜態提早編譯器,直接拔Java文件編譯成本地機器代碼的過程,例如GCJ。

Javac的編譯過程:git

  • 解析與填充符號表的過程。
  • 插入式註解處理器的註解過程。
  • 分析與字節碼生成的過程。
  • Javac編譯動做的入口是com.sun.tools.javac.main.JavaCompiler類,上述3個過程的代碼邏輯集中在這個類的compile()和compile2()方法中,其中主體代碼如圖所示,整個編譯最關鍵的處理就由圖中標註的8個方法來完成,下面咱們具體看一下這8個方法實現了什麼功能。

解析與填充符號表的過程:程序員

  • 詞法分析,是將源代碼的字符流轉變爲標記(Token)集合,單個字符是程序編寫過程的最小元素,而標記則是編譯過程的最小元素,關鍵字、變量名、字面量、運算符均可以成爲標記。
  • 語法分析,是根據Token序列構造抽象語法樹的過程,抽象語法樹(Abstract Syntax Tree,AST)是一種用來描述程序代碼語法結構的樹形表示方式,語法樹的每個階段都表明着程序代碼中的一個語法結構(Construct),例如包、類型、修飾符、運算符、接口、返回值甚至代碼註釋等均可以是一個語法結構。
  • 填充符號表,符號表是由一組符號地址和符號信息構成的表格。符號表中所登記的信息在編譯的不一樣階段都要用到。在語義分析中,符號表所登記的內容將用於語義檢查(如檢查一個名字的使用和原先的說明是否一致)和產生中間代碼。在目標代碼生成階段,當對符號名進行地質分配時,符號表是地址分配的依據。填充符號表的過程由com.sun.tools.javac.comp.Enter類實現,此過程的出口是一個待處理列表(To Do List),包含了每個編譯單元的抽象語法樹的頂級節點,以及package-info.java(若是存在的話)的頂級節點

註解處理器:github

  • 在JDK1.6中實現了JSR-269規範,提供了一組插入式註解處理器的標準API在編譯期間對註解進行處理,在這些插件裏面,能夠讀取、修改、添加抽象語法樹中的任意元素。若是這些插件在處理註解期間對語法樹進行了修改,編譯器將回到解析及填充符號表的過程從新處理,直到全部插入式註解處理器都沒有再對語法樹進行修改成止。
  • 插入式註解處理器的初始化過程是在initPorcessAnnotations()方法中完成的,而它的執行過程則是在processAnnotations()方法中完成的。
  • 實現註解處理器的代碼須要繼承抽象類javax.annotation.processing.AbstractProcessor,這個抽象類中只有一個必須覆蓋的abstract方法:「process()」,除了process()方法的傳入參數以外,還有一個很經常使用的實例變量「processingEnv」,它是AbstractProcessor中的一個protected變量,在註解處理器初始化的時候(init()方法執行的時候)建立,繼承了AbstractProcessor的註解處理器代碼能夠直接訪問到它。它表明了註解處理器框架提供的一個上下文環境,要建立新的代碼、向編譯器輸出信息、獲取其餘工具類等都須要用到這個實例變量。註解處理器除了process()方法及其參數以外,還有兩個能夠配合使用的Annotations:@SupportedAnnotationTypes和@SupportedSourceVersion,前者表明了這個註解處理器對哪些註解感興趣,可使用星號「*」做爲通配符表明對全部的註解都感興趣,後者指出這個註解處理器能夠處理哪些版本的Java代碼。每個註解處理器在運行的時候都是單例的,若是不須要改變或生成語法樹的內容,process()方法就能夠返回一個值爲false的布爾值,通知編譯器這個Round中的代碼未發生變化,無須構造新的JavaCompiler實例,在此次實戰的註解處理器中只對程序命名進行檢查,不須要改變語法樹的內容,所以process()方法的返回值都是false。
    package com.ecut.javac;
    
    import java.util.EnumSet;
    import java.util.Set;
    
    import javax.annotation.processing.AbstractProcessor;
    import javax.annotation.processing.Messager;
    import javax.annotation.processing.ProcessingEnvironment;
    import javax.annotation.processing.RoundEnvironment;
    import javax.annotation.processing.SupportedAnnotationTypes;
    import javax.annotation.processing.SupportedSourceVersion;
    import javax.lang.model.SourceVersion;
    import javax.lang.model.element.Element;
    import javax.lang.model.element.ElementKind;
    import javax.lang.model.element.ExecutableElement;
    import javax.lang.model.element.Name;
    import javax.lang.model.element.TypeElement;
    import javax.lang.model.element.VariableElement;
    import javax.lang.model.util.ElementScanner7;
    import javax.tools.Diagnostic.Kind;
    
    //這個註解處理器對那些註解感興趣,使用*表示支持全部的Annotations
    @SupportedAnnotationTypes(value = "*")
    //這個註解處理器能夠處理那些Java版本的代碼,只支持Java1.8的代碼
    @SupportedSourceVersion(value = SourceVersion.RELEASE_8)
    public class NameCheckProcessor extends AbstractProcessor {
    
        private NameCheck nameCheck;
    
        /**
         * 初始化檢查插件
         * 繼承了AbstractProcessor的註解處理器能夠直接訪問繼承了processingEnv,它表明上下文環境,要穿件新的代碼、向編譯器輸出信息、獲取其餘工具類都須要用到這個實例
         *
         * @param processingEnv ProcessingEnvironment
         */
        @Override
        public synchronized void init(ProcessingEnvironment processingEnv) {
            super.init(processingEnv);
            this.nameCheck = new NameCheck(processingEnv);
        }
    
    
        /**
         * 對語法樹的各個節點今夕名稱檢查
         * java編譯器在執行註解處理器代碼時要調用的過程
         *
         * @param annotations
         * @param roundEnv
         * @return
         */
        @Override
        public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
            if (!roundEnv.processingOver()) {
                for (Element element : roundEnv.getRootElements()) {
                    nameCheck.check(element);
                }
            }
            return false;
        }
    
        /**
         * 程序名稱規範的編譯期插件
         * 程序名稱規範的編譯器插件 若是程序命名不合規範,將會輸出一個編譯器的Warning信息
         *
         * @author kevin
         */
        public static class NameCheck {
            Messager messager = null;
            public NameCheckScanner nameCheckScanner;
    
            private NameCheck(ProcessingEnvironment processingEnv) {
                messager = processingEnv.getMessager();
                nameCheckScanner = new NameCheckScanner(processingEnv);
            }
    
            /**
             * 對Java程序明明進行檢查,根據《Java語言規範(第3版)》6.8節的要求,Java程序命名應當符合下列格式:
             * <ul>
             * <li>類或接口:符合駝式命名法,首字母大寫。
             * <li>方法:符合駝式命名法,首字母小寫。
             * <li>字段:
             * <ul>
             * <li>類,實例變量:符合駝式命名法,首字母小寫。
             * <li>常量:要求所有大寫
             * </ul>
             * </ul>
             *
             * @param element
             */
            public void check(Element element) {
                nameCheckScanner.scan(element);
            }
    
            /**
             * 名稱檢查器實現類,繼承了1.6中新提供的ElementScanner6<br>
             * 將會以Visitor模式訪問抽象語法數中得元素
             *
             * @author kevin
             */
            public static class NameCheckScanner extends ElementScanner7<Void, Void> {
                Messager messager = null;
    
                public NameCheckScanner(ProcessingEnvironment processingEnv) {
                    this.messager = processingEnv.getMessager();
                }
    
                /**
                 * 此方法用於檢查Java類
                 */
                @Override
                public Void visitType(TypeElement e, Void p) {
                    scan(e.getTypeParameters(), p);
                    checkCamelCase(e, true);
                    super.visitType(e, p);
                    return null;
                }
    
                /**
                 * 檢查方法命名是否合法
                 */
                @Override
                public Void visitExecutable(ExecutableElement e, Void p) {
                    if (e.getKind() == ElementKind.METHOD) {
                        Name name = e.getSimpleName();
                        if (name.contentEquals(e.getEnclosingElement().getSimpleName())) {
                            messager.printMessage(Kind.WARNING, "一個普通方法:" + name + " 不該當與類名重複,避免與構造函數產生混淆", e);
                            checkCamelCase(e, false);
                        }
                    }
                    super.visitExecutable(e, p);
                    return null;
                }
    
                /**
                 * 檢查變量是否合法
                 */
                @Override
                public Void visitVariable(VariableElement e, Void p) {
                    /* 若是這個Variable是枚舉或常量,則按大寫命名檢查,不然按照駝式命名法規則檢查 */
                    if (e.getKind() == ElementKind.ENUM_CONSTANT || e.getConstantValue() != null || heuristicallyConstant(e)) {
                        checkAllCaps(e);
                    } else {
                        checkCamelCase(e, false);
                    }
                    super.visitVariable(e, p);
                    return null;
                }
    
                /**
                 * 判斷一個變量是不是常量
                 *
                 * @param e
                 * @return
                 */
                private boolean heuristicallyConstant(VariableElement e) {
                    if (e.getEnclosingElement().getKind() == ElementKind.INTERFACE) {
                        return true;
                    } else if (e.getKind() == ElementKind.FIELD && e.getModifiers().containsAll(EnumSet.of(javax.lang.model.element.Modifier.FINAL, javax.lang.model.element.Modifier.STATIC, javax.lang.model.element.Modifier.PUBLIC))) {
                        return true;
                    }
                    return false;
                }
    
                /**
                 * 檢查傳入的Element是否符合駝式命名法,若是不符合,則輸出警告信息
                 *
                 * @param e
                 * @param initialCaps
                 */
                private void checkCamelCase(Element e, boolean initialCaps) {
                    String name = e.getSimpleName().toString();
                    boolean previousUpper = false;
                    boolean conventional = true;
                    int firstCodePoint = name.codePointAt(0);
                    if (Character.isUpperCase(firstCodePoint)) {
                        previousUpper = true;
                        if (!initialCaps) {
                            messager.printMessage(Kind.WARNING, "名稱:" + name + " 應當已小寫字符開頭", e);
                            return;
                        }
                    } else if (Character.isLowerCase(firstCodePoint)) {
                        if (initialCaps) {
                            messager.printMessage(Kind.WARNING, "名稱:" + name + " 應當已大寫字母開否", e);
                            return;
                        }
                    } else {
                        conventional = false;
                    }
                    if (conventional) {
                        int cp = firstCodePoint;
                        for (int i = Character.charCount(cp); i < name.length(); i += Character.charCount(cp)) {
                            cp = name.codePointAt(i);
                            if (Character.isUpperCase(cp)) {
                                if (previousUpper) {
                                    conventional = false;
                                    break;
                                }
                                previousUpper = true;
                            } else {
                                previousUpper = false;
                            }
                        }
                    }
                    if (!conventional) {
                        messager.printMessage(Kind.WARNING, "名稱:" + name + "應當符合駝式命名法(Camel Case Names)", e);
                    }
                }
    
                /**
                 * 大寫命名檢查,要求第一個字符必須是大寫的英文字母,其他部分能夠下劃線或大寫字母
                 *
                 * @param e
                 */
                private void checkAllCaps(VariableElement e) {
                    String name = e.getSimpleName().toString();
                    boolean conventional = true;
                    int firstCodePoint = name.codePointAt(0);
                    if (!Character.isUpperCase(firstCodePoint)) {
                        conventional = false;
                    } else {
                        boolean previousUnderscore = false;
                        int cp = firstCodePoint;
                        for (int i = Character.charCount(cp); i < name.length(); i += Character.charCount(cp)) {
                            cp = name.codePointAt(i);
                            if (cp == (int) '_') {
                                if (previousUnderscore) {
                                    conventional = false;
                                    break;
                                }
                                previousUnderscore = true;
                            } else {
                                previousUnderscore = false;
                                if (!Character.isUpperCase(cp) && !Character.isDigit(cp)) {
                                    conventional = false;
                                    break;
                                }
                            }
    
                        }
                    }
                    if (!conventional) {
                        messager.printMessage(Kind.WARNING, "常量:" + name + " 應該所有以大寫字母" + "或下劃線命名,而且以字符開否", e);
                    }
                }
            }
        }
    
    }

    測試類:後端

    package com.ecut.javac;
    
    public class BADLY_NAME_CODE {
        enum colors {
            red, blue, green;
        }
    
        static final int _FORTy_TWO = 42;
        public static int NOT_A_CONSTANT = _FORTy_TWO;
    
        protected void Badly_Named_Code() {
            return;
        }
    
        public void NOTcameCASEmethodNAME() {
            return;
        }
    }

    運行結果以下圖:數組

語義分析與字節碼生成:框架

  • 語法分析以後,編譯器得到了程序代碼的抽象語法樹表示,語法樹能表示一個結構正確的源程序的抽象,但沒法保證源程序是符合邏輯的。而語義分析的主要任務是對結構上正確的源程序進行上下文有關性質的審查,如進行類型審查。
  • 語義分析過程分爲標註檢查以及數據及控制流分析兩個步驟。字節碼生成以前還須要解語法糖。
  • 標註檢查,步驟檢查的內容包括諸如變量使用前是否已被聲明、變量與賦值之間的數據類型是否可以匹配等。標註檢查步驟在Javac源碼中的實現類是com.sun.tools.javac.comp.Attr和com.sun.tools.javac.comp.Check類。
  • 數據及控制流分析,是對程序上下文邏輯更進一步的驗證,它能夠檢查出諸如程序局部變量在使用前是否有賦值、方法的每條路徑是否都有返回值、是否全部的受查異常都被正確處理了等問題。
  • 解語法糖, 語法糖(System Sugar),也稱糖衣語法,指在計算機語言中添加的某種語法,這種語法對語言的功能並無影響,可是更方便程序員使用。一般來講,使用語法糖可以增長程序的可讀性,從而減小程序代碼出錯的機會。java中經常使用的語法糖主要是泛型、變長參數、自動裝箱/拆箱等,虛擬機運行時不支持這些語法,他們在編譯階段還原回簡單的基礎語法結構,這個過程稱爲解語法糖。
  • 字節碼生成,是javac編譯過程的最後一個階段,這個階段不只把前面各個步驟所生成的信息(語法樹、符號表)轉化成字節碼寫到磁盤裏,編譯器還進行了少許的代碼添加和轉換工做。 好比:實力構造器()方法和類構造器()方法就是在這個階段添加到語法樹中(並非默認構造函數,如何用戶代碼中沒有任何構造函數,添加默認構造函數是在填充符號表階段完成)

二、Java語法糖的味道eclipse

泛型與類型檫除:

  • Java中的泛型只在程序源碼中存在,在編譯後的字節碼文件中就已經替換爲原來的原生類型(裸類型),而且在相應的地方插入強制轉型代碼。
  • 所以對於運行期的java語言來講,ArrayList<Integer>與ArrayList<String>就是同一個類,因此泛型技術就是一顆語法糖,java語言中的泛型實現方法稱爲類型檫除,基於這種方法實現的泛型稱爲僞泛型。

    泛型擦除前:

    package com.ecut.javac;
    
    import java.util.HashMap;
    import java.util.Map;
    
    public class GenericTest {
    
        public static void main(String[] args) {
            Map< String , String >  map = new HashMap<>();
    
            map.put("How are you ?","吃了嗎?");
    
            map.put("Hi","您好!");
    
            System.out.println(map.get("Hi"));
        }
    }

    編譯後,泛型擦除後:

    package com.ecut.javac;
    
    import java.io.PrintStream;
    import java.util.HashMap;
    import java.util.Map;
    
    public class GenericTest
    {
      public static void main(String[] args)
      {
        Map map = new HashMap();
    
        map.put("How are you ?", "吃了嗎?");
    
        map.put("Hi", "您好!");
    
        System.out.println((String)map.get("Hi"));
      }
    }

    泛型重載:

    package com.ecut.javac;
    
    import java.util.List;
    
    public class GenericTypes {
    
       //報錯信息:「method(list<string>)」與「method(list<integer>)」衝突;兩種方法具備相同的擦除功能
        public static void method(List<String> list ){
            System.out.println("invoke method(List<String> list ");
        }
    
        public static void method(List<Integer> list ){
            System.out.println("invoke method(List<Integer> list ");
        }
    }
  • Signature屬性存儲一個方法字節碼層面的特徵簽名(區別java層面特徵簽名,還包含返回值和受查異常表),這個屬性中保存的參數類型並非原生的類型,而是參數化的類型信息。
  • LocalVariableTable屬性用來描述棧幀中局部變量與Java源碼中定義的變量之間的關係。
  • Class文件的Signature參數檫除類型只是檫除Code屬性中的字節碼,實際上元數據還保留了泛型信息。

自動裝箱、拆箱與遍歷循環:

  • 自動裝箱、拆箱與遍歷循環解語法糖測試
    package com.ecut.javac;
    
    import java.util.Arrays;
    import java.util.List;
    import java.util.stream.Collectors;
    import java.util.stream.Stream;
    
    public class ForeachTest {
        public static void main(String[] args) {
            List<Integer> list = Arrays.asList(1, 2, 3, 4);
            // JDK1.8
            List<Integer> list2 = Stream.of(1, 2, 3, 4).collect(Collectors.toList());
            // JDK1.9
            //List<Integer> list3 = Lists.newArrayList(1, 2, 3, 4);
            int sum = 0;
            for (int i : list) {
                sum += i;
            }
            System.out.println(sum);
        }
    }

    編譯後:

    package com.ecut.javac;
    
    import java.io.PrintStream;
    import java.util.Arrays;
    import java.util.Iterator;
    import java.util.List;
    import java.util.stream.Collectors;
    import java.util.stream.Stream;
    
    public class ForeachTest
    {
      public static void main(String[] args)
      {
        List list = Arrays.asList(new Integer[] { Integer.valueOf(1), Integer.valueOf(2), Integer.valueOf(3), Integer.valueOf(4) });
    
        List list2 = (List)Stream.of(new Integer[] { Integer.valueOf(1), Integer.valueOf(2), Integer.valueOf(3), Integer.valueOf(4) }).collect(Collectors.toList());
    
        int sum = 0;
        for (Iterator localIterator = list.iterator(); localIterator.hasNext(); ) { int i = ((Integer)localIterator.next()).intValue();
          sum += i;
        }
        System.out.println(sum);
      }
    }

    泛型在編譯過程當中會進行擦除,將泛型參數去除;自動裝箱、拆箱在變以後被轉化成了對應的包裝盒還原方法,如Integer.valueOf()與Integer.intValue()方法;而遍歷循環則被還原成了迭代器的實現,這也是爲何遍歷器循環須要被遍歷的類實現Iterator接口的緣由。變長參數(asList()),它在調用的時候變成了一個數組類型的參數,在變長參數出來以前,程序員使用數組來完成相似功能。

  • 自動裝箱的陷阱
    package com.ecut.javac;
    
    /**
     * 基本數據類型和引用類型的區別主要在於基本數據類型是分配在棧上的,而引用類型是分配在堆上的
     * 不管是基本數據類型仍是引用類型,他們都會先在棧中分配一塊內存,對於基本類型來講,這塊區域包含的是基本類型的內容;
     * 而對於引用類型來講,這塊區域包含的是指向真正內容的指針,真正的內容被手動的分配在堆上。
     */
    public class AutoBox {
        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;
            Integer h = new Integer(3);
            Integer i = new Integer(3);
            /*
            包裝類遇到「==」號的狀況下,若是不遇到算數運算符(+、-、*、……)是不會自動拆箱的.因此這裏「==」比較的是對象(地址)
             */
            //true 對於Integer 類型,整型的包裝類系統會自動在常量池中初始化-128至127的值,若是c和d都指向同一個對象,即同一個地址。
            System.out.println("c==d:" + (c == d));
            //false 可是對於超出範圍外的值就是要經過new來建立包裝類型,因此內存地址也不相等
            System.out.println("e==f:" + (e == f));
            //true 由於遇到運算符自動拆箱變爲數值比較,因此相等。
            System.out.println("c==(a+b):" + (c == (a + b)));
            //true 包裝類都重寫了equals()方法,他們進行比較時是比的拆箱後數值。可是並不會進行類型轉換
            System.out.println("c.equals(a+b)" + (c.equals(a + b)));
            //true ==遇到算數運算符會自動拆箱(long) 3==(int)3
            System.out.println("g==(a+b)" + (g == (a + b)));
            //false equals首先看比較的類型是否是同一個類型,若是是,則比較值是否相等,不然直接返回false
            System.out.println("g.equals(a+b):" + g.equals(a + b));
            //true equals首先看比較的類型是否是同一個類型,若是是,則比較值是否相等,不然直接返回false
            System.out.println("h.equals(i):" + h.equals(i));
            //false 經過new來建立包裝類型,因此內存地址也不相等
            System.out.println("h == i:" + (h == i));
        }
    }

條件編譯:

  •  java語言之中並無使用預處理器,由於Java語言自然的編譯方式(編譯器並不是一個個地編譯Java文件,而是將全部編譯單元的語法樹頂級節點輸入到待處理列表後再進行編譯,所以各個文件之間可以互相提供符號信息)無須使用預處理器。
  • Java語言可使用條件爲常量的if語句進行條件編譯,根據常量的真假來將分支中不成立的代碼塊消除掉。
  • 只能使用if,若使用常量甚至於其餘帶有條件判斷能力的語句描述搭配,則可能在控制流分析中提示錯誤,拒絕編譯Uncreachable Code。
    package com.ecut.javac;
    
    public class IfTest {
        public static void main(String[] args) {
            if(true){
                System.out.println("true");
            }else{
                System.out.println("false");
            }
        }
    }

    編譯後:

    package com.ecut.javac;
    
    import java.io.PrintStream;
    
    public class IfTest
    {
      public static void main(String[] args)
      {
        System.out.println("true");
      }
    }

源碼地址:

https://github.com/SaberZheng/jvm-test/tree/master/src/com/ecut/javac

轉載請於明顯處標明出處:

http://www.javashuo.com/article/p-ydzalcge-a.html

相關文章
相關標籤/搜索