JVM学习笔记(11) 第四部分 程序编译与代码优化 第10章 前端编译与优化

文章目录

  • [第10章 前端编译与优化前端编译与优化](#第10章 前端编译与优化前端编译与优化)
    • [10.0 个人感悟](#10.0 个人感悟)
    • [10.1 概述](#10.1 概述)
    • [10.2 Javac编译器](#10.2 Javac编译器)
      • [10.2.1 Javac的源码与调试](#10.2.1 Javac的源码与调试)
      • [10.2.2 解析与填充符号表](#10.2.2 解析与填充符号表)
      • [10.2.3 注解处理器](#10.2.3 注解处理器)
      • [10.2.4 语义分析与字节码生成](#10.2.4 语义分析与字节码生成)
    • [10.3 Java语法糖的味道](#10.3 Java语法糖的味道)
      • [10.3.1 泛型](#10.3.1 泛型)
      • [10.3.2 自动装箱、拆箱与遍历循环](#10.3.2 自动装箱、拆箱与遍历循环)
      • [10.3.3 条件编译](#10.3.3 条件编译)
      • [10.3.4 其他常见语法糖](#10.3.4 其他常见语法糖)
    • [10.4 实战:插入式注解处理器(本章略过)](#10.4 实战:插入式注解处理器(本章略过))

从计算机程序出现的第一天起,对效率的追逐就是程序员天生的坚定信仰,这个过程犹如一场没有终点、永不停歇的F1方程式竞赛,程序员是车手,技术平台则是赛道上飞驰的赛车。

第10章 前端编译与优化前端编译与优化

10.0 个人感悟

1. 原来编译器帮我们默默做了这么多事。 以前写Java代码的时候,泛型、增强for循环、自动装箱拆箱、变长参数这些特性用得理所当然,从来没想过它们背后需要编译器做多少"翻译"工作。读完这一章才恍然意识到------那些让我们编码更舒服的语法,JVM根本一个都不认识。编程语言的设计者把复杂留给了编译器,把简单留给了我们,极大提高了程序员的编码效率和幸福感,不像某些同事Q,.Q致敬!

2. 糖衣炮弹。语法糖让代码更甜,但甜味的代价是在编译期完成了"换汤不换药"的转换。lombok确实好用,有兴趣可以了解了解。

3. 关于技术债。 Java泛型选择类型擦除方案,本质上是为了兼容JDK 1.5之前海量的存量代码.这是当时约束条件下的最优解,但也从此给Java语言背上了一笔沉重的技术债------运行期拿不到泛型类型信息、基本类型无法作为泛型参数、字节码中充斥着强制类型转换和桥接方法。这件事给我最大的触动是:技术选型中没有完美的方案,只有特定历史条件下的最佳权衡。实际工作中也是如此。

4. 实战代码 时间精力因素,对于实战代码插入式注解处理器没整,大家感兴趣可以看看原书。

10.1 概述

在Java技术下谈"编译期"而没有具体上下文语境的话,其实是一句很含糊的表述。Java技术体系中存在三类编译过程,各有代表性的编译器产品:

编译器类型 作用 代表性产品
前端编译器 *.java文件转变成*.class文件 JDK的Javac、Eclipse JDT中的增量式编译器(ECJ)
即时编译器(JIT) 运行期把字节码转变成本地机器码 HotSpot的C1、C2编译器、Graal编译器
提前编译器(AOT) 直接把程序编译成与目标机器指令集相关的二进制代码 JDK的Jaotc、GNU Compiler for the Java(GCJ)、Excelsior JET

理解前端编译的意义,需要把握两个重要认知:

  • 前端编译器对代码的运行效率几乎没有任何优化措施。Java虚拟机设计团队选择把对性能的优化全部集中到运行期的即时编译器中,这样可以让那些不是由Javac产生的Class文件(如Kotlin、Scala编译的字节码)也同样能享受到编译器优化措施所带来的性能红利。
  • 相当多新生的Java语法特性,都是靠编译器的"语法糖"来实现的,而不是依赖字节码或者Java虚拟机的底层改进来支持。

Java中即时编译器在运行期的优化过程,支撑了程序执行效率的不断提升;而前端编译器在编译期的优化过程,支撑着程序员的编码效率和语言使用者的幸福感的提高。

10.2 Javac编译器

10.2.1 Javac的源码与调试

从Javac代码的总体结构来看,编译过程大致可以分为1个准备过程和3个处理过程

  • 准备过程
    • 初始化插入式注解处理器。
  • 解析与填充符号表过程
    • 词法、语法分析:将源代码的字符流转变为标记集合,构造出抽象语法树。
    • 填充符号表:产生符号地址和符号信息。
  • 插入式注解处理器的注解处理过程:插入式注解处理器的执行阶段。
  • 分析与字节码生成过程
    • 标注检查:对语法的静态信息进行检查。
    • 数据及控制流分析:对程序动态运行过程进行检查。
    • 解语法糖:将简化代码编写的语法糖还原为原有的形式。
    • 字节码生成:将前面各个步骤所生成的信息转化成字节码。

需要特别注意的是,执行插入式注解时又可能会产生新的符号。如果有新的符号产生,就必须转回到之前的解析、填充符号表的过程中重新处理这些新符号,因此这几个过程之间存在循环迭代的关系。

10.2.2 解析与填充符号表

1. 词法、语法分析

词法分析是将源代码的字符流转变为标记(Token) 集合的过程。单个字符是程序编写时的最小元素,但标记才是编译时的最小元素。关键字、变量名、字面量、运算符都可以作为标记。例如,代码int a = b + 2包含了6个标记,分别是inta=b+2

语法分析是根据标记序列构造抽象语法树(Abstract Syntax Tree,AST) 的过程。抽象语法树是一种用来描述程序代码语法结构的树形表示方式,其每一个节点都代表着程序代码中的一个语法结构,例如包、类型、修饰符、运算符、接口、返回值甚至连代码注释等都可以是一种特定的语法结构。经过词法和语法分析生成语法树以后,编译器就不会再对源码字符流进行操作了,后续的操作都建立在抽象语法树之上。

2. 填充符号表

符号表是由一组符号地址和符号信息构成的数据结构,可以类比想象成哈希表中键值对的存储形式。符号表中所登记的信息在编译的不同阶段都要被用到:在语义分析过程中,符号表所登记的内容将用于语义检查(如检查一个名字的使用和原先的声明是否一致)和产生中间代码;在目标代码生成阶段,当对符号名进行地址分配时,符号表是地址分配的直接依据。

10.2.3 注解处理器

JDK 5之后,Java语言提供了对注解的支持。可以把插入式注解处理器看作是一组编译器的插件,当这些插件工作时,允许读取、修改、添加抽象语法树中的任意元素。如果这些插件在处理注解期间对语法树进行过修改,编译器将回到解析及填充符号表的过程重新处理,直到所有插入式注解处理器都没有再对语法树进行修改为止。

著名的Lombok工具就是通过这一机制实现的------它在编译期通过注解处理器修改抽象语法树,自动生成getter/setter、构造器等代码,从而让源代码保持简洁。

10.2.4 语义分析与字节码生成

1. 标注检查

标注检查的内容包括诸如变量使用前是否已被声明、变量与赋值之间的数据类型是否能够匹配等。在标注检查中,还有一个重要的动作称为常量折叠 ------代码中写a = 1 + 2,在语法树上仍能看到字面量1和2,但经过常量折叠后,将会被折叠为字面量3。因此代码里定义a = 1 + 2比起直接定义a = 3,并不会增加程序运行期任何一个CPU指令的运算量。

2. 数据及控制流分析

数据及控制流分析是对程序上下文逻辑更进一步的验证,可以检查出诸如程序局部变量在使用前是否有赋值、方法的每条路径是否都有返回值、是否所有的受查异常都被正确处理等问题。

3. 解语法糖

语法糖是指在计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但更方便程序员使用。Java虚拟机运行时并不支持这些语法,它们在编译阶段会被还原回简单的基础语法结构,这个过程就是解语法糖。Java中的主要语法糖包括泛型、可变参数、自动装箱/拆箱等。

4. 字节码生成

字节码生成是Javac编译过程的最后一个阶段,不仅仅把前面各个步骤所生成的信息转化成字节码写到磁盘中,编译器还进行了代码添加和转化工作。例如实例构造器<init>()方法和类构造器<clinit>()方法就是在这个阶段添加到语法树中的。编译器会把语句块、变量初始化、调用父类的实例构造器等操作收敛到<init>()<clinit>()方法中,并保证按先执行父类实例构造器、然后初始化变量、最后执行语句块的顺序。

10.3 Java语法糖的味道

10.3.1 泛型

泛型的本质是参数化类型的应用,即所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口和泛型方法。

类型擦除机制

Java语言中的泛型采用类型擦除 的方式实现。用大白话讲,这个泛型只存在源码中,编译器将源码编译成字节码之时,就会把泛型"擦除",所以字节码中并不存在泛型。例如,ArrayList<Integer>ArrayList<String>在编译之后,都变成了同一个原生类型ArrayList。因此,对于运行期的Java语言来说,ArrayList<Integer>ArrayList<String>就是同一个类。

类型擦除的具体规则是:类型变量会被擦除,并替换为其限定类型------如果类型参数是无限制通配符或没有上下界限定,则替换为Object;如果存在上下界限定,则根据子类替换原则取类型参数的最左边限定类型(即父类)。

为什么选择类型擦除?

Java泛型是从JDK 1.5才开始加入的特性,在此之前Java并没有泛型。选择类型擦除方案的主要目的是为了兼容以前的版本------这种方案只需改动编译器,Java虚拟机和字节码指令完全不需要改变,存量代码可以无缝升级。

类型擦除的配套措施

仅仅擦除泛型信息还不够,编译器还需要在擦除后插入额外的代码来保证程序正确性:

  • 插入强制类型转换 :当从泛型集合中取出元素时,编译器自动插入强制类型转换代码。例如String s = list.get(0)在编译后会被转换成String s = (String) list.get(0)
  • 自动产生桥接方法:当子类重写父类的泛型方法时,编译器会自动生成桥接方法来保证多态性。

泛型的局限与取舍

类型擦除式泛型与C#的具现化式泛型相比,存在明显局限:性能较低(需要强制类型转换和自动装箱拆箱)、不支持基本数据类型作为泛型参数、运行期无法获取泛型类型信息等。但Java选择这条路径是出于对当时语言现状的务实权衡------保证平滑升级的优先级高于设计一个完美的泛型系统。

10.3.2 自动装箱、拆箱与遍历循环

自动装箱与拆箱

自动装箱和自动拆箱是一种语法糖,实现了基本数据类型与包装类型之间的自动转换。装箱就是自动将基本数据类型转换为包装器类型,拆箱就是自动将包装器类型转换为基本数据类型。

java 复制代码
Integer a = 1;      // 自动装箱,编译后相当于 Integer.valueOf(1)
int b = a;          // 自动拆箱,编译后相当于 a.intValue()

这一语法糖的实现原理是编译器在编译阶段将装箱和拆箱操作替换为对应的包装和还原方法调用。自动装箱/拆箱极大地简化了基本类型与包装类混用的代码,但也带来了潜在的性能问题------频繁的装箱拆箱会产生大量临时对象,增加GC压力。

遍历循环(增强for循环)

增强for循环是Java 5引入的语法糖,在编译之后被还原成了迭代器的实现,这也是为什么遍历循环需要被遍历的类实现Iterable接口的原因。

java 复制代码
for (Integer i : list) {
    System.out.println(i);
}
// 编译后等价于
for (Iterator<Integer> it = list.iterator(); it.hasNext(); ) {
    Integer i = it.next();
    System.out.println(i);
}

10.3.3 条件编译

Java语言中并没有预处理器,所以无法像C/C++那样通过#ifdef等宏定义来实现条件编译。但Java也可以通过特定的编码方式,实现条件编译的效果:

java 复制代码
if (true) {
    System.out.println("true");
} else {
    System.out.println("false");  // 编译器提示:Dead Code
}

这段代码中的else分支会被编译器在编译期直接优化掉------因为if的条件是编译期常量true,编译器可以确定else分支永远不会被执行。Java编译器在解语法糖阶段会对这种不可达代码进行消除处理,从而实现类似于条件编译的效果。这种优化同样适用于while(false)等场景。

10.3.4 其他常见语法糖

Java中的语法糖还包括但不限于以下内容:

语法糖 说明 编译后等价形式
变长参数 void method(String... args) 数组参数
内部类 定义在类内部的类 独立的class文件(带$分隔符)
枚举类 enum SEX { MAN, WOMAN } 继承Enum的final类
断言语句 assert flag; if条件+抛出AssertionError
switch支持字符串 switch(str) {...} 先计算hashCodeequals比较
try-with-resources try (Resource r = ...) {...} 自动在finally中调用close()

10.4 实战:插入式注解处理器(本章略过)

书中10.4节以"NameCheckProcessor"为例,演示了如何通过插入式注解处理器在编译期检查代码是否符合命名规范。这个案例完整展示了Javac注解处理API的使用方法,包括如何继承AbstractProcessor、如何声明支持的注解类型和源代码版本、如何通过Messager输出编译信息等。

时间精力因素,对于实战代码没整,大家感兴趣可以看看原书10.4节或查阅JSR 269官方文档


如果对你有帮助,请点赞,关注,收藏。感谢,共勉,祝好!

相关推荐
大大杰哥2 小时前
力扣hot100笔记(1)
笔记·leetcode
qq_334563552 小时前
MySQL如何实现数据库审计日志记录_开启通用日志与插件审计
jvm·数据库·python
Sss_Ass2 小时前
跟着老师不迷路系列---跟着李述铜老师学习汇编语言之内核寄存器简介
学习·学习方法·汇编语言·李述铜
疯狂成瘾者2 小时前
SLF4J的学习路线
java·学习·slf4j
雾岛听蓝2 小时前
Qt按钮与标签控件详解
开发语言·经验分享·笔记·qt
m0_640309302 小时前
如何大幅提升 Google Sheets 数据库更新脚本的执行效率
jvm·数据库·python
Greyson12 小时前
CSS如何实现单选按钮自定义样式_利用伪元素隐藏默认UI
jvm·数据库·python
2401_835956812 小时前
Go语言怎么防SQL注入_Go语言SQL注入防护教程【深入】
jvm·数据库·python
m0_514520572 小时前
宝塔面板怎样实现数据库的多地异地自动备份_结合阿里云OSS与定时任务插件
jvm·数据库·python