从计算机程序出现的第一天起,对效率的追求就是程序天生的坚定信仰,这个过程犹如一场没有终点、永不停歇的F1方程式竞赛,程序员是车手,技术平台则是在赛道上飞驰的赛车。
- Java代码的三种编译过程
- 前端编译:将*.java文件转变为 *.class文件,如Javac
- JIT编译:将*.class文件转变为转变为机器码,如HotSpot的C1,C2编译器
- AOT编译:直接把*.java文件编译为本地机器代码,如GCJ编译器
- Javac的编译过程(深入学习可以去读Javac的源码)
- 解析和填充符号表
- 词法、语法分析:词法分析是将源码字符流转变为Token集合,Token是编译过程的最小元素如关键字,变量名,字面量,运算符等;语法分析是根据Token集合来构造抽象语法树。(这里涉及编译原理的知识)
- 填充符号表:符号表在后续处理分析过程中需要使用,类似于key,value的结构。
- 注解处理:源代码中写的注解就在此时进行处理,通过插入式注解处理器API对源代码里的注解进行处理,可能会对抽象语法树进行修改,如果修改了,就要重新进行一次解析和填充符号表,这个过程称为一个round,直到没有对抽象语法树有修改才进入下一阶段。
- 语义分析和字节码生成
- 标注检查:检查赋值和变量类型是否匹配,此时有一个重要的动作,常量折叠。
- 数据及控制流分析:分析代码的基本语法逻辑是否合法,对于局部变量final修饰的保证就是在此时检测的,运行时不会检测
- 解语法糖:Java里的语法糖需要在此时替换为实际的代码实现,因为虚拟机是不认语法糖的
- 字节码生成:这部分主要是将前面生成的语法树,符号表写入磁盘,增加默认构造函数是在此阶段完成的。
- 解析和填充符号表
- Java常见语法糖
- 擦除式泛型:Java的泛型符只存在于源码,编译之后的字节码被插入了强制类型转换。但是擦除,仅仅是对Code属性中的字节码进行了擦除,元数据还是有保留泛型信息,这也是为什么能通过反射取得参数化类型的根本依据。
- 自动装箱,拆箱:用valueOf函数和intValue函数实现
- Foreach循环:使用迭代器实现,所以要求被遍历的类必须要实现Iterable接口。
- 变长参数:用数组实现
- 热点代码有哪些
- 被多次调用的方法体
- 被多次执行的循环体(JIT编译的仍然是这个循环的整个方法体,而不是这个循环)
- 如何判定一个代码是热点代码
- 基于采样的热点探测:周期性的检查线程栈顶,发现一个方法出现的频率很高就认为是热点代码
- 基于计数器的热点探测:为代码块建立一个计数器,当计数值突破阈值的时候就认为是热点代码,触发JIT编译。主要有两种计数器分别是方法调用计数器和回边计数器。方法调用计数器存在热度衰减。
- JIT编译优化技术
- 方法内联
- 冗余消除访问
- 复写传播
- 无用代码消除
- 公共子表达式消除
- 数组边界检查消除:采用提前到编译器检查或者隐式异常处理(会进入内核态)实现
- 逃逸分析:栈上分配,同步消除,标量替换等
这部分主要讲了Java代码如何从*.java文件一步步编译称为*.class文件,不同的编译方式下进行的优化,以及不同的执行方式。