1. 程式人生 > >Java-JVM-javac原始碼筆記

Java-JVM-javac原始碼筆記

Java-JVM-javac原始碼筆記

摘要

本文只是簡單記錄下javac的原始碼閱讀筆記

未完待續

0x01 簡介

1.1 解釋執行和編譯執行

可以參考文章Java-JVM-編譯原理
Java程式一般是將.java檔案編譯為.class檔案,然後再執行時由JVM的直譯器(如templateInterpreter_x86_64.cpp,bytecodeInterpreter_x86.cpp等)解釋執行位元組碼檔案。

  • 原生代碼
    指適合當前計算機執行的指令集

解釋執行和編譯執行:

  • 解釋執行
    逐條解釋執行源語言,如phpjavascript
    就是典型的解釋性語言。具體到java,就是用直譯器直接解釋執行基於棧的位元組碼指令集;
  • 編譯執行
    將源語言程式碼編譯為目的碼,執行時不再需要編譯,而是直接在支援目的碼的平臺上執行,效率更高。具體到java,指以方法為基本單位,將位元組碼翻譯為本地機器碼後再執行。

HotSpot的解釋執行和即時編譯:

  • 解釋執行
    將已編譯的位元組碼檔案逐行轉換為原生代碼,並使用直譯器執行之。每次運行同樣程式碼也需要反覆轉換位元組碼到原生代碼過程。
    優點:不用等待
  • 即時編譯(Just In Time Compile, JIT編譯)
    指以方法為基本單位,將位元組碼翻譯為原生代碼後再執行。也就是說說,初次執行時速度慢,以後執行可以直接執行原生代碼,速度快
    優點:總的來說效率更高

HotSpot的兩種執行模式有不同的規定:

  • -server模式:先解釋位元組碼檔案後執行。且有JIT即時編譯器,會將JVM統計的執行熱點程式碼的位元組碼檔案編譯為本地機器程式碼,提升執行效率。當熱點不再時,就會釋放這些原生代碼,待執行時重新解釋執行。
  • -client模式:逐條解釋執行位元組碼檔案。

Java的編譯有三類:

1.1 前端編譯器

  • 簡介
    Javac,此類前端編譯器的優化主要是針對Java編碼過程
  • 功能
    .java轉為.class
  • 主流實現
    Javac

1.2 JIT編譯器(後端編譯器)

  • 簡介
    Just in time
    ,即時編譯器。可以把熱點程式碼直接轉為機器碼,提升效率。同時也是主要優化方向。對於程式執行表現至關重要。
  • 功能
    .class轉為機器碼
  • 主流實現
    HotSpot之C1(Client編譯器,優化手法簡單,編譯時間短。針對啟動效能有要求的客戶端GUI程式),C2(Server編譯器,優化手段複雜,編譯時間較長,執行總體效能更好。針對心梗峰值)。且在JDK1.7後,Server模式JVM採用分層編譯為預設編譯策略,會根據編譯器編譯、優化的規模和耗時,劃分不同的編譯層次:
    1.0層:程式直接解釋執行,
    2.1層,即C1編譯。將位元組碼編譯為本地機器程式碼,

1.3 AOT編譯器

  • 簡介
    Ahead of time,靜態提前編譯器
  • 功能
    .java轉為機器碼
  • 主流實現
    GNU Compiler for the Java(GCJ)

0x02 Javac原始碼

這裡除錯使用的是openjdk8javac程式碼在jdk8/langtools/src/share/classes/com/sun/tools/javac之中。

main方法位於com/sun/tools/javac/Main.java:

public static void main(String[] args) throws Exception {
    System.exit(compile(args));
}

然後是到com/sun/tools/javac/main/Main.java的以下方法:

public Result compile(String[] args) {
    Context context = new Context();
    JavacFileManager.preRegister(context); // can't create it until Log has been set up
    // 關鍵是這一步
    Result result = compile(args, context);
    if (fileManager instanceof JavacFileManager) {
        // A fresh context was created above, so jfm must be a JavacFileManager
        ((JavacFileManager)fileManager).close();
    }
    return result;
}

後序會達到這個Main類的下面這個方法,核心程式碼如下:

 public Result compile(String[] args,
                          String[] classNames,
                          Context context,
                          List<JavaFileObject> fileObjects,
                          Iterable<? extends Processor> processors)
{
     // 檢測到語法不對,就返回代表錯誤的碼Result.CMDERR
    if (args.length == 0
                    && (classNames == null || classNames.length == 0)
                    && fileObjects.isEmpty()) {
                Option.HELP.process(optionHelper, "-help");
                return Result.CMDERR;
            }
    Collection<File> files;    
    // 得到要命令列中要編譯的檔案集合   
    files = processArgs(CommandLine.parse(args), classNames);
    fileManager = context.get(JavaFileManager.class);

    if (!files.isEmpty()) {
        // add filenames to fileObjects
        comp = JavaCompiler.instance(context);
        List<JavaFileObject> otherFiles = List.nil();
        JavacFileManager dfm = (JavacFileManager)fileManager;
        for (JavaFileObject fo : dfm.getJavaFileObjectsFromFiles(files))
            otherFiles = otherFiles.prepend(fo);
        for (JavaFileObject fo : otherFiles)
            fileObjects = fileObjects.prepend(fo);
    }
    JavaCompiler comp = null;
    
    comp = JavaCompiler.instance(context);
    
    comp.compile(fileObjects,
              classnames.toList(),
              processors);
}

接下來,會走入關鍵類com/sun/tools/javac/main/JavaCompiler.java,方法主要看compilecompile2:

public void compile(List<JavaFileObject> sourceFileObjects,
                        List<String> classnames,
                        Iterable<? extends Processor> processors)
{
// 初始化插入式註解處理器
initProcessAnnotations(processors);

            // These method calls must be chained to avoid memory leaks
            delegateCompiler =
                // 2.註解處理執行
                processAnnotations(
                    // 1.1 parseFiles:詞法分析和語法分析
                    // 1.2 輸入到符號表
                    enterTrees(stopIfError(CompileState.PARSE, parseFiles(sourceFileObjects))),
                    classnames);
            // 3.分析及位元組碼class檔案生成
            delegateCompiler.compile2();
}

0x03 Javac編譯過程

主要分為
解析與填充符號表 -> 註解處理 -> 分析與位元組碼生成

3.1 解析與填充符號表

就是前面提到過的parseFiles,解析語法樹的過程使用的是javac自己的一套,可以參考這篇文章JCTree語法樹結點型別

public List<JCCompilationUnit> parseFiles(Iterable<JavaFileObject> fileObjects) {
   if (shouldStop(CompileState.PARSE))
       return List.nil();

    // 語法樹物件
    ListBuffer<JCCompilationUnit> trees = new ListBuffer<>();
    Set<JavaFileObject> filesSoFar = new HashSet<JavaFileObject>();
    
    for (JavaFileObject fileObject : fileObjects) {
        if (!filesSoFar.contains(fileObject)) {
            filesSoFar.add(fileObject);
            trees.append(parse(fileObject));
        }
    }
    return trees.toList();
}

這裡主要會進行詞法和語法樹解析:

  1. 詞法分析,CharStream拆分為tokens
  2. 語法分析,將tokens構建為抽象語法樹(AST)。其每個節點代表一個語法結構,如包、運算子等。

語法分析使用的是com.sun.tools.javac.parser.JavacParser