1. 程式人生 > >【深入Java虛擬機器】之十早期(編譯期)優化

【深入Java虛擬機器】之十早期(編譯期)優化

轉自:https://blog.csdn.net/tjiyu/article/details/53786262

Java編譯(二)Java前端編譯:

Java原始碼編譯成Class檔案的過程

      

      在上篇文章《Java三種編譯方式:前端編譯 JIT編譯 AOT編譯》中瞭解到了它們各有什麼優點和缺點,以及前端編譯+JIT編譯方式的運作過程。

      

下面我們詳細瞭解Java前端編譯:Java原始碼編譯成Class檔案的過程;我們從官方JDK提供的前端編譯器javac入手,用javac編譯一些測試程式,除錯跟蹤javac原始碼,看看javac整個編譯過程是如何實現的。

1、javac編譯器

1-1、javac原始碼與除錯

       javac編譯器是官方JDK中提供的前端編譯器,JDK/bin目錄下的javac只是一個與平臺相關的呼叫入口,具體實現在JDK/lib目錄下的tools.jar。此外,JDK6開始提供在執行時進行前端編譯,預設也是呼叫到javac,如圖:

       javac是由Java語言編寫的,而HotSpot虛擬機器則是由C++語言編寫;標準JDK中並沒有提供javac的原始碼,而在OpenJDK中的提供;我們需要在Eclipse中除錯跟蹤javac原始碼,看整個編譯過程是如何實現的。

       javac編譯器原始碼下載(JDK8):http://hg.openjdk.java.net/jdk8u/jdk8u-dev/langtools/archive/tip.tar.bz2

       javac編譯器原始碼目錄:**\src\share\classes\com\sun\tools\javac

       在Eclipse新建工程匯入後,可以看到javac原始碼的目錄結構如下:

       javac編譯器程式入口:com.sun.tools.javac.Main類中的main()方法;

       執行javac程式,先是解析命令列引數,由com.sun.tools.javac.main.Main.compile()方法處理,程式碼片段如下:

       因為沒有給引數,可看到輸出的是javac用法,如下:

       這就是平時我們用JDK/bin/javac的用法,更多javac選項用法請參考:http://docs.oracle.com/javase/8/docs/technotes/tools/unix/javac.html

       除錯編譯檔案,需要右鍵工程 -> Debug As -> Debug Configurations ->切換到Arguments選項卡,在Program arguments中輸入我們要用javac編譯的Java程式檔案的路徑即可;然後就可以打斷點Debug執行除錯了,如圖:

1-2、javac編譯過程

       JVM規範定義了Class檔案結構格式,但沒有定義如何從java程式檔案轉化為Class檔案,所以不同編譯器可以有不同實現。

       從javac編譯器原始碼來看,其編譯過程可以分為3個子過程:

       1、解析與填充符號表過程:解析主要包括詞法分析和語法分析兩個過程;

       2、插入式註解處理器的註解處理過程;

       3、語義分析與位元組碼的生成過程;

       如圖所示(來自參考4):

       javac編譯動作入口: com.sun.tools.javac.main.JavaCompiler類;

       3個編譯過程邏輯集中在這個類的compile()和compile2()方法;

       如圖所示:

1-3、javac中的訪問者模式

       訪問者模式可以將資料結構和對資料結構的操作解耦,使得增加對資料結構的操作不需要修改資料結構,也不必修改原有的操作,而執行時再定義新的Visitor實現者就行了。

       Javac經過第一步解析(詞法分析和語法分析),會生成用來一棵描述程式程式碼語法結構的抽象語法樹,每個節點都代表程式程式碼中的一個語法結構,包括:包、型別、修飾符、運算子、介面、返回值、甚至註釋等;而後的不同編譯階段都定義了不同的訪問者去處理該語法樹(節點)。

       瞭解這些更容易理解javac的編譯過程實現,而後面分析過程中會再對訪問者模式的實現作相關說明。

2、解析與填充符號表

2-1、解析:詞法、語法分析

      解析包括:詞法分析和語法分析兩個過程;

2-1-1、詞法分析

1、概念解理

      詞法分析是將原始碼的字元流轉變為標記(Token)集合;

      標記:

    標記是編譯過程的最小元素;

    包括關鍵字、變數名、字面量、運算子(甚至一個".")等;

2、原始碼分析:                                

       由com.sun.tools.javac.parser.Scanner類實現對外部提供服務;

       由com.sun.tools.javac.parser.JavaTokenizer類實現具體的Token分析動作(JavaTokenizer.readToken()方法);

       Scanner.nextToken()呼叫JavaTokenizer.readToken()方法讀取下一個Token;    

       返回com.sun.tools.javac.parser.Tokens.Token類例項表示的一個Token;

 

       Scanner.nextToken()方法如下:

          

      注意,下面語法分析時才會不斷呼叫Scanner.nextToken()讀取一個個Token進來解析。

2-1-2、語法分析

