1.深入理解JVM——那还是别人的故事
国庆期间,思考了一个问题:怎样才算深入理解了Java虚拟机?
把周志明的《深入理解Java虚拟机》看5遍算不算透彻理解了JVM?
似乎不算:笔者已经阅读过好几遍,其中部分章节应该超过5遍。但,依然觉得很多技术点如同罩上了一层薄纱。
为什么会这样呢?
如果把《Java语言规范》和《Java虚拟机规范》看作是JDK和JVM的需求说明书,那么《深入理解Java虚拟机》就相当于周志明老师梳理总结的JDK和JVM的规格说明书。
这是一种娓娓道来的知识论述
,即JVM的知识本身已经是客观存在,作者会摆出他的MainIdea
和Support Idea
,而后通过事实
和例子
事实进行推理
,进而证明作者对JVM的规格理解的正确性和逻辑自洽性。
然而,无论马可波罗讲故事的能力有多强,他描述的神秘东方始终是他看到的客观存在。
因此,如果我没有亲自阅读JDK和JVM的源码,那还是别人的故事。
2.阅读OpenJDK源码,从这里开始
下载了OpenJDK 8的源码,您会面对着海量的代码,不知如何入手。
笔者认为,不如想想JVM的体系结构:
JVM体系中,第一个环节是将源代码(.java)
变成字节码(.class)
,也就是说前端编译器(javac)
承载了《Java语言规范》和《Java虚拟机规范》中大部分的规格。
至少,可以认为,阅读前端编译器(javac)
的源码,可以理解编译期的静态行为。
从javac对应的源码看,也的确如此:
- comp包中可以看到语义分析、数据流分析、语法糖擦除、泛型擦除等代码
- parser包可以看到词法分析生成TokenQueue的代码
上述三点,基本就覆盖了《Java语言规范》的大部分内容,也覆盖了《Java虚拟机规范》中关于字节码规格的大部分内容。
3.javac的入口解读
3.1.langtools/src/share/classes/com/sun/tools/javac/Main.java
这是javac的入口类,代码结构很简单
类结构
这个类仅有3个方法,main函数调用了compile函数
与周边类的关系
在Main#compile()函数中,又去调用了 com/sun/tools/javac/main/Main.java
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源文件进行处理。
类的行为
除了compile方法、processArgs方法,其它都属于打辅助的方法(如:bugMessge方法就是对log对象进行了封装,用于记录错误日志),这些辅助方法笔者就不在此赘述了,依然可以参考https://github.com/JHerculesqz/jdk8
上笔者详细标注的注释说明。
我们在此稍微展开一下compile方法的细节:
我们可以将这个方法拆分为3段:
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的命令行配置参数。
4.总结
- 本文表达了笔者的对于深入理解JVM的一种观点:即,通过阅读OpenJDK源码才能达到真正的理解JVM。
- 本文解析了javac入口部分的源码,为后续进一步理解JVM的前端编译过程奠定了基础。