1.深入理解JVM——那还是别人的故事

国庆期间,思考了一个问题:怎样才算深入理解了Java虚拟机?

把周志明的《深入理解Java虚拟机》看5遍算不算透彻理解了JVM?

似乎不算:笔者已经阅读过好几遍,其中部分章节应该超过5遍。但,依然觉得很多技术点如同罩上了一层薄纱。

为什么会这样呢?

如果把《Java语言规范》和《Java虚拟机规范》看作是JDK和JVM的需求说明书,那么《深入理解Java虚拟机》就相当于周志明老师梳理总结的JDK和JVM的规格说明书。

这是一种娓娓道来的知识论述,即JVM的知识本身已经是客观存在,作者会摆出他的MainIdeaSupport Idea,而后通过事实例子事实进行推理,进而证明作者对JVM的规格理解的正确性和逻辑自洽性。

然而,无论马可波罗讲故事的能力有多强,他描述的神秘东方始终是他看到的客观存在。

因此,如果我没有亲自阅读JDK和JVM的源码,那还是别人的故事。

image-20211012225213714

2.阅读OpenJDK源码,从这里开始

下载了OpenJDK 8的源码,您会面对着海量的代码,不知如何入手。

image-20211012230038531

笔者认为,不如想想JVM的体系结构:

image-20211012225535950

JVM体系中,第一个环节是将源代码(.java)变成字节码(.class),也就是说前端编译器(javac)承载了《Java语言规范》和《Java虚拟机规范》中大部分的规格。

至少,可以认为,阅读前端编译器(javac)的源码,可以理解编译期的静态行为。

从javac对应的源码看,也的确如此:

  • comp包中可以看到语义分析、数据流分析、语法糖擦除、泛型擦除等代码

image-20211012230328641

  • jvm包可以看到从AST树生成字节码的代码

image-20211012230413468

  • parser包可以看到词法分析生成TokenQueue的代码

image-20211012230441083

上述三点,基本就覆盖了《Java语言规范》的大部分内容,也覆盖了《Java虚拟机规范》中关于字节码规格的大部分内容。

3.javac的入口解读

3.1.langtools/src/share/classes/com/sun/tools/javac/Main.java

这是javac的入口类,代码结构很简单

类结构

这个类仅有3个方法,main函数调用了compile函数

image-20211012230802827

与周边类的关系

在Main#compile()函数中,又去调用了 com/sun/tools/javac/main/Main.java

image-20211012230924610

3.2.langtools/src/share/classes/com/sun/tools/javac/main/Main.java

这个类承载了javac真正开始干活儿的流程,代码略多,我们从类的属性类的行为与周边类的关系来剖析它。

类的属性

笔者只在此列举几个重要的属性,其它的属性可以到https://github.com/JHerculesqz/jdk8上找到笔者详细标注的注释说明。

recognizedOptions:这是一个Option数组,每个Option元素可以认为是javac后面命令行配置参数。这个数组记录了javac内置的合法的命令行参数。

optionHelper:这是OptionHelper这个抽象类的具体实现类,它的主要职责就是维护用户实际在javac后面输入的命令行参数。

filenames:维护了用户在javac后面输入的待编译的.java源文件。

fileManager:javac内置的一个文件管理模块,负责对待编译的.java源文件进行处理。

image-20211012231318310

类的行为

除了compile方法、processArgs方法,其它都属于打辅助的方法(如:bugMessge方法就是对log对象进行了封装,用于记录错误日志),这些辅助方法笔者就不在此赘述了,依然可以参考https://github.com/JHerculesqz/jdk8上笔者详细标注的注释说明。

image-20211012231906655

我们在此稍微展开一下compile方法的细节:

image-20211012232303828

我们可以将这个方法拆分为3段:

  • 前戏:初始化Main中的各个成员对象。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public Result compile(String[] args,
                      String[] classNames,
                      Context context,
                      List<JavaFileObject> fileObjects,
                      Iterable<? extends Processor> processors)
{
    //HCZ:前戏:在上下文对象中塞out对象,初始化log对象、options对象、fileNames、classnames
    context.put(Log.outKey, out);
    log = Log.instance(context);

    if (options == null)
        options = Options.instance(context); // creates a new one

    filenames = new LinkedHashSet<File>();
    classnames = new ListBuffer<String>();
    JavaCompiler comp = null;
    
    …………
}
  • 命令行参数处理:调用Main#processsArgs方法,处理javac后面的命令行参数

注意:这里有一些高级技术细节,当前不必深入其中,在展开这些高级技术细节时可以再来展开这部分源码。

比如:JVM支持我们自己实现javac的插件,这里插件生命周期的初始化部分就在这段代码中。