1、概念解理

      語法分析是根據Token序列構造抽象語法樹的過程;

      抽象語法樹(Abstract Syntax Tree,AST):

    是一種用來描述程式程式碼語法結構的樹形表示方式;

    每個節點都代表程式程式碼中的一個語法結構;

    語法結構(Construct)包括:包、型別、修飾符、運算子、介面、返回值、甚至註釋等;

2、原始碼分析:

       由com.sun.tools.javac.parser.JavacParser類完成整個過程,該類實現com.sun.tools.javac.parser.Parser介面;

       一個類檔案解析產生的抽象語法樹的所有內容儲存在JCCompilationUnit類例項裡,JCCompilationUnit類是由com.sun.tools.javac.tree.JCTree類擴充套件;

       JCTree是個抽象類,實現了Tree介面,Tree接口裡有一個"<R,D> R accept(TreeVisitor<R,D> visitor, D data)"方法用來接收訪問者,所以Tree介面是訪問者模式中的抽象節點元素

       JCTree類中有一個Visitor內部類,同時也是一個抽象類,作為訪問者模式中的抽象訪問者

       一個JCTree類例項相當於抽象語法樹的一個節點,它會擴充套件許多型別,對應不同語法結構型別的樹節點,如JCStatement,JCClassDecl,JCMethodDecl,JCBlock等等,這些類是訪問者模式中的具體節點元素

       JCTree擴充套件的JCMethodDecl方法型別節點結構如下:

       

       程式碼執行的解析過程,如下:    

1)、由JavaCompiler.compile()方法呼叫JavaCompiler.parseFiles()方法完成引數輸入的所有檔案的編譯;

2)、JavaCompiler.parseFiles()方法中又呼叫本類中的parse()方法對其中一個檔案進行編譯;

       該方法中生成JavacParser類例項,然後呼叫該例項的parseCompilationUnit()方法開始進行整個檔案的解析(包括"package"包名),如下:


        
  1. Parser parser = parserFactory.newParser(content, keepComments(), genEndPos, lineDebugInfo);
  2. tree = parser.parseCompilationUnit();          

       返回的tree是JCCompilationUnit型別例項,儲存了一個類檔案解析產生的抽象語法樹的所有內容,也可以說是抽象語法樹的根節點;                

3)、JavacParser.parseCompilationUnit()方法中呼叫JavacParser.typeDeclaration()進行檔案中所有型別定義的解析;

       JavacParser.typeDeclaration()又呼叫JavacParser.classOrInterfaceOrEnumDeclaration()進行類或介面的解析;

       如果是類又呼叫classDeclaration()對該類進行解析....              

JCTree def = typeDeclaration(mods, docComment);
        

       返回一個JCTree類例項表示檔案中所有型別定義定義的語法樹(不包括"package"包名);                

      這期間會不斷呼叫Scanner.nextToken()讀取一個個Token進來解析;        

3、編譯測試:

      下面我們用javac編譯JavacTest.java檔案來跟蹤整個解析過程,測試檔案程式碼如下:


      
  1. package com.jvmtest;
  2. public class JavacTest {
  3. private int i;
  4. public int getI() {
  5. return i;
  6. }
  7. public void setI(int i) {
  8. this.i = i;
  9. }
  10. }

      對於解析JavacTest.java檔案生成的抽象語法樹,由返回的JCCompilationUnit類例項表示,如下圖所示:


       最外層節點為"com.jvmtest"包名的定義,同時它也是語法樹的根節點;

       再裡一層是"public class JavacTest"類的定義;

       再裡面可以看到一個欄位變數"i"的結構節點,以及兩個方法"getI"和"setI"節點;                

4、類例項建構函式重名為<init>()

       先在再上面的測試程式中加入類例項建構函式:


      
  1. Public JavacTest() {
  2. }

       需要注意的是,在classOrInterfaceBodyDeclaration()解析類時,如果遇到新增的類建構函式,會重名為<init>(),如下:    

      如測試程式中加入類建構函式,可以看到被重新命名<init>(),但在生成的樹結構上名稱還是表現為"JavacTest",如下

  

      經過上面解析,後續所有操作都建立在抽象語法樹之上,下面不會再對原始碼檔案操作;

2-2、填充符號表

1、概念解理

        符號表(Symbol Table)是由一組符號地址和符號資訊構成的表格,可以想象成雜湊表中K-V值的形式;                

        符號表登記的資訊在編譯的不同階段都要用到,如:

            1)、用於語義檢查和產生中間程式碼;

            2)、在目的碼生成階段,符號表是對符號名進行地址分配的依據;

