第十章 早期(编译期)优化
10.1 概述
Java 语言的编译期实际上是一段不确定的操作过程,他可能是指 java 文件编译到 class 文件的过程,也可能是 JIT 把字节码转为机器码的过程,还有可能是使用静态提前编译器(AOT,Ahead Of Time Compiler)直接把 java 文件编译成本地机器码的过程。下面列举了三类编译过程中一些比较有代表性的编辑器:
- 前端编译器:Sun 的 Javac、Eclipse JDT 中的增量式编译器(ECJ)
- JIT 编译器:HotSpot VM 的 C1、C2 编译器。
- AOT 编译器:GNU Compiler for the Java(GCT)、Excelsior JET。
这三类过程大家熟悉的应该是第一类。本章后续文字中提到的 “编译器”、“编译期” 都仅限于第一类,第二类我们放到下一章去讨论。
10.2 Javac 编译器
10.2.1 Javac 的源码与调试
OpenJdk 下载地址 :http://hg.openjdk.java.net
下载后向 IDEA 中导入相应源码,下载方式请自行百度。
从 Sun Javac 的代码来看,编译过程大致可以分为三个过程:
- 解析与填充符号表过程
- 插入式注解处理器的注解处理过程
- 分析与字节码生成过程
如图:
方法主要集中在 JavaCompiler 类 compile() 和 compile2() 方法中,主体代码如下:
try {
// 准备过程:初始化插入式注解处理器
initProcessAnnotations(processors);
// These method calls must be chained to avoid memory leaks
// 翻译:这些方法调用必须链接在一起,以避免内存泄漏
delegateCompiler =
// 执行注解处理
processAnnotations(
// 输入到符号表
enterTrees(stopIfError
// 词法分析、语法分析
(CompileState.PARSE, parseFiles(sourceFileObjects))),
classnames);
// 分析及字节码生成
/**
* The phases following annotation processing: attribution,
* desugar, and finally code generation.
*
* 注解处理之后的阶段:标注,解语法糖和最终代码生成。
*/
delegateCompiler.compile2();
delegateCompiler.close();
elapsed_msec = delegateCompiler.elapsed_msec;
} catch (Abort ex) {
if (devVerbose)
ex.printStackTrace(System.err);
} finally {
if (procEnvImpl != null)
procEnvImpl.close();
}
10.2.2 解析与填充符号表
解析步骤包括词法分析、语法分析两个过程:
- 词法、语法分析:
词法分析是将字符流转变为 标记(Token)集合,例如 int 三个字符是一个 Token。这个过程是由 com.sun.tools.javac.parser.Scanner 类完成的。
语法分析则是根据 Token 序列构造抽象语法树的过程,抽象语法树(Abstract Syntax Tree,AST)是一种用来描述程序代码语法结构的树形表示方式。语法树的每一个节点都代表着程序代码中的一个语法结构,例如包、类型、修饰符、运算符、接口、返回值甚至代码注释等都可以是一个语法结构。 - 填充符号表:符号表是一组符号地址和符号信息构成的表格。符号表中所登记的信息在编译的不同阶段都要用到。语义分析中符号表所登记的内容将用于语义检查和产生中间代码。在目标代码生成阶段,当对符号名进行地址分配时,符号表是地址分配的依据。
10.2.3 注解处理器
插入式注解我们可以把它看作是一组编译器的插件,在这些插件中可以读取、修改、添加抽象语法树中的任意元素。如果这些插件在处理注解期间对语法书进行了修改,编译器将回到解析及填充符号表的过程重新处理。每一次循环称呼为一个 Round 。
有了编译器注解处理的标准 API 后,我们的代码才有可能干涉编译器的行为。在 Javac 源码中,插入式注解处理器的初始化过程是在 initPorcessAnnotations() 方法中完成的,而他的执行过程是在 processAnnotations() 方法中完成的,这个方法判断是否还有新的注解处理器需要执行,如果有的话通过 com.sun.tools.javac.processing.JavacProcessingEnvironment 类的 doProcessing() 方法生成一个新的 JavaCompiler 对象对编译的后续步骤进行处理。
10.2.4 语义分析与字节码生成
语法分析后,编译器获得了 AST ,AST 能表示一个结构正确的源程序的抽象,但无法保证源程序是符合逻辑的。而语义分析的主要任务是对结构上正确的源程序进行上下文有关性质的审查,如类型审查。这里主要有 4 步:
- 标注检查
- 数据及控制流分析
- 解语法糖
- 字节码生成
10.3 Java 语法糖
几乎各种语言都或多或少的提供过一些语法糖来方便程序员的代码开发。他们不会提供实质性的功能改进,但是他们能提高效率或语法严谨性,或减少代码出错。
10.3.1 泛型与类型擦除
Java 语言的泛型实际上只在程序源码中存在,在编译后的字节码文件中就已经替换为原来的原生类型(Raw Type,也称为裸类型)了,并且在相应的地方插入了强制转型代码。所以对于运行期的 Java 来说,ArrayList<int> 和 ArrayList<String> 就是同一个类,所以泛型技术实际上是 Java 语言的一颗语法糖,Java 语言中的泛型实现方法称为类型擦除,基于这种方法实现的泛型称为伪泛型。例如:
public static void main(String[] args) {
Map<String, String> map = new HashMap<>();
map.put("hello", "你好");
map.put("how are you", "你好吗");
System.out.println(map.get("hello"));
System.out.println(map.get("how are you"));
}
这段代码很简单,但是编译成 class 文件后再反编译呢 :
这里可以看到其实已经加入了 强转 的代码。
10.2.3 自动装箱、拆箱与遍历循环
从纯技术角度来说,自动拆装箱和遍历(Foreach 循环)这些语法糖,无论是是线上还是思想上都不如上文讲的泛型相比。
这里作者给了一个思考题但没有给出答案,这里列出:
public static void main(String[] args) {
Integer a = 1;
Integer b = 2;
Integer c = 3;
Integer d = 3;
Integer e = 321;
Integer f = 321;
Long g = 3L;
System.out.println(c == d);
System.out.println(e == f);
System.out.println(c == (a + b));
System.out.println(c.equals(a + b));
System.out.println(g == (a + b));
System.out.println(g.equals(a + b));
}
如果 Java 基础扎实的话,结果其实挺明显的:
最后一个结果可能会有些困扰,附上 Long 的源码即一目了然:
是的,这里会先判断是不是 Long 的实例,不是的话直接抛 false 了。String、Integer 等同理。
10.3.3 条件编译
这个就比较简单,例如下面的代码:
public static void main(String[] args) {
if (true) {
System.out.println(true);
} else {
System.out.println(false);
}
}
编译后成了这样:
public static void main(String[] args) {
System.out.println(true);
}
10.4 实战:插入式注解-自动生成 Get / Set
书上是 插入式注解处理器解决 Java 程序命名规范检查的,但是我对 Lombok 插件比较感兴趣,他是一个自动生成 Get / Set 方法,构造方法 等的插件,只需要标记 @Data 等即可实现,初步猜测原理应该也是插入式注解处理器,想手撸一个简易版。代码如下,项目代码已上传 Git,可以直接下载运行:https://github.com/ccqctljx/SimonLombok
需要注意的地方是,下面的 List 不是 java.util.List,而是 com.sun.tools.javac.util.List 。我在这个坑里待了好久,希望大家不要踩。
/**
* @Date 2020/2/27 20:08
*/
@SupportedAnnotationTypes("com.simon.annotation.SimonData")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class GenGSProcessor extends AbstractProcessor {
/**
* Messager接口提供注解处理器用来报告错误消息、警告和其他通知的方式
* 它不是注解处理器开发者的日志工具,而是用来写一些信息给使用此注解器的第三方开发者的
* 注意:我们应该对在处理过程中可能发生的异常进行捕获,通过Messager接口提供的方法通知用户(在官方文档中描述了消息的不同级别。非常重要的是Kind.ERROR)。
* 此外,使用带有Element参数的方法连接到出错的元素,
* 用户可以直接点击错误信息跳到出错源文件的相应行。
* 如果你在process()中抛出一个异常,那么运行注解处理器的JVM将会崩溃(就像其他Java应用一样),
* 这样用户会从javac中得到一个非常难懂出错信息
*/
private Messager messager;
/**
* 实现Filer接口的对象,用于创建文件、类和辅助文件。
* 使用Filer你可以创建文件
* Filer中提供了一系列方法,可以用来创建class、java、resources文件
* filer.createClassFile()[创建一个新的类文件,并返回一个对象以允许写入它]
* filer.createResource() [创建一个新的源文件,并返回一个对象以允许写入它]
* filer.createSourceFile() [创建一个用于写入操作的新辅助资源文件,并为它返回一个文件对象]
*/
private Filer filer;
/**
* 用来处理Element的工具类
* Elements接口的对象,用于操作元素的工具类。
*/
private JavacElements elementUtils;
/**
* 用来处理TypeMirror的工具类
* 实现Types接口的对象,用于操作类型的工具类。
*/
private Types typeUtils;
/**
* 提供了待处理的抽象语法树
* 这个依赖需要将${JAVA_HOME}/lib/tools.jar 添加到项目的classpath,IDE默认不加载这个依赖
*/
private JavacTrees trees;
/**
* 这个依赖需要将${JAVA_HOME}/lib/tools.jar 添加到项目的classpath,IDE默认不加载这个依赖
* TreeMaker 创建语法树节点的所有方法,创建时会为创建出来的JCTree设置pos字段,
* 所以必须用上下文相关的TreeMaker对象来创建语法树节点,而不能直接new语法树节点。
*/
private TreeMaker treeMaker;
/**
* 提供了创建标识符的方法
*/
private Names names;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
System.out.println("GenGSProcessor Initial Start......");
super.init(processingEnv);
this.messager = processingEnv.getMessager();
this.filer = processingEnv.getFiler();
this.elementUtils = (JavacElements) processingEnv.getElementUtils();
this.typeUtils = processingEnv.getTypeUtils();
this.trees = JavacTrees.instance(processingEnv);
Context context = ((JavacProcessingEnvironment) processingEnv).getContext();
this.treeMaker = TreeMaker.instance(context);
this.names = Names.instance(context);
System.out.println("GenGSProcessor Initial End......");
}
/**
* 该方法将一轮一轮的遍历源代码
* 处理注解前需要先获取两个重要信息,
* 第一是注解本身的信息,具体来说就是获取注解对象,有了注解对象以后就可以获取注解的值。
* 第二是被注解元素的信息,具体来说就是获取被注解的字段、方法、类等元素的信息
*
* @param annotations 该方法需要处理的注解类型
* @param roundEnv 关于一轮遍历中提供给我们调用的信息.
* @return 该轮注解是否处理完成 true 下轮或者其他的注解处理器将不会接收到次类型的注解.用处不大.
*/
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
System.out.println("processing.......");
// roundEnv.getRootElements()会返回工程中所有的Class,在实际应用中需要对各个Class先做过滤以提高效率,避免对每个Class的内容都进行扫描
roundEnv.getRootElements();
messager.printMessage(Diagnostic.Kind.NOTE, "SimonDataProcessor注解处理器处理中");
TypeElement currentAnnotation = null;
// 遍历注解集合,也即@SupportedAnnotationTypes中标注的类型
for (TypeElement annotation : annotations) {
messager.printMessage(Diagnostic.Kind.NOTE, "遍历本注解处理器处理的所有注解,当前遍历到的注解是:" + annotation.getSimpleName());
currentAnnotation = annotation;
}
// 获取所有包含 SimonData 注解的元素 (roundEnv.getElementsAnnotatedWith(SimonData.class))
// 返回所有被注解了 @SimonData 的元素的列表。
Set<? extends Element> elementSet = roundEnv.getElementsAnnotatedWith(SimonData.class);
messager.printMessage(Diagnostic.Kind.NOTE, "SimonDataProcessor注解处理器处理 @SimonData 注解");
for (Element element : elementSet) {
// 类名
String className = element.getSimpleName().toString();
messager.printMessage(Diagnostic.Kind.NOTE, "当前被标注注解的方法所在的类是:" + className);
// 如果是类的话,获取子字段
if (ElementKind.CLASS == element.getKind()) {
// 这里根据类 Element 拿出抽象语法树
JCTree tree = trees.getTree(element);
tree.accept(new TreeTranslator(){
@Override
public void visitClassDef(JCTree.JCClassDecl jcClassDecl){
jcClassDecl.defs.stream()
// 拿出变量
.filter(k -> k.getKind().equals(Tree.Kind.VARIABLE))
.map(t -> (JCTree.JCVariableDecl) t)
.forEach(jcVariableDecl -> {
//添加get方法
jcClassDecl.defs = jcClassDecl.defs.append(makeGetterMethodDecl(jcVariableDecl));
//添加set方法
jcClassDecl.defs = jcClassDecl.defs.append(makeSetterMethodDecl(jcVariableDecl));
});
super.visitClassDef(jcClassDecl);
}
});
}
}
return true;
}
/**
* 创建get方法
*
* @param jcVariableDecl
* @return
*/
private JCTree.JCMethodDecl makeGetterMethodDecl(JCTree.JCVariableDecl jcVariableDecl) {
//方法的访问级别
JCTree.JCModifiers modifiers = treeMaker.Modifiers(Flags.PUBLIC);
//方法名称
Name methodName = generateMethodNames(jcVariableDecl.getName(), GET_METHOD, names);
//设置返回值类型
JCTree.JCExpression returnMethodType = jcVariableDecl.vartype;
// return 语句
ListBuffer<JCTree.JCStatement> statements = new ListBuffer<>();
statements.append(treeMaker.Return(treeMaker.Select(treeMaker.Ident(names.fromString("this")), jcVariableDecl.getName())));
//设置方法体
JCTree.JCBlock methodBody = treeMaker.Block(0, statements.toList());
List<JCTree.JCTypeParameter> methodGenericParams = List.nil();
List<JCTree.JCVariableDecl> parameters = List.nil();
List<JCTree.JCExpression> throwsClauses = List.nil();
//构建方法
return treeMaker.MethodDef(modifiers, methodName, returnMethodType, methodGenericParams, parameters, throwsClauses, methodBody, null);
}
/**
* 创建set方法
*
* @param jcVariableDecl
* @return
*/
private JCTree.JCMethodDecl makeSetterMethodDecl(JCTree.JCVariableDecl jcVariableDecl) {
//方法的访问级别
JCTree.JCModifiers modifiers = treeMaker.Modifiers(Flags.PUBLIC);
//定义方法名
Name methodName = generateMethodNames(jcVariableDecl.getName(), SET_METHOD, names);
//定义返回值类型
JCTree.JCExpression returnMethodType = treeMaker.TypeIdent(TypeTag.VOID);
ListBuffer<JCTree.JCStatement> statements = new ListBuffer<>();
//this.xxx = xxx; setter方法中的赋值语句
JCTree.JCStatement jcStatement = treeMaker.Exec(treeMaker.Assign(
treeMaker.Select(treeMaker.Ident(names.fromString("this")), jcVariableDecl.getName()),
treeMaker.Ident(jcVariableDecl.getName())));
statements.append(jcStatement);
//定义方法体
JCTree.JCBlock methodBody = treeMaker.Block(0, statements.toList());
List<JCTree.JCTypeParameter> methodGenericParams = List.nil();
//定义入参
JCTree.JCVariableDecl param = treeMaker.VarDef(treeMaker.Modifiers(Flags.PARAMETER, List.nil()), jcVariableDecl.name, jcVariableDecl.vartype, null);
//设置入参
List<JCTree.JCVariableDecl> parameters = List.of(param);
List<JCTree.JCExpression> throwsClauses = List.nil();
//构建新方法
return treeMaker.MethodDef(modifiers, methodName, returnMethodType, methodGenericParams, parameters, throwsClauses, methodBody, null);
}
}
/**
* 工具类
* @Date 2020/2/27 21:20
*/
public class GenerateGSTools {
public static final String GET_METHOD = "get";
public static final String SET_METHOD = "set";
/**
* 根据类型生成方法
* @param fieldName 字段名称
* @param methodType 方法类型
* @return
*/
public static Name generateMethodNames(Name fieldName, String methodType, Names names) {
return names.fromString(methodType + upperFirstLetter(fieldName.toString()));
}
/**
* 将首字母大写
* @param str
* @return
*/
private static String upperFirstLetter(String str) {
char[] ch = str.toCharArray();
if (ch[0] >= 'a' && ch[0] <= 'z') {
ch[0] = (char) (ch[0] - 32);
}
return new String(ch);
}
}
/**
* 注解类
* @Date 2020/2/27 20:23
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface SimonData {
}
其中借鉴了以下博客(最后一篇直接是手撸源码。。我都快写完才发现的):
打包自定义插入式注解: https://www.cnblogs.com/avenwu/p/4173899.html
获取类、字段:https://blog.csdn.net/zhuhai__yizhi/article/details/51394810
编辑语法树:https://blog.csdn.net/a_zhenzhen/article/details/86065063
https://www.cnblogs.com/kanyun/p/11541826.html
生成 GET / SET 方法:https://www.jianshu.com/p/68fcbc154c2f
撸完代码,跑了个例子,成就感爆棚!:
至此,全面成功~~
读书越多越发现自己的无知,Keep Fighting!
本文仅是在自我学习 《深入理解Java虚拟机》这本书后进行的自我总结,有错欢迎友善指正。
欢迎友善交流,不喜勿喷~
Hope can help~