比如:javac在关闭nonBatchMode模式后,启用编译缓存,也在这段代码中。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
public Result compile(String[] args,
                      String[] classNames,
                      Context context,
                      List<JavaFileObject> fileObjects,
                      Iterable<? extends Processor> processors)
{
    …………
    /*
         * TODO: Logic below about what is an acceptable command line
         * should be updated to take annotation processing semantics
         * into account.
         */
    try {
        //HCZ:如果各种不合法,返回编译错误,并告知记得敲一下-help
        if (args.length == 0
            && (classNames == null || classNames.length == 0)
            && fileObjects.isEmpty()) {
            Option.HELP.process(optionHelper, "-help");
            return Result.CMDERR;
        }

        Collection<File> files;
        try {
            //HCZ:调用Main#processArgs,获得命令行参数中的.java文件对象列表
            files = processArgs(CommandLine.parse(args), classNames);
            //HCZ:Main#processArgs竟然返回了null?,返回错误信息
            if (files == null) {
                // null signals an error in options, abort
                return Result.CMDERR;
            } else if (files.isEmpty() && fileObjects.isEmpty() && classnames.isEmpty()) {
                // it is allowed to compile nothing if just asking for help or version info
                //HCZ:文件列表是Empty,说明可能是javac --help/-X/-version/-fullversion,则返回正确
                if (options.isSet(HELP)
                    || options.isSet(X)
                    || options.isSet(VERSION)
                    || options.isSet(FULLVERSION))
                    return Result.OK;
                if (JavaCompiler.explicitAnnotationProcessingRequested(options)) {
                    //HCZ:还不太懂?
                    error("err.no.source.files.classes");
                } else {
                    //HCZ:又不是-help这种参数配置,说明用户输入错了,返回错误
                    error("err.no.source.files");
                }
                return Result.CMDERR;
            }
        } catch (java.io.FileNotFoundException e) {
            //HCZ:指定的文件找不到,则返回错误
            warning("err.file.not.found", e.getMessage());
            return Result.SYSERR;
        }

        //HCZ:如果指定了stdout,清一下log对象,重定向到System.out上
        boolean forceStdOut = options.isSet("stdout");
        if (forceStdOut) {
            log.flush();
            log.setWriters(new PrintWriter(System.out, true));
        }

        //HCZ:如果没有设置nonBatchMode,则在上下文对象中创建CacheFSInfo对象,用于多次编译时的缓存。
        // allow System property in following line as a Mustang legacy
        boolean batchMode = (options.isUnset("nonBatchMode")
                             && System.getProperty("nonBatchMode") == null);
        if (batchMode)
            CacheFSInfo.preRegister(context);

        // FIXME: this code will not be invoked if using JavacTask.parse/analyze/generate
        // invoke any available plugins
        //HCZ:如果存在javac的插件,则...
        //啥是javac插件?参考:https://www.baeldung.com/java-build-compiler-plugin
        //?待研究:如下初始化javac插件的逻辑细节。
        String plugins = options.get(PLUGIN);
        if (plugins != null) {
            JavacProcessingEnvironment pEnv = JavacProcessingEnvironment.instance(context);
            ClassLoader cl = pEnv.getProcessorClassLoader();
            ServiceLoader<Plugin> sl = ServiceLoader.load(Plugin.class, cl);
            Set<List<String>> pluginsToCall = new LinkedHashSet<List<String>>();
            for (String plugin: plugins.split("\\x00")) {
                pluginsToCall.add(List.from(plugin.split("\\s+")));
            }
            JavacTask task = null;
            Iterator<Plugin> iter = sl.iterator();
            while (iter.hasNext()) {
                Plugin plugin = iter.next();
                for (List<String> p: pluginsToCall) {
                    if (plugin.getName().equals(p.head)) {
                        pluginsToCall.remove(p);
                        try {
                            if (task == null)
                                task = JavacTask.instance(pEnv);
                            plugin.init(task, p.tail.toArray(new String[p.tail.size()]));
                        } catch (Throwable ex) {
                            if (apiMode)
                                throw new RuntimeException(ex);
                            pluginMessage(ex);
                            return Result.SYSERR;
                        }
                    }
                }
            }
            for (List<String> p: pluginsToCall) {
                log.printLines(PrefixKind.JAVAC, "msg.plugin.not.found", p.head);
            }
        }
        
    …………
}
  • 调用JavaComplier#compile,进行真正的.java到.class的编译

说明:这里也可以跳过xdoclint的代码。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
public Result compile(String[] args,
                      String[] classNames,
                      Context context,
                      List<JavaFileObject> fileObjects,
                      Iterable<? extends Processor> processors)
{
    …………
        //HCZ:初始化JavaCompiler
        comp = JavaCompiler.instance(context);

        // FIXME: this code will not be invoked if using JavacTask.parse/analyze/generate
        //HCZ:xdoclint相关
        //待研究:如下xdoclint的各种初始化细节
        String xdoclint = options.get(XDOCLINT);
        String xdoclintCustom = options.get(XDOCLINT_CUSTOM);
        if (xdoclint != null || xdoclintCustom != null) {
            Set<String> doclintOpts = new LinkedHashSet<String>();
            if (xdoclint != null)
                doclintOpts.add(DocLint.XMSGS_OPTION);
            if (xdoclintCustom != null) {
                for (String s: xdoclintCustom.split("\\s+")) {
                    if (s.isEmpty())
                        continue;
                    doclintOpts.add(s.replace(XDOCLINT_CUSTOM.text, DocLint.XMSGS_CUSTOM_PREFIX));
                }
            }
            if (!(doclintOpts.size() == 1
                  && doclintOpts.iterator().next().equals(DocLint.XMSGS_CUSTOM_PREFIX + "none"))) {
                JavacTask t = BasicJavacTask.instance(context);
                // standard doclet normally generates H1, H2
                doclintOpts.add(DocLint.XIMPLICIT_HEADERS + "2");
                new DocLint().init(t, doclintOpts.toArray(new String[doclintOpts.size()]));
                comp.keepComments = true;
            }
        }

        //HCZ:如果有待编译的java文件列表,则...
        fileManager = context.get(JavaFileManager.class);

        if (!files.isEmpty()) {
            // add filenames to fileObjects
            //HCZ:这里为啥又要初始化一次JavaCompiler?
            comp = JavaCompiler.instance(context);
            //HCZ:待研究,啥是otherFiles?
            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);
        }
        //HCZ:调用JavaCompiler#compile方法,里面有机关,详见那边的标注
        comp.compile(fileObjects,
                     classnames.toList(),
                     processors);

        if (log.expectDiagKeys != null) {
            if (log.expectDiagKeys.isEmpty()) {
                log.printRawLines("all expected diagnostics found");
                return Result.OK;
            } else {
                log.printRawLines("expected diagnostic keys not found: " + log.expectDiagKeys);
                return Result.ERROR;
            }
        }

        if (comp.errorCount() != 0)
            return Result.ERROR;
    } catch (IOException ex) {
    …………
}   
  • 收尾工作:各种异常捕获后的收尾工作
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public Result compile(String[] args,
                      String[] classNames,
                      Context context,
                      List<JavaFileObject> fileObjects,
                      Iterable<? extends Processor> processors)
{
    …………
            } catch (IOException ex) {
        ioMessage(ex);
        return Result.SYSERR;
    } catch (OutOfMemoryError ex) {
        resourceMessage(ex);
        return Result.SYSERR;
    } catch (StackOverflowError ex) {
        resourceMessage(ex);
        return Result.SYSERR;
    } catch (FatalError ex) {
        feMessage(ex);
        return Result.SYSERR;
    } catch (AnnotationProcessingError ex) {
        if (apiMode)
            throw new RuntimeException(ex.getCause());
        apMessage(ex);
        return Result.SYSERR;
    } catch (ClientCodeException ex) {
        // as specified by javax.tools.JavaCompiler#getTask
        // and javax.tools.JavaCompiler.CompilationTask#call
        throw new RuntimeException(ex.getCause());
    } catch (PropagatedException ex) {
        throw ex.getCause();
    } catch (Throwable ex) {
        // Nasty.  If we've already reported an error, compensate
        // for buggy compiler error recovery by swallowing thrown
        // exceptions.
        if (comp == null || comp.errorCount() == 0 ||
            options == null || options.isSet("dev"))
            bugMessage(ex);
        return Result.ABNORMAL;
    } finally {
        if (comp != null) {
            try {
                comp.close();
            } catch (ClientCodeException ex) {
                throw new RuntimeException(ex.getCause());
            }
        }
        filenames = null;
        options = null;
    }
    return Result.OK;
    …………
}   

与周边类的关系

从前文的源码分析可以看到,当前类最终是调用JavaCompiler#compile方法实现的前端编译过程,而JavaCompiler有依赖了Options类读写javac的命令行配置参数。

image-20211012233450136

4.总结

  • 本文表达了笔者的对于深入理解JVM的一种观点:即,通过阅读OpenJDK源码才能达到真正的理解JVM。
  • 本文解析了javac入口部分的源码,为后续进一步理解JVM的前端编译过程奠定了基础。