java 程序的编译工作通常使用 IDE 或 Maven,Gradle 等工具完成,开发过程容易忽视java编译期隐藏的技术细节,深入理解 javac、编译等相关概念。
javac 是 JDK 的Java语言前端编译器工具,将满足 Java 语言规范(JLS, Java Language Specification)的 .java 源文件编译成为满足JVM规范(JVMS, Java Virtual Machine Specification)的 .class 字节码文件。
javac的知识合集 = 编译原理 + JDK( JSR-269 Pluggable Annotation Processing API,JSR 199 the Java Compiler API,语法糖,javac工作原理等)+ JLS + JVMS + bytescode
javac的知识引申 = 生成字节码编译过程,并发编程内存模型、java 关键字实现、字节码增强技术等。
一般而言,javac 的编译过程为:源代码 –(词法分析)–> 符号Token流 –(语法分析)–> 抽象语法树 –(注解处理器)–> 插入式注解语法树 -(语义分析)-> 完整语法树 –(生成代码)–> 字节码。是java的前端编译器。
javac由java语言编写,方便调试学习。
1 hello javac
1.1 javac 经典应用
.java 源文件:
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
class SayHiWorld{
public static void main(String[] args) {
System.out.println("Hi! World!" + org.joda.time.DateTime.now().toString("E"););
}
}
编译为 .class 类文件:
javac -help
javac -cp "lib/jodatime.jar" -d target -target 1.8 HelloWorld.java
- -d 指定了生成class文件的根目录,并且会根据class的包路径创建子目录。
- -cp JRE搜索资源文件的路径指定,默认当前路径,只会影响当前进程,覆盖CLASSPATH。
1.2 javac 实例场景
├── lib
│ └── jodatime.jar
├── resources
│ └── config.xml
├── src
│ ├── HelloWorld.java
│ └── service
│ ├── WorldConfig.java
│ ├── WorldService.java
│ └── impl
│ └── EarthServiceImpl.java
└── target
自动化编译脚本
PROJECT_DIR=/Users/sapphire/Projects/java/basic
# clean target directory
rm -rf $PROJECT_DIR/target/*
# prepare arg files
find $PROJECT_DIR/src -name "*.java">$PROJECT_DIR/target/files.txt
echo "-d $PROJECT_DIR/target" >$PROJECT_DIR/target/options.txt
# compile
javac -cp "$PROJECT_DIR/lib/*" @$PROJECT_DIR/target/options.txt @$PROJECT_DIR/target/files.txt
# copy resources to target
cp -rf $PROJECT_DIR/resources/* $PROJECT_DIR/target
# clean temp files
rm -rf $PROJECT_DIR/target/options.txt $PROJECT_DIR/target/files.txt
1.3 编译 与 javac
1.3.1 编译
编译:将便于人编写、阅读、维护的高级计算机语言写的源代码程序,翻译为计算机能解读、运行的低阶机器语言的程序的过程。负责这一过程的处理的工具叫做编译器。
简单理解,编译 = 高级语言源代码 —(分析 + 翻译 + 优化)—> 低级语言机器码。
编译 | 描述 |
---|---|
分析 | 语法、词法、语义 |
翻译 | java语言规范 -> jvm规范 |
优化 | 改善编码风格并提高效率 |
编译原理中,根据编译任务不同:
字节码不是机器能识别的语言,还需要 JVM 再将字节码转换成机器码。
编译期 | 任务 | 代表 |
---|---|---|
前期编译器 | .java -> .class | Sun Javac、Eclipse JDT 的增量式编译器 |
后端运行期编译器 | .class -> 机器码 | HotSpot 的 C1、C2 编译器 |
静态前期编译器 | .java -> 机器码 | GCJ、Excelsior JET |
前端编译为字节码的好处:
- 解耦后端JVM编译,减少JVM工作,避免每次执行时词法、语法、语义分析。方便读取,执行速度比直接解析源代码(AOT)快。
- 字节码可以由Groovy,Clojure,Scala跨语言生成,供JVM调用。
- 字节码有版本信息,可以在编译过程在一些语言层面上擦除版本变化。
- 字节码格式比源码紧凑、轻量,方便方便网络传输。嵌入式设备不够资源跑起完整的编译器,只需嵌入一个小巧的JVM就可以编译源码。
1.3.2 前端编译 javac
javac 是java前端编译的一种编译器实现。
🗣 Tips :
- Javac对代码的运行效率几乎没有优化措施,性能的优化集中在后端的即时编译器中。javac编译器实现一些“语法糖”,例如foreach语法、注解等。字节码是对程序应该如何表现的描述,JVM对Program的行为和硬件有更多了解,JIT时对字节码进行任意优化。在很多情况下,编译时优化阻碍了JIT时更重要的优化。
- 《深入理解JAVA虚拟机》第十、十一章 编译运行期优化
1.3.3 反编译
- javap 将字节码转化为看得懂字节码,
synchronized
底层依赖了ACC_SYNCHRONIZED
标记和monitorenter
、monitorexit
两个指令来实现同步。 - jad 不支持 Java 8 - lambda 表达式,字符串的 switch 是通过
equals()
和hashCode()
实现的。 - IDEA插件 jclasslib 字节码查看器
view | show bytecode with jclasslib
。
1.3.4 后端编译 Just-In-Time Compiler
本文关注javac的编译过程,对后端编译一笔带过。
传统 JVM 通过解释字节码将其翻译成对应的机器指令执行。为了解决效率问题,小部分热点代码消耗大部分的资源,引入 JIT 技术。
- 进行热点探测(Hot Spot Detection)识别热点代码(Hot Spot Code)翻译成机器码后缓存。HotSpot 使用基于计数器的热点探测(Counter Based Hot Spot Detection)设计方法计数器(方法、代码块)、回计数器(for/while)统计方法的执行次数,超过阀值就认为是热点方法,触发JIT编译。
- HotSpot 内置
Client Compiler
(C1 更好的编译速度)和Server Compiler
(C2 更好的编译质量)两种JIT编译模式分层编译。 - 编译优化,如逃逸分析、 锁消除、 锁膨胀、 方法内联、 空值检查消除、 类型检测消除、 公共子表达式消除。
JVM 实际采用解释器和JIT混用模式 Java HotSpot(TM) 64-Bit Server VM (build 13+33, mixed mode, sharing)
2 javac 编译过程源码分析
从 Sun Javac 的源码来看,编译main方法位于com.sun.tools.javac.Main
// my code
public static void main(String[] args) {
Main m = new Main("fx_debug");
m.compile(new String[]{"/.../HelloWorld.java"});
}
// com.sun.tools.javac.Main
public static void main(String[] args) throws Exception {
System.exit(compile(args));//入口
}
public Result compile(String[] args,
String[] classNames,
Context context,
List<JavaFileObject> fileObjects,
Iterable<? extends Processor> processors) {
// ...检测javac行中命令参数不对,返回错误码 Result.CMDERR
// ...得到编译文件集合
files = processArgs(CommandLine.parse(args), classNames);
fileManager = context.get(JavaFileManager.class);
//...实例编译器
JavaCompiler.instance(context).compile(fileObjects,
classnames.toList(),
processors);
}
//com.sun.tools.javac.main.JavaCompiler
public void compile(List<JavaFileObject> sourceFileObjects,
List<String> classnames,
Iterable<? extends Processor> processors) {
// 初始化插入式注解处理器
initProcessAnnotations(processors);
delegateCompiler =
// 2.注解处理执行
processAnnotations(
// 1.2 输入到符号表
enterTrees(stopIfError(CompileState.PARSE,
// 1.1 词法分析和语法分析
parseFiles(sourceFileObjects))), classnames);
// 3.语义分析及字节码class文件生成
delegateCompiler.compile2();
编译过程大致可以分为3个过程
分别对应Token流,语法树,注解语法树,字节码输出。
2.1 Parse and Enter
两个重要的接口与实现
接口 | 作用 | 实现 |
---|---|---|
com.sun.tools.javac.parser.Lexer | 词法分析 | com.sun.tools.javac.parser.JavacParser |
com.sun.tools.javac.parser.Parser | 构建抽象语法树 | com.sun.tools.javac.parser.Scanner |
该阶段将源码文件解析构建抽象语法树( Abstract Syntax Tree,AST )。从功能上分为 词法分析 和 语法分析,实际上同时进行。编译时,通过 ParserFactory 与 ScannerFactory 工厂类管理 JavacParser 与 Scanner 对象。JavacParser 解析时,Scanner 读取源文件字符流,逐个读入 Token,构建抽象语法树。之后,编译器就基本不会再对源码文件进行操作,后续操作都建立在抽象语法树之上。(添加默认无参构造方法等)
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();
}
2.1.1 词法分析
将Java源代码按照Java关键字、自定义关键字、符号等按顺序分解为了可识别的Token流。
输入 | 输出 | 描述 |
---|---|---|
源代码的字符流 | 标记(Token)集合 | 关键字、变量名、字面量、运算符 |
字符char是程序编写过程中的的最小元素,标记Token是编译过程的最小元素。
主要实现类 | 功能 |
---|---|
com.sun.tools.javac.parser.JavacParser | 规定哪些词符合Java语言规范,具体读取和归类不同词法的操作由scanner完成。 |
com.sun.tools.javac.parser.Scanner | 负责逐个读取源代码的单个字符,然后解析符合Java语言规范的Token序列,调用一次nextToken()都构造一个Token |
com.sun.tools.javac.parser.Tokens$Token | 规定了所有Java语言的合法关键词,包含了开始/结束位置,类型。 |
com.sun.tools.javac.parser.Tokens$TokenKind | 描述一个Token的类型,如IDENTIFIER(自定义标识)、BOOLEAN、BREAK、BYTE、CASE。 |
com.sun.tools.javac.util.Names | 用来存储和表示解析后的词法,每个字符集合都会是一个Name对象,所有的对象都存储在Name.Table内部类中。 |
com.sun.tools.javac.parser.KeyWords * | 负责将字符集合对应到token集合中。JDK9后由Tokens完成 |
🗣 Tips :
- java命名规范指出声明变量的时候必须以字母、下划线或者美元符开头,包括字母、数字、下划线或者美元符。由JavacParser规定并识别 int y=x+1; package per.rsf.javac; 的Token流。
- Tokens根据Token.name先转化成Name对象,建立Name和Token的对应关系,保存在key数组中。这个key数组只保存了在Token类中定义的所有Token到Name对象的关系,而其他所有字符集合Tokens都会将它对应到TokenKind.IDENTIFIER类型
- Javac中每个与文件相关的实现类都直接或间接实现了JavaFileObject接口,这个接口专门为操作.java文件及.class文件而定义的。每个RegularFileObject类对象可以代表一个Java源文件。调用getCharContent()方法获取字符流输入。
- 关键代码nextToken的主要逻辑:处理特殊字符、标识符、16进制、数字、分隔符、斜杠开头、反斜杠开头、双引号开头、默认处理。
例:package的Token读取流程
2.1.2 语法分析
输入 | 输出 | 描述 |
---|---|---|
标记(Token)集合 | 抽象语法树(AST) | 包、类型、运算符、修饰符、接口、返回值、代码注释 |
将Token流组装成更结构化的语法树,描述程序代码语法结构,检查是否符合Java语言规范。每个语法树上的节点都是com.sun.tools.javac.tree.JCTree
的一个实例,继承自com.sun.source.tree.Tree
接口,代表着程序代码中的一个语法结构(Construct),如包、类型、修饰符、运算符、接口、返回值甚至代码注释。
实现类 | 功能 |
---|---|
com.sun.tools.javac.tree.TreeMaker | 生成语法节点,根据Name对象构建一个语法节点 |
com.sun.tools.javac.tree.JCTree | 生成的语法节点都会继承jctree和实现(如根节点JCCompilationUnit) |
com.sun.tools.javac.tree.JCTree#Tag | enum类,区分语法树的类型。类型的数值是上一个节点类型的数值+1 |
com.sun.tools.javac.tree.JCTree#pos | 语法节点在源文件中的起始位置,-1表示不存在 |
com.sun.tools.javac.tree.JCTree#type | Java类型(int、float、String) |
🗣 Tips :
-
JCCompilationUnit表示一个编译单元,一般是一个源文件(可以是多个类)内容对应一个编译单元,同时这也是顶层的树节点。包含包注解 List、包名 JCExpression、和树 List。
-
在遍历像抽象语法树这样由各种类的实例所组成的树形结构时,通常会借助Visitors访问者模式来完成。
2.1.3 填充符号表
该步骤实际发生在语义分析中,在处理注解前。
一个类中的符号变量,除了类本身定义,其他类定义。如调用其他类方法、变量,继承或实现父类和接口等。
调用其他类的符号变量时,就需要通过符号表来进行查找。(符号引用)
这些类的符号也需解析到符号表中。com.sun.tools.javac.comp.Enter
按照递归向下的顺序解析语法树,
- 将所有类中出现的符号输入到自身的符号表,并将类符号、类的参数类型符号(泛型参数类型)、超类符号,继承类型符号和继承的接口类型符号都存储到一个未处理列表中。
- 将这个未处理列表中的所有类都解析到各自的符号列表中。
输入 | 输出 | 描述 |
---|---|---|
当前范围的定义域(definitions) | 待处理列表,包含需要分析并生成类文件的树. | 一组符号地址和符号信息构成的表格 |
符号表是由一组符号地址和符号信息构成的表格。在语义分析中,符号表所登记的内容将用于语义检查和产生中间代码。在目标生成阶段,当对符号名进行地址分配时,符号表是地址分配的依据。
实现类 | 功能 |
---|---|
com.sun.tools.javac.comp.Enter | 内容填充,符号表(Symbol Table)填充的出口是一个待处理列表,包含了每一个编译单元的抽象语法树的顶级节点以及 package-info.java 的顶级节点。 |
com.sun.tools.javac.comp.MemberEnter | 使类变得完整,确定类的泛型参数、父类、接口,该类的所有符号输入到它所对应的scope |
VarSymbol | 预定义符号的输入,对操作符的处理 |
2.2 Annotation Processing
JDK 6 实现了插入式注解处理API(JSR-269),位于javax.annotation.processing
和javax.lang.model
包。
通过声明一个注解,实现一个注解处理器。注册服务后
在编译期由com.sun.tools.javac.processing.JavacProcessingEnvironment
处理注解。读取、修改、添加抽象语法树中的任意元素。像反射一样访问类、字段、方法和注解等元素,创建新的源文件。每一个插入式注解处理器操作语法树,编译器将回到解析及填充符号表的过程循环。
作用:减少编写配置文件的劳动量,提高代码可读性。
测试使用时,测试类和实现类写在不同子模块下!!!否则编译不通过!!!一般需要插件工具完成,如 Lombok 插件
2.2.1 javac 命令编译过程
- 声明一个注解
- 创建一个注解处理器,注解处理器需实现
javax.annotation.processing.Processor
接口或继承javax.annotation.processing.AbstractProcessor
类。重写process
方法。 - 为注解处理器注册服务。在
META-INF.services
文件夹下创建javax.annotation.processing.Processor
文件。写入注解处理器的全称。 - 为了能够让项目能够通过编译,我们需要为Java编译器添加一个不进行注解处理的参数
<!--maven默认运行javac编译,将javax.annotation.processing.Processor文件作为类路径的一部分。此时编译器期望注释处理器已编译成实例。引发“错误的服务配置文件...”错误。-proc:none 禁用注释处理-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<compilerArgument>-proc:none</compilerArgument>
</configuration>
</plugin>
也可使用 javac -processor
指定注解处理器
主要源码:
// 注解处理器
// 以下注解可以重写get...方法 如getSupportedAnnotationTypes getSupportedSourceVersion
// @SupportedAnnotationTypes("...")//注册注解处理器要处理的注解类型
// @SupportedSourceVersion(SourceVersion.RELEASE_8) 注册注解处理器要处理的源代码版本
// @SupportedOptions 注册通过命令行传递给处理器的操作选项
public MyProcessor extends AbstractProcessor{
@Override
// 自动调用,ProcessingEnvironment类提供工具类:Filer,Types,Elements,Messager等
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
}
@Override
// 处理器的main()方法。自动调用,扫描和处理注解的主要逻辑,RoundEnvironment类用于查找出程序元素上使用的注解
// 返回值true:接下来的处理器不可处理该注解;false:接下来的处理器可以处理该处理器处理的注解。
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
return false;
}
}
public interface ProcessingEnvironment {
Map<String,String> getOptions();
//Messager用来报告错误,警告和其他提示信息
Messager getMessager();
//Filer用来创建新的源文件,class文件以及辅助文件
Filer getFiler();
//Elements中包含用于操作Element的工具方法
Elements getElementUtils();
//Types中包含用于操作TypeMirror的工具方法
Types getTypeUtils();
SourceVersion getSourceVersion();
Locale getLocale();
}
public interface RoundEnvironment {
// 是否最终轮
boolean processingOver();
// 上一轮注解处理器是否产生错误
boolean errorRaised();
// 返回上一轮注解处理器生成的根元素
Set<? extends Element> getRootElements();
// 返回包含指定注解类型的元素的集合
Set<? extends Element> getElementsAnnotatedWith(TypeElement a);
// 返回包含指定注解类型的元素的集合
Set<? extends Element> getElementsAnnotatedWith(Class<? extends Annotation> a);
}
元素 | 含义 |
---|---|
Element | 程序元素(源代码) |
VariableElement | 代表一个字段,枚举常量,方法或者构造方法的参数,局部变量及异常参数等元素 |
PackageElement | 包元素 |
TypeElement | 类或接口元素 |
ExecutableElement | 代码方法,构造函数,类或接口的初始化代码块等元素,也包括注解类型元素 |
TypeMirror | 声明类型(类类型和接口类型),数组,类型变量和空类型。也代表通配类型参数,可执行文件的签名和返回类型等。TypeMirror = Element.asType() |
DeclaredType | 声明类型:类类型还是接口类型 |
2.2.2 编译器 API 实例过程
使用 CompilationTask
的 setProcessors
方法可以传入注解处理器。
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
VisitProcessor processor = new VisitProcessor();
StandardJavaFileManager manager = compiler.getStandardFileManager(diagnostics, null, null);
File file = new File(".../VisitProcessor.java");
Iterable < ? extends JavaFileObject> sources = manager.getJavaFileObjectsFromFiles(Arrays.asList(file));
CompilationTask task = compiler.getTask(null, manager, diagnostics, Arrays.asList("-d", "target/classes"), null, sources);
task.setProcessors(Arrays.asList(processor));
task.call();
manager.close();
2.2.3 实例
仿照findbugs实现一个简单的类编写规范检查
注解类 CHECK
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.SOURCE)
public @interface Check {//标识一个类被 findbugs 检查
}
注解处理类 CLASSCHECKER
//以下两个注解可以重写 getSupportedAnnotationTypes 与 getSupportedSourceVersion 方法
@SupportedAnnotationTypes("per.rsf.jsr269.anno.Check")//注册注解处理器要处理的注解类型
@SupportedSourceVersion(SourceVersion.RELEASE_8)//注册注解处理器要处理的源代码版本
//@SupportedOptions 注册通过命令行传递给处理器的操作选项
public class ClassChecker extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
Set<? extends Element> elements = roundEnv.getRootElements();
ClassScanner scanner8 = new ClassScanner();
for (Element element : elements) {
scanner8.scan(element);
}
return false;
}
}
class ClassScanner extends ElementScanner8<Set<? extends Element>, Element> {
//字段
@Override
public Set<? extends Element> visitVariable(VariableElement e, Element element) {
// if e.getConstantValue() != null 访问静态常量
// if e.getEnclosingElement().getKind() == ElementKind.ENUM 访问枚举
// else 访问实例变量
// ...
return super.visitVariable(e, element);
}
//类
@Override
public Set<? extends Element> visitType(TypeElement e, Element element) {
// e.getQualifiedName() 访问全类名
// e.getSimpleName() 访问类名
// ...
return super.visitType(e, element);
}
//方法
@Override
public Set<? extends Element> visitExecutable(ExecutableElement e, Element element){
// e.getReturnType().getKind() == TypeKind.BOOLEAN 访问方法返回类型
// ...
return super.visitExecutable(e, element);
}
}
注册服务
在文件夹resources/META-INF/services
下创建文件javax.annotation.processing.Processor
,内容如下:
per.rsf.jsr269.processor.ClassChecker
测试类
@Check
public class testCHECK {
static int a;
String UPPER;
public boolean haveIS(){
return true;
}
}
结果
将项目打成jar包使用
javac -cp /.../javac/target/javac.jar testCHECK.java
🗣 Tips :
- 模拟Lombok实现get set方法,依赖
com.sun.tools.javac.*
包
2.3 Analyse and Generate
2.3.1 语义分析
输入 | 输出 | 描述 |
---|---|---|
语法树 | 字节码文件 | 标注检查,数据及控制流分析 |
语法分析将源文件抽象成结构正确的抽象语法树,但无法保证符合逻辑的。还需添加默认的构造器,检查变量使用前是否已经初始化等。
实现类 | 功能 |
---|---|
com.sun.tools.javac.comp.Attr | 标注检查:名称消解、变量使用声明、类型检查、常量折叠、推导泛型方法的参数类型 |
com.sun.tools.javac.comp.Flow | 数据及控制流分析:局部变量在使用前是否被正确赋值、final变量不被重复修饰、确定方法返回值类型、方法的每条路径是否都有返回值、是否所有的受查异常都被正确处理 |
com.sun.tools.javac.comp.Check | 用来辅助Attr类检查语法树中变量类型是否正确,如二元操作符两边的操作数的类型是否匹配,方法返回值是否和接收的引用值类型匹配 |
com.sun.tools.javac.comp.Resolve | 检查变量,方法或者类的访问是否合法,变量是否是静态变量 |
com.sun.tools.javac.comp.ConstFold | 将一个字符串常量中的多个字符合并成一个字符串 |
com.sun.tools.javac.comp.Infer | 帮助推导泛型方法的参数类型 |
🗣 Tips :
- 常量折叠:a=1+2 -> a=3
- 如果代码中没有提供任何构造函数,自动添加一个没有参数、访问权限与当前类一致的默认构造函数。如果提供了构造函数,则在代码生成阶段添加。
语法糖(SYNTACTIC SUGAR)
Java中最常用的 语法糖 主要是前面提到过的泛型、变长参数、自动装箱/拆箱等。JVM运行时不支持这些语法,它们在编译阶段还原回简单的基础语法结构,这个过程称为解语法糖。解语法糖的过程由JavaCompiler#desugar()方法触发。
实现类 | 功能 |
---|---|
com.sun.tools.javac.comp.TransTypes | Generic Java to conventional Java |
com.sun.tools.javac.comp.Lower | inner classes, class literals, assertions, foreach loops, etc. |
2.3.2 字节码生成
输入 | 输出 | 描述 |
---|---|---|
语法树、符号表 | 字节码文件 | 编译器添加和转换少量的代码,生成字节码文件。 |
把前面各个步骤所生成的信息转化成字节码,
- 代码收敛,将方法块转成符合JVM语法的命令形式,jvm的所有操作都是基于栈的,所有操作都必须经过进出栈来完成。
- 按照jvm的文件组织格式将字节码输出到以class文扩展名的文件中。
在收敛代码的过程中,将实例构造器方法和类重载构造器方法添加到语法树之中。
实现类 | 功能 |
---|---|
com.sun.tools.javac.jvm.Gen | 遍历语法树,生成JVM操作码序列结合 |
om.sun.tools.javac.jvm.Items | 表示任何可寻址的操作项,这些操作项都可以作为一个单位出现在操作栈上,不同的Item对应不同JVM操作码 |
com.sun.tools.javac.jvm.Code | 存储生成的字节码,并提供一些能够映射操作码的方法 |
com.sun.tools.javac.jvm.ClassWriter | 输出字节码,生成最终的Class文件 |
CLINIT
与INIT
方法编译器自动收集static代码块,收敛顺序为先父后子,先单后块,父接口“随遇而安”。final static 静态不可变常量提前被编译器放入常量池,无需初始化。
方法编译器自动收集非static代码块,收敛顺序为先父后子,先单后块最后构造函数。
方法(实例化阶段)永远在方法(类初始化阶段)执行后执行。🗣 Tips :
clinit
类构造器,在jvm进行类加载—验证—解析—初始化中的初始化阶段(类实例化 调用静态字段或方法)调用,对静态变量,静态代码块进行初始化。多线程时,clinit
方法阻塞,一个类只会在一个JVM进程运行期间执行一次clinit
方法。init
实例构造器,类实例化阶段(new 反射 克隆 反序列化),对非静态变量解析初始化。若实例时类没有初始化,先执行clinit
方法。
static class Parent{
public int C=1;//init
public static int A=1;//clinit
static{ A=2;}//clinit
static class Sub extends Parent{
public int C=2;//init
public static int B=A//clinit
}
public static void main(String[]args){
System.out.println((new Sub).C);//2
System.out.println(Sub.B);//2
}
}
3 javac API
JDK 6 增加了规范 JSR-199 和 JSR-296,提供相关的 API
绿色标注的包是官方 API(Official API), JSR-199 和 JSR-296.
黄色标注的包为(Supported API).
紫色标注的包代码全部在 com.sun.tools.javac.*
包下,为内部 API(Internal API)和编译器的实现类。
类名 | 注释 |
---|---|
javax.annotation.processing | 注解处理 (JSR-296) |
javax.lang.model | 注解处理和编译器 Tree API 使用的语言模型 (JSR-296) |
javax.lang.model.element | 语言元素 |
javax.lang.model.type | 类型 |
javax.lang.model.util | 语言模型工具 |
javax.tools | Java 编译器 API (JSR-199) |
com.sun.source.* | 编译器 Tree API,提供 javac 工具使用的抽象语法树 AST 的只读访问 |
com.sun.tools.javac.* | 内部 API 和编译器的实现类 |
4 javac 调试
4.1 修改 idea.vmoptions
-Dcompiler.process.debug.port=12346
4.2 配置远程debug
4.3 启用idea编译调试
IDEA默认会禁用编译调试,这里需要开启一个开关,以此让IDEA在编译之前等待调试程序的链接。并且,这个配置在IDEA重启后会失效。
双击Shift
,打开平常搜索类的界面,输入debug build process
本文作者: Sapphire
本文链接: https:rensifei.site/2017/03/javac/
版权声明: 转载请注明出处!