JVM之字节码的编译原理
Java最初诞生的目的就是为了在不依赖特定的物理硬件和操作系统环境下运行,那么也就是说Java程序实现跨平台我的基石其实就是字节码。
Java之所以能够解决程序的安全性问题、跨平台移植性等问题,最主要的原因就是Java源代码的编译结果并非是本地机器指令,而是字节码。当Java源代码成功编译成字节码后,如果想在不同的平台上面运行,则无需再次编译,也就是说Java源码只需一次编译就可处处运行,这就是**“Write Once,Run Anywhere”**的思想。
简单来说,字节码就相当于是一份通用的契约,尽管不同平台上的Java虚拟机的实现细节不尽相同,但是它们共同执行的字节码内容却是一样的。
javac编译器简介
JIT编译
Java程序最初是仅仅通过解释器解释执行的,即对字节码逐条解释执行,这种方式的执行速度相对会比较慢,尤其当某个方法或代码块运行的特别频繁时,这种方式的执行效率就显得很低。于是后来在虚拟机中引入了JIT编译器(即时编译器),当虚拟机发现某个方法或代码块运行特别频繁时,就会把这些代码认定为“Hot Spot Code”(热点代码),为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各层次的优化,完成这项任务的正是JIT编译器。
Java源码的编译结果是字节码,那么肯定需要有一种编译器能够将Java源码编译为字节码,承担这个重任的就是配置在**“PATH”**环境变量中的javac编译器,javac是一种能够将Java源码编译为字节码的前端编译器,其他的前端编译器还有诸如Eclipse JDT中的增量式编译器ECJ等。相对应的还有后端编译器,它在程序运行期间将字节码转变成机器码(现在的Java程序在运行时基本都是解释执行加编译执行),如HotSpot虚拟机自带的JIT(Just In Time Compiler)编译器(分Client端和Server端)
Javac与Eclipse Compiler for Java编译器
Hotspot VM
并没有强制要求前端编译器只能使用 javac来编译字节码,其实只要编译结果符合JVM规范都可以被JVM所识别,所以在Java的前端编译器领域,除了 Javac之外, 还有一种被大家经常用到的前端编译器,那就是内置在 Eclipse
中的ECJ
( Eclipse Compiler for Java)编译器。相信有不少开发人员会误以为 Eclipse中同样也是使用JDK或者 OpenJDK
中的 Javac编译器来编译字节码,其实不是这样的, Eclipse中所使用的ECJ前端编译器完全 是自主研发的,并且可以和 Javac相媲美,甚至可以比 Javac更优秀。
和 Javac的全量式编译不同,ECJ是一种增量式编译器。在 Eclipse中,当开发人员编写 完代码后,使用“Ctrl+S”快捷键时,ECJ编译器所采取的编译方案是把未编译部分的源码 逐行进行编译,而非每次都全量编译。因此ECJ的编译效率会比 Javac更加迅速和高效,当 然编译质量和 javac相比其实大致还是一样的。这里大家需要注意,前端编译器并不会直接涉及编译优化等方面的技术,而是将这些具体优化细节移交给 Hotspot I的JIT编译器负责
编译原理
Javac编译器在 将Java源码编译为一个有效的字节码文件,主要会经历4个步骤,分别是:词法解析→语法解析→语义解析→生成字节码
JVM并不会与Java语言“终生绑定”,任何语言编写的程序 都可以运行在JVM中,前提是源码的编译结果满足并包含Java虚拟机的内部指令集、符号表 以及其他的辅助信息,它就是一个有效的字节码文件,就能够被虚拟机所识别并装载运行。
当弄清楚这些基本概念之后,接下来我们再来了解 Javac的编译原理,其实所谓编译原 理,大家可以理解为Java源码编译为字节码时所需要经历的一些编译步骤。 Javac编译器在 将Java源码编译为一个有效的字节码文件,主要会经历4个步骤,分别是:词法解析→语法解析→语义解析→生成字节码,如图所示
image-20210509125953581.png
第一步:词法解析
什么是词法解析?
编译的第一步是词法解析,那么究竟什么是词法解析呢?在Java语言中,关键字相信 大家都非常熟悉,所谓关键字指的就是 Java API内部预定义的一些字符集合(如 public、 private、 class 等)。那么词法解析要做的事情就是将Java源码中的关键字和标示符等内容转换为符合Java语法规范的 Token序列,然后按照指定的顺序规则进行匹配校验,这就是词法解析步骤。
词法分析是将源代码的字符流转变为标记(Token)集合。单个字符是程序编写过程中的的最小元素,而标记则是编译过程的最小元素,关键字、变量名、字面量、运算符等都可以成为标记,比如整型标志int由三个字符构成,但是它只是一个标记,不可拆分。
1、词法解析步骤
在javac编译器中,词法解析器接口是com.sun.tools.javac.parser.Lexer,它的直接派生实现是位于同包下的Scanner类,该类的对象实例由ScannerFactory工厂负责创建。Scanner类的主要任务就是按照单个字符的方式读取Java源文件中的关键字和标示符等内容,然后将其转换为符合Java语法规范的Token序列。在这里大家要注意,负责词法解析工作的并不是Scanner类,而是com.sun.tools.javac.parser.JavacParser类,该类的对象实例由ParserFactory工厂负责创建,也就是说,由JavacParser类负责控制词法解析时的具体细节,而Scanner类仅仅只是负责读取源码中的字符集合以及与Token序列之间的转换任务,如图2-3所示
首先由 compile方法调用com.sun. tools. javac. main. javacompiler类的 parseFiles()方法, 该方法的主要任务就是调用 parse()方法获取 Javacparser类的对象实例,然后调用 Javacparser 类的 parseCompilationUnit()方法执行词法解析,如下所示:
2、Token序列
词法解析器在成功读取到Java源码中的关键字和标示符等内容后,会将其转换为符合Java语法规范和Token序列,那么究竟什么是Token呢?实际上Token其实就是一个枚举类型,内部定义了许多符合Java语法规范并与源码字符相对应的枚举常量。你可以在com.sun.tools.javac.parser.Token类中找到所有枚举常量,如下所示
EOF(),
ERROR(),
IDENTIFIER(Tag.NAMED),
ABSTRACT("abstract"),
ASSERT("assert", Tag.NAMED),
BOOLEAN("boolean", Tag.NAMED),
BREAK("break"),
BYTE("byte", Tag.NAMED),
CASE("case"),
CATCH("catch"),
CHAR("char", Tag.NAMED),
CLASS("class"),
CONST("const"),
CONTINUE("continue"),
DEFAULT("default"),
DO("do"),
DOUBLE("double", Tag.NAMED),
ELSE("else"),
ENUM("enum", Tag.NAMED),
EXTENDS("extends"),
FINAL("final"),
FINALLY("finally"),
FLOAT("float", Tag.NAMED),
FOR("for"),
GOTO("goto"),
IF("if"),
IMPLEMENTS("implements"),
IMPORT("import"),
INSTANCEOF("instanceof"),
INT("int", Tag.NAMED),
INTERFACE("interface"),
LONG("long", Tag.NAMED),
NATIVE("native"),
NEW("new"),
PACKAGE("package"),
PRIVATE("private"),
PROTECTED("protected"),
PUBLIC("public"),
RETURN("return"),
SHORT("short", Tag.NAMED),
STATIC("static"),
STRICTFP("strictfp"),
SUPER("super", Tag.NAMED),
SWITCH("switch"),
SYNCHRONIZED("synchronized"),
THIS("this", Tag.NAMED),
THROW("throw"),
THROWS("throws"),
TRANSIENT("transient"),
TRY("try"),
VOID("void", Tag.NAMED),
VOLATILE("volatile"),
WHILE("while"),
INTLITERAL(Tag.NUMERIC),
LONGLITERAL(Tag.NUMERIC),
FLOATLITERAL(Tag.NUMERIC),
DOUBLELITERAL(Tag.NUMERIC),
CHARLITERAL(Tag.NUMERIC),
STRINGLITERAL(Tag.STRING),
TRUE("true", Tag.NAMED),
FALSE("false", Tag.NAMED),
NULL("null", Tag.NAMED),
UNDERSCORE("_", Tag.NAMED),
ARROW("->"),
COLCOL("::"),
LPAREN("("),
RPAREN(")"),
LBRACE("{"),
RBRACE("}"),
LBRACKET("["),
RBRACKET("]"),
SEMI(";"),
COMMA(","),
DOT("."),
ELLIPSIS("..."),
EQ("="),
GT(">"),
LT("<"),
BANG("!"),
TILDE("~"),
QUES("?"),
COLON(":"),
EQEQ("=="),
LTEQ("<="),
GTEQ(">="),
BANGEQ("!="),
AMPAMP("&&"),
BARBAR("||"),
PLUSPLUS("++"),
SUBSUB("--"),
PLUS("+"),
SUB("-"),
STAR("*"),
SLASH("/"),
AMP("&"),
BAR("|"),
CARET("^"),
PERCENT("%"),
LTLT("<<"),
GTGT(">>"),
GTGTGT(">>>"),
PLUSEQ("+="),
SUBEQ("-="),
STAREQ("*="),
SLASHEQ("/="),
AMPEQ("&="),
BAREQ("|="),
CARETEQ("^="),
PERCENTEQ("%="),
LTLTEQ("<<="),
GTGTEQ(">>="),
GTGTGTEQ(">>>="),
MONKEYS_AT("@"),
CUSTOM;
3、源码字符集合与Token对应关系
词法解析器在将源码字符集合转换为Token之前,会先将每一个字符集合都转换为一个对应的Name对象,每一个源码字符命令就是一个Name对象,然后再由com.sun.tools.javac.parser.Keywords类负责实际的Token转换工作,对应关系奖会存在Keywords类的数组key中
// 通过 Names 调用 Name 的 fromChars() 方法获取出 Name对象
name = names.fromChars(sbuf, 0, sp);
// 根据 Name 对象获取出与之对应的 Token
token = keywords.key(name);
调用key()
方法获取指定Token
public Token key(Name name) {
return (name.getIndex() > maxKey) ? IDENTIFIER : key[name.getIndex()];
}
4、调用parseCompilationUnit
执行词法解析
public JCTree.JCCompilationUnit parseCompilationUnit() {
Token firstToken = token;
JCModifiers mods = null;
boolean consumedToplevelDoc = false;
boolean seenImport = false;
boolean seenPackage = false;
ListBuffer<JCTree> defs = new ListBuffer<>();
if (token.kind == MONKEYS_AT)
mods = modifiersOpt();
if (token.kind == PACKAGE) {
int packagePos = token.pos;
List<JCAnnotation> annotations = List.nil();
seenPackage = true;
if (mods != null) {
checkNoMods(mods.flags);
annotations = mods.annotations;
mods = null;
}
nextToken();
JCExpression pid = qualident(false);
accept(SEMI);
JCPackageDecl pd = toP(F.at(packagePos).PackageDecl(annotations, pid));
attach(pd, firstToken.comment(CommentStyle.JAVADOC));
consumedToplevelDoc = true;
defs.append(pd);
}
boolean checkForImports = true;
boolean firstTypeDecl = true;
while (token.kind != EOF) {
if (token.pos <= endPosTable.errorEndPos) {
// error recovery
skip(checkForImports, false, false, false);
if (token.kind == EOF)
break;
}
if (checkForImports && mods == null && token.kind == IMPORT) {
seenImport = true;
defs.append(importDeclaration());
} else {
Comment docComment = token.comment(CommentStyle.JAVADOC);
if (firstTypeDecl && !seenImport && !seenPackage) {
docComment = firstToken.comment(CommentStyle.JAVADOC);
consumedToplevelDoc = true;
}
if (mods != null || token.kind != SEMI)
mods = modifiersOpt(mods);
if (firstTypeDecl && token.kind == IDENTIFIER) {
ModuleKind kind = ModuleKind.STRONG;
if (token.name() == names.open) {
kind = ModuleKind.OPEN;
nextToken();
}
if (token.kind == IDENTIFIER && token.name() == names.module) {
if (mods != null) {
checkNoMods(mods.flags & ~Flags.DEPRECATED);
}
defs.append(moduleDecl(mods, kind, docComment));
consumedToplevelDoc = true;
break;
} else if (kind != ModuleKind.STRONG) {
reportSyntaxError(token.pos, Errors.ExpectedModule);
}
}
JCTree def = typeDeclaration(mods, docComment);
if (def instanceof JCExpressionStatement)
def = ((JCExpressionStatement)def).expr;
defs.append(def);
if (def instanceof JCClassDecl)
checkForImports = false;
mods = null;
firstTypeDecl = false;
}
}
JCTree.JCCompilationUnit toplevel = F.at(firstToken.pos).TopLevel(defs.toList());
if (!consumedToplevelDoc)
attach(toplevel, firstToken.comment(CommentStyle.JAVADOC));
if (defs.isEmpty())
storeEnd(toplevel, S.prevToken().endPos);
if (keepDocComments)
toplevel.docComments = docComments;
if (keepLineMap)
toplevel.lineMap = S.getLineMap();
this.endPosTable.setParser(null); // remove reference to parser
toplevel.endPositions = this.endPosTable;
return toplevel;
}
第二步:语法解析
当词法解析结束后,javac就会进入到编译的第二个阶段,也就是语法解析。所谓语法解析指的就是将词法解析后的Token序列整合为一棵结构化的抽象语法树,因为我们知道,一个try语句后面肯定会接上一个catch或者finally子句,这就是语法解析步骤。
语法分析是根据Token序列来构造抽象语法树的过程。抽象语法树是一种用来描述程序代码语法结构的树形表示方式,语法树的每一个节点都代表着程序代码中的一个语法结构,如bao、类型、修饰符、运算符等。经过这个步骤后,编译器就基本不会再对源码文件进行操作了,后续的操作都建立在抽象语法树之上。
1、调用qualident()方法解析package语法节点
当词法解析器成功地将package关键字声明转换为Token并完成词法解析后,就会调用qualident()方法根据Token.PACKAGE解析为package语法节点
public JCExpression qualident(boolean allowAnnos) {
// 解析为JCIdent语法节点
JCExpression t = toP(F.at(token.pos).Ident(ident()));
while (token.kind == DOT) {
int pos = token.pos;
nextToken();
List<JCAnnotation> tyannos = null;
if (allowAnnos) {
tyannos = typeAnnotationsOpt();
}
// 解析为JCFieldAccess语法节点
t = toP(F.at(pos).Select(t, ident()));
if (tyannos != null && tyannos.nonEmpty()) {
t = toP(F.at(tyannos.head.pos).AnnotatedType(tyannos, t));
}
}
return t;
}
Ident()方法如下
public JCIdent Ident(Name name) {
// 根据Name对象解析出一个JCIdent语法节点
JCIdent tree = new JCIdent(name, null);
tree.pos = pos;
return tree;
}
Select()方法如下
public JCFieldAccess Select(JCExpression selected, Name selector) {
// 根据Name对象解析出嵌套JCFieldAccess语法节点
JCFieldAccess tree = new JCFieldAccess(selected, selector, null);
tree.pos = pos;
return tree;
}
观察 Ident() 与 **Select()**方法你会发现它们的形参都是Name,所以在解析语法树或者语法节点时,首先需要将Token
转换为对应的Name对象。在解析JCIdent语法节点的Ident()方法参数中调用了ident()方法,该方法会返回一个与Token对应的Name对象,如下
protected Name ident(boolean advanceOnErrors) {
if (token.kind == IDENTIFIER) {
Name name = token.name();
nextToken();
return name;
} else if (token.kind == ASSERT) {
log.error(DiagnosticFlag.SYNTAX, token.pos, Errors.AssertAsIdentifier);
nextToken();
return names.error;
} else if (token.kind == ENUM) {
log.error(DiagnosticFlag.SYNTAX, token.pos, Errors.EnumAsIdentifier);
nextToken();
return names.error;
} else if (token.kind == THIS) {
if (allowThisIdent) {
// Make sure we're using a supported source version.
checkSourceLevel(Feature.TYPE_ANNOTATIONS);
Name name = token.name();
nextToken();
return name;
} else {
log.error(DiagnosticFlag.SYNTAX, token.pos, Errors.ThisAsIdentifier);
nextToken();
return names.error;
}
} else if (token.kind == UNDERSCORE) {
if (Feature.UNDERSCORE_IDENTIFIER.allowedInSource(source)) {
log.warning(token.pos, Warnings.UnderscoreAsIdentifier);
} else {
log.error(DiagnosticFlag.SYNTAX, token.pos, Errors.UnderscoreAsIdentifier);
}
Name name = token.name();
nextToken();
return name;
} else {
accept(IDENTIFIER);
if (advanceOnErrors) {
nextToken();
}
return names.error;
}
}
2、调用importDeclaration()方法解析import语法树
当成功解析package语法节点后,语法解析的下一步就是解析import语法树。编译器在执行词法解析过程中,会匹配当前Token是否是Token.IMPORT,如果Token匹配成功,parseCompilationUnit()方法就会调用importDeclaration()方法根据Token.IMPORT解析为import语法树。源码如下
protected JCTree importDeclaration() {
int pos = token.pos;
nextToken();
boolean importStatic = false;
// 匹配Token.STATIC
if (token.kind == STATIC) {
importStatic = true;
nextToken();
}
// 根据Name 对象解析出一个JCIdent语法节点
JCExpression pid = toP(F.at(token.pos).Ident(ident()));
do {
int pos1 = token.pos;
accept(DOT);
if (token.kind == STAR) {
pid = to(F.at(pos1).Select(pid, names.asterisk));
nextToken();
break;
} else {
pid = toP(F.at(pos1).Select(pid, ident()));
}
} while (token.kind == DOT);
accept(SEMI);
// 将JCIdent和JCFieldAccess语法节点整合为一个JCImport语法树
return toP(F.at(pos).Import(pid, importStatic));
}
上述代码示例中,首先会匹配 Token. STATIC,用于检测 Impor关键字声明中是否包含 static静态导入。接下来 importDeclaration()方法便会调用语法解析器的Ident()方法解析出一 个 JCIdent语法节点,如果 Import关键字声明中定义多级目录时,则会调用语法解析器的 Select()方法,将其解析为嵌套的 JCFieldAccess 语法节点,这和之前解析 package语法节点 是一样的。 当语法解析器成功解析 JCIdent和 JCFieldAccess i语法节点后, importDeclaration()方法 就会调用语法解析器的 Import()方法,将之前解析过的语法节点整合为一棵 JCImport语法 树,如下所示:
public JCImport Import(JCTree qualid, boolean importStatic) {
// 解析 JCImport语法树
JCImport tree = new JCImport(qualid, importStatic);
tree.pos = pos;
return tree;
}
实际开发过程中,可能会有多个import关键字声明,那么parseCompilationUnit()方法内部则会通过循环迭代的方式解析JCImport语法树,然后将其存储在一个集合中。
3、调用classDeclaration()方法解析class语法树
在import解析并合并为JCImport语法树后,在parseCompilationUnit()方法内部就会通过typeDeclatation()方法调用classOrInterfaceOrEnumDeclaration()方法将class主体信息解析为一棵JCClassDecl语法树。如下所示:
protected JCStatement classOrInterfaceOrEnumDeclaration(JCModifiers mods, Comment dc) {
if (token.kind == CLASS) {
// 将class类型解析为一棵JCClassDecl 语法树
return classDeclaration(mods, dc);
} else if (token.kind == INTERFACE) {
// 将interface类型解析为一棵JCClassDecl 语法树
return interfaceDeclaration(mods, dc);
} else if (token.kind == ENUM) {
// 将枚举类型解析为一棵JCClassDecl语法树
return enumDeclaration(mods, dc);
} else {
int pos = token.pos;
List<JCTree> errs;
if (LAX_IDENTIFIER.accepts(token.kind)) {
errs = List.of(mods, toP(F.at(pos).Ident(ident())));
setErrorEndPos(token.pos);
} else {
errs = List.of(mods);
}
final JCErroneous erroneousTree;
if (parseModuleInfo) {
erroneousTree = syntaxError(pos, errs, Errors.ExpectedModuleOrOpen);
} else {
erroneousTree = syntaxError(pos, errs, Errors.Expected3(CLASS, INTERFACE, ENUM));
}
return toP(F.Exec(erroneousTree));
}
}
classDeclatation()方法如下
protected JCClassDecl classDeclaration(JCModifiers mods, Comment dc) {
int pos = token.pos;
accept(CLASS);
Name name = typeName();
List<JCTypeParameter> typarams = typeParametersOpt();
JCExpression extending = null;
if (token.kind == EXTENDS) {
nextToken();
extending = parseType();
}
List<JCExpression> implementing = List.nil();
if (token.kind == IMPLEMENTS) {
nextToken();
implementing = typeList();
}
//解析类中的所有成员信息,并存储在集合中
List<JCTree> defs = classOrInterfaceBody(name, false);
//将类中的所有成员信息整合为一棵JCClassDecl语法树
JCClassDecl result = toP(F.at(pos).ClassDef(
mods, name, typarams, extending, implementing, defs));
attach(result, dc);
return result;
}
上述代码中,classBody的解析由 classOrInterfaceBody()方法负责, 类成员信息全部解析成功后所有的类成员信息在List集合中,并由语法解析器的ClassDef()方法将其整合为一棵JCClassDecl
ClassDef()方法如下
public JCClassDecl ClassDef(JCModifiers mods, Name name,
List<JCTypeParameter> typarams, JCExpression extending,
List<JCExpression> implementing, List<JCTree> defs) {
// 解析 JCClassDecl语法树
JCClassDecl tree = new JCClassDecl(mods, name, typarams, extending, implementing, defs, null);
tree.pos = pos;
return tree;
}
当成功将classBody中的内容信息解析并整合为一棵JCClassDecl语法树后,parseCompilationUnit()方法就会调用语法解析器的TopLevel()方法将之前解析过的package语法节点、import语法树和class语法树等内容信息全部整合为一棵JCCompilationUnit语法树。代码如下:
public JCCompilationUnit TopLevel(List<JCTree> defs) {
for (JCTree node : defs)
Assert.check(node instanceof JCClassDecl
|| node instanceof JCPackageDecl
|| node instanceof JCImport
|| node instanceof JCModuleDecl
|| node instanceof JCSkip
|| node instanceof JCErroneous
|| (node instanceof JCExpressionStatement
&& ((JCExpressionStatement)node).expr instanceof JCErroneous),
() -> node.getClass().getSimpleName());
JCCompilationUnit tree = new JCCompilationUnit(defs);
tree.pos = pos;
return tree;
}
第三步:语义解析
1、主要步骤
语法树能表示一个结构正确的源程序的抽象,但无法保证源程序是符合逻辑的。而语义分析的主要任务是读结构上正确的源程序进行上下文有关性质的审查。
语义分析过程分为标注检查和数据及控制流分析两个步骤:
- 为没有构造方法的类型添加缺省的无参构造方法;
- 检查任何类型的变量在使用前是否都已经经历过初始化;
- 检查变量类型是否与值匹配;
- 将String类型的常量进行合并处理;
- 检查代码中的所有操作语句是否可达;
- 异常检查;
- 解除Java语法糖;
2、额外话题
这里有一个比较有趣的现象,那就是一个string类型的变量中如果包含多个常量信息并通过符号“+”组合在一起时,底层究竟创建了多少个string对象?
从表面看,相信大家都会误以为string的对象数量与常量数量是等价的,可是事实并非如此,编译器在执行语义解析时,会检查string变量中是否包含多个常量信息并通过符号“+”组合在一起,如果确实存在,语义解析器则会将其合并为一个字符串,这就是常量折叠操作。
如下所示:
// 语义解析前
String str = "名字:朱佳文" + ", 性别:男 ";
// 语义解析后
String str = 名字:朱佳文, 性别:男 ";
当经历过这一系列语义解析步骤之后,就构成了一个完善的编译前提,编译器将会使用这个扩充后的语法树将其编译为Java
字节码
第四步:字节码生成
成功经历过词法分析、语法分析和语义分析等步骤后,所解析出来的语法树已经非常完善了,那么javac编译器最后的任务就是调用com.sun.tools.javac.jvm.Gen类将这棵语法树编译为Java字节码文件,所谓编译字节码就是把符合Java语法规范的Java代码转换为符合JVM规范的字节码文件。
ps:JVM的架构模型是基于栈的,也就是说,在JVM中所有的操作都需要经过入栈和出栈来完成。
标签:Java,字节,编译器,pos,语法,编译,token,JVM,解析 From: https://blog.51cto.com/u_11906056/7060959字节码生成是Javac编译过程的最后一个阶段。字节码生成阶段不仅仅是把前面各个步骤所生成的信息转化成字节码写到磁盘中,编译器还进行了少量的代码添加和转换工作。 实例构造器()方法和类构造器()方法就是在这个阶段添加到语法树之中的(这里的实例构造器并不是指默认的构造函数,而是指我们自己重载的构造函数,如果用户代码中没有提供任何构造函数,那编译器会自动添加一个没有参数、访问权限与当前类一致的默认构造函数,这个工作在填充符号表阶段就已经完成了)。