概述
程序编译与代码优化
- 前言
- Javac编译器
- 语义分析与字节码生成
- 标注检查
- 数据及控制流分析
- 解语法糖
- 字节码生成
- 后端编译与优化
- 及时编译器
- 编译对象和触发条件
- 编译过程
- 编译器优化技术
- 方法内联
- 逃逸分析
- 公共子表达式消除
- 数组边界检查消除
- 后记
前言
Java中的编译主要分为3个部分:
- 前段编译:把.java文件转化为.class文件的过程。
- 即时编译:把字节码转化为本地机器码的过程。
- 提前编译:把字节码转化为与目标及其指令集相关的二进制代码的过程。
如果单指对代码运行效率的“优化”,那么在前端编译的Javac中几乎没有任何优化措施可言,虚拟机中全部的优化措施都集中在运行期的即时编译中,这样可以让不是Javac产生的Class文件同样得到性能优化。
但是如果指开发阶段的“优化”,Javac也有许多优化措施能够降低程序员的编码复杂度、提高编码效率。比如说许多Java的新特性都是通过语法糖来实现的。
Javac编译器
从Javac的代码总体结构看,前端编译大致可以分为1个准备阶段和3个处理阶段:
- 准备过程:初始化插入式注解器。
- 解析与填充符号表过程,包括:
- 词法语法分析。将源代码的字符流转变为标记集合,构造出抽象语法树。
- 填充 符号表。产生符号地址和符号信息。
- 插入式注解处理器的处理过程。
- 分析与字节码生成,包括:
- 标注检查。对语法静态信息的检查。
- 数据流及控制流分析。对程序动态运行信息的检查。
- 解语法糖。
- 生成字节码。
在上述过程中注解处理过程中总会生成新的符号,所以又必须回到之前的解析过程。
语义分析与字节码生成
我们编码时候能够在IDE中看到出错的代码被标注了红线,其绝大部分都是来源于语义分析阶段的检查结果。语义分析过程主要可以分为标注检查和数据及控制流分析两个步骤。
标注检查
主要检查诸如变量使用前是否声明、变量与赋值之间的数据类型是否能够匹配等等。
在标注检查中还会进行一个常量折叠的优化操作,例如:int a = 1 + 2
,经过常量折叠之后,1+2会被直接优化成3,这也是为数不多的前端优化之一。
数据及控制流分析
这一部分主要是对代码流程的分析,比如局部变量在赋值之前是否被声明、方法的每条路径是否具有返回值、是否所有的受常异常都被正确处理了之类。
解语法糖
语法糖是指在计算机中添加某种语法,这种语法对编译过程和结果没有任何影响,但是能够方便程序员使用该语言。
在Java中比较常用的语法糖包括:泛型、变长参数、自动装箱拆箱 等。
解语法糖就是指在前端编译的过程中把语法糖进行还原。
字节码生成
字节码生成阶段不仅仅是把前面各个步骤生成的信息转成字节码指令存放到磁盘上,编译器还进行了少量的代码添加和转换工作。
例如添加实例构造器和类构造器。
后端编译与优化
后端编译指的是把Class文件转换成与本地基础设施相关的二进制机器码。以下内容,指的是在HotSpot虚拟机中的编译器。
及时编译器
在HotSpot中,Java程序最开始都是使用解释器解释执行的,在运行一段时间之后,虚拟机会发现某些方法或者代码块的运行非常频繁,这部分代码被称为“热点代码”,为了提高热点代码的运行效率,虚拟机将会把这些代码编译成本地机器代码,并且使用各种手段进行优化,完成这个任务的编译器被称为即时编译器。
解释器除了在最开始的时候能够帮助程序快速运行之外,在后面的过程中还可以帮助编译器收集代码运行的各种信息,也可以在编译器激进优化出错的时候充当逃生门。
在HotSpot虚拟机中采用了两个(或三个)即时编译器,其中前两个存在已久,被称为“客户端编译器”(C1)和“服务端编译器”(C2),第三个编译器是Graal编译器,目标是为了取代C2编译器,目前还在试验阶段。
编译对象和触发条件
前面提到运行过程中即时编译器的目标是“热点代码”,这里的热点代码主要包括两类:
- 多次被调用的方法
- 多次执行的循环体、
那么虚拟机如何判断热点代码呢?主要有两个方法:
- 基于采样的热点探测:定期的检查各个线程的调用栈顶,如果发现某个方法经常出现在栈顶,那么该方法就是热点代码。
- 基于计数器的热点探测:为每个方法建立计数器,统计方法的执行次数,一旦超过某个阈值,那么该代码就是热点代码。
在HotSpot中采用的主要是第二种方法,虚拟机为每个方法准备了两类计数器:方法调用计数器和回边计数器。
方法调用计数器的次数并非是绝对的,当超过一定时间限度,次数仍然不足以提交给即时编译器,那么该方法的调用计数器将会减少一半,这个过程被称为方法调用计数器的热度衰减。而回边计数器的调用次数则是绝对的。
编译过程
那么编译器是如何进行编译的呢?
对于客户端编译器来说,主要分为三个阶段。
第一个阶段会将字节码转换成一种高级中间代码表示(HIR),在这个阶段也会对字节码进行方法内联、常量传播等优化。
第二个阶段将会把HIR转变成低级中间代码表示(LIR),这一步将在HIR中完成空值检查消除、范围检查消除等优化。
最后将在LIR上分配寄存器,并在LIR上做窥孔优化,最后产生机器码。
而服务端编译器则更加复杂,会进行大部分优化动作,还可以根据客户端编译器和解释器提供的信息做出一些激进优化。
编译器优化技术
由于优化技术多种多样,这边只介绍其中比较具有代表性的4个:
- 最重要的优化技术之一:方法内联
- 最前沿的优化技术之一:逃逸分析
- 语言无关的经典优化技术之一:公共子表达式消除
- 语言相关的经典优化技术之一:数组边界检查消除。
方法内联
方法内联说白了就是把调用方法的代码“复制”到调用的地方。
但是在Java中实现也并非是这么简单,虚拟机会首先进行类型继承关系分析,如果不是虚函数,则直接进行内联就行了,但是如果是虚函数,则需要查看是否有多个版本,如果有只有一个版本则进行守护内联,但是如果有多个版本,则会进行内联缓存,在第一次调用的时候缓存下方法和版本号,如果下一次版本号一致,则使用缓存中的方法。
逃逸分析
逃逸分析的原理就是指对对象作用域的分析,根据对象作用域的不同进行不同的优化操作。
- 栈上分配:如果一个对象没有逃逸出线程,那么可以直接在栈上分配对象,对象将随着栈帧出栈而消亡。
- 标量替换:如果一个对象没有逃逸出方法,可以不创建这个对象,而是把对象拆分为各个成员变量。
- 同步消除:如果一个对象没有逃出线程,也就不会被其他线程访问,那么关于这个对象的同步措施可以消除掉。
公共子表达式消除
如果一个表达式E之前已经被计算过了,并且E中的值没有发生任何变化,那么E就被称为公共子表达式,程序将不会再次计算表达式E,而是采用之前的计算结果。
数组边界检查消除
Java中访问数组元素的时候会自动进行上下界的安全检查,但是如果一个数组下标识常量,那么在编译期根据数据流分析的结果来确定常量是否越界就行了,不需要每次都进行越界检查。
后记
《深入理解Java虚拟机》的学习笔记到此告一段落。
昨天和上一届的师兄聊天,师兄表示面试的时候提问到并发和虚拟机的部分其实并不是很多,大部分内容都集中在Java基础和算法的部分,所以接下来几篇都会针对Java基础部分来写,根据内推军面经提纲中的知识点进行逐一学习。
最后
以上就是尊敬眼睛为你收集整理的深入理解JVM(程序编译与代码优化篇)前言Javac编译器后端编译与优化后记的全部内容,希望文章能够帮你解决深入理解JVM(程序编译与代码优化篇)前言Javac编译器后端编译与优化后记所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复