2、原始碼分析:

      根據上一步生成的抽象語法樹列表,由JavaCompiler.enterTrees()方法完成填充符號表;

      由com.sun.tools.javac.comp.Enter類實現填充符號表動作,Enter類繼承JCTree.Visitor內部抽象類,重寫了一些visit**()方法來處理抽象語法樹,作為訪問者模式中的具體訪問者;


       符號由com.sun.tools.javac.code.Symbol抽象類表示, 實現了Element介面,Element接口裡有一個accept()方法用來接收訪問者,所以Element介面是訪問者模式中的抽象節點元素;

      Symbol類擴充套件成多種型別的符號,如ClassSymbol表示類的符號、MethodSymbol表示方法的符號等等,這些類是訪問者模式中的具體節點元素

      Symbol類和MethodSymbol類定義如下:


      
  1. public abstract class Symbol extends AnnoConstruct implements Element {
  2. /** The kind of this symbol.
  3. * @see Kinds
  4. */
  5. public int kind;
  6. /** The flags of this symbol.
  7. */
  8. public long flags_field;
  9. /** An accessor method for the flags of this symbol.
  10. * Flags of class symbols should be accessed through the accessor
  11. * method to make sure that the class symbol is loaded.
  12. */
  13. public long flags() { return flags_field; }
  14. /** The name of this symbol in Utf8 representation.
  15. */
  16. public Name name;
  17. /** The type of this symbol.
  18. */
  19. public Type type;
  20. /** The owner of this symbol.
  21. */
  22. public Symbol owner;
  23. /** The completer of this symbol.
  24. */
  25. public Completer completer;
  26. /** A cache for the type erasure of this symbol.
  27. */
  28. public Type erasure_field;
  29. // <editor-fold defaultstate="collapsed" desc="annotations">
  30. /** The attributes of this symbol are contained in this
  31. * SymbolMetadata. The SymbolMetadata instance is NOT immutable.
  32. */
  33. protected SymbolMetadata metadata;
  34. ......
  35. }


      
  1. /** A class for method symbols.
  2. */
  3. public static class MethodSymbol extends Symbol implements ExecutableElement {
  4. /** The code of the method. */
  5. public Code code = null;
  6. /** The extra (synthetic/mandated) parameters of the method. */
  7. public List<VarSymbol> extraParams = List.nil();
  8. /** The captured local variables in an anonymous class */
  9. public List<VarSymbol> capturedLocals = List.nil();
  10. /** The parameters of the method. */
  11. public List<VarSymbol> params = null;
  12. /** The names of the parameters */
  13. public List<Name> savedParameterNames;
  14. /** For an attribute field accessor, its default value if any.
  15. * The value is null if none appeared in the method
  16. * declaration.
  17. */
  18. public Attribute defaultValue = null;
  19. ......
  20. }

      從上面可以看到它們包含了哪些資訊;

      程式碼執行的填充過程,如下:    

        1)、JavaCompiler.enterTrees()方法呼叫Enter.main()方法;

              根據上一步生成的抽象語法樹列表完成填充符號表,返回填充了類中所有符號的抽象語法樹列表;

        2)、Enter.main()方法呼叫中本類的complete()方法;

               complete()方法先呼叫Enter.classEnter()方法完成填充包符號、類符號以及匯入資訊等;

        3)、接著complete()方法還會不斷呼叫前面生成的每個類的類符號例項的ClassSymbol.complete()方法;

               ClassSymbol.complete()方法會呼叫到MemberEnter.complete(),以完成整個類的填充符號表;

        4、MemberEnter.complete()中會新增類的預設建構函式(如果沒有任何的);

               還會呼叫 MemberEnter.finish()方法完成對類中欄位和方法符號的填充;

               等等(其實先處理註解資訊)...

      注意,EnterTrees()方法最終完成返回一個待處理列表("todo" list),其實該列表還是抽象語法樹列表,符號只是填充到上一步生成的抽象語法樹列表中;可以從上面語法分析給出的JCMethodDecl類中看到有一個MethodSymbol類的成員變數;

3、編譯測試

      還用上面的JavacTest.java檔案測試,其中getI()方法的符號如下(顯示符號名稱):

      測試JavacTest.java檔案填充符號表的前後,抽象語法樹列表變化(紅色)如下:

4、計算方法的特徵簽名

      其實MethodSymbol方法符號中的MethodType型別的type成員就是其特徵簽名;

      在.MemberEnter.visitMethodDef(JCMethodDecl tree)中填充方法符號的時候計算特徵簽名,如下:


      
  1. public void visitMethodDef(JCMethodDecl tree) {
  2. ......
  3. MethodSymbol m = new MethodSymbol( 0, tree.name, null, enclScope.owner);
  4. ......
  5. // Compute the method type
  6. m.type = signature(m, tree.typarams, tree.params,
  7. tree.restype, tree.recvparam,
  8. tree.thrown,
  9. localEnv);
  10. ......
  11. }

      MethodType如下:


      
  1. public static class MethodType extends Type implements ExecutableType {
  2. public List<Type> argtypes;
  3. public Type restype;
  4. public List<Type> thrown;
  5. /** The type annotations on the method receiver.
  6. */
  7. public Type recvtype;
  8. public MethodType(List<Type> argtypes,
  9. Type restype,