编译
- 前端编译器: JDK的Javac 、EclipseJDT中的增量式编译器(ECJ)
- 即时编译器:HotSpot虚拟机的C1\c2编译器、Graal编译器
- 提前编译器:JDK的Jaotc、GNU Compiler for Java, Excelsisor JET.
前端编译器
编译过程分为1个准备过程和3个处理过程:
1)初始化插入式注解处理器
2)解析与填充符号表
语法、词法分析
词法分析是将源代码的字符流转变为标记集合的过程,单个字符是程序编写的最小元素,但标记才是编译时的最小元素。关键字、变量名、字面量、运算符都可以作为标记。语法分析时根据标记序列构造抽象语法树的过程,抽象语法树是用来描述程序代码语法结构的树形表示方式,抽象语法树的每一个节点都代表程序代码中的一个语法结构。例如包、类型、修饰符、运算符、接口、返回值、代码注释等。
经过词法和语法分析生成语法树以后,编译器后续的操作都建立在抽象语法树之上。
填充符号表
符号表是一组符号地址和符号信息构成的数据结构,符号表中所登记的信息在编译的不同阶段都要被用到。如,在语义分析过程中,符号表所登记的内容将用于语义检查和产生中间代码,在目标代码生成阶段,对符号名进行地址分配时,符号表是地址解析分配的直接依据。
3)注解处理器
插入式注解处理器可以提前至编译期对代码中的特定注解进行处理,从而影响到前端编译器(javac)的工作过程。
如果在处理注解期间对语法树进行过修改,编译器将回到解析以及填充符号表的过程重新处理,知道所有插入式注解处理器都没有再对语法树进行修改位置,每次循环称为一个轮次。
4)语义分析与字节码生成
语义分析过程分为标注检查和数据以及控制流分析两个步骤。
在经过语法分析后,编译器获得了程序代码的抽象语法树表示,抽象语法树能够表示一个结构正确的源程序,但无法保证源程序的语义是符合逻辑的。而语义分析的主要任务则是对结构上正确的源程序进行上下文相关性质的检查,譬如进行类型检查、控制流检查、数据流检查等等。
标注检查
检查的内容包括变量是用钱是否已经被声明、变量与赋值之间的数据类型是否能够匹配等等。
常量折叠:int a = 1 + 1 ; ==> int a = 2;数据流以及控制流分析
数据流分析以及控制流分析是对程序上下文逻辑更进一步的验证,它可以检查出诸如程序局部变量在使用前是否有赋值、方法的每条路径是否都有返回值、是否所有的受检查异常都被正确处理了等问题。
编译器的数据以及控制流分析与类加载时的数据以及控制流分析的目的基本哈桑一致,但是校验范围有所区别。解语法糖
Java语言的语法糖包含泛型、变长参数、自动装箱拆箱等,Java虚拟机在运行时并不知节支持这些语法,它们在编译阶段被还原回原始的基础语法结构,这个过程称为解语法糖。字节码生成
字节码生成阶段需要把前面哥哥步骤生成的信息转化成字节码指令写到磁盘中外,还要进行少量的代码添加和转换工作。
如实例构造器<init>()方法和<cinit>()方法的添加。
如果用户代码中没有提供任何构造函数,那么编译器将会添加一个没有参数的、可访问性与当前类型一致的默认构造函数,这个工作在填充符号表阶段已经完成。编译器会把语句块、变量变量初始化、调用父类的实例构造器方等操作收敛到<init>()和<cinit>()方法中,并且保证无论源码中出现的顺序如何,都一定是按先执行父类的实例构造器,然后初始化变量,最后执行语句块的顺序执行,
语法糖
泛型、自动装箱、拆箱、遍历循环、变长参数和条件编译、内部类、枚举类、断言、数值直面量、枚举和字符串的switch、try with resource, lambda等。
泛型
类型擦除
自动装箱、拆箱和循环遍历
1)大于127的值不会自动拆箱
2)没有遇到运算符的情况下不会自动拆箱
3)equals方法不处理数据转型
变长参数
条件编译
Java编译器将所有编译但愿的语法树顶级节点输入到待处理列表后再进行编译,因此各个文件之间可以互相提供符号信息。
根据条件中布尔值常量值的真假,编译器将把分支中不成立的代码块消除掉。
后端编译
当程序需要迅速启动和执行的时候,解释器可以首先发挥作用,省去编译的时间,立即运行。
当程序运行环境中内存资源限制较大,可以使用解释执行节约内存。
当程序启动以后,编译器逐渐发挥作用,把越来越多的代码编译成本地代码,减少解释器的中间损耗,获得更高的效率。
即时编译器
即时编译器将运行比较频繁的代码块或者方法编译成本地机器吗,并且对这段代码进行优化。
在Hotspot中内置了2个即时编译器 Client Compiler、 Server Compiler。 虚拟机会根据自身版本与宿主机的硬件性能自动选择运行模式,也可以使用-client 或者 -Server 来强制指定虚拟机运行在客户端模式或者服务端模式。
解释模式(Interperted Mode):-Xint
编译模式(Compiled Mode): -Xcomp
混合模式(Mixed Mode): -Xmixed (默认)
编译器编译本地代码需要占用程序运行时间,位了打到响应速度与运行效率之间打到最佳平衡,HotSpot在编译子系统中加入了分层编译的功能。分层编译根据编译器编译、优化的规模与耗时,划分出不同的编译层次:
第0层,程序春解释执行,并且解释器不开启性能监控。
第一层:使用客户端编译器将字节码编译为本地代码,进行简单可靠的稳定优化,不开启性能监控功能。
第二层:使用客户端编译器执行,仅开启方法以及回边次数统计等优先的性能监控功能。
第三层:使用客户端编译器执行,开启全部性能监控,除了第2层的痛就之外,还会收集如分支跳转、虚方法调用版本等全部的统计信息。
第四层:使用服务端编译器将字节码编译为本地代码,服务端编译器会启用更多编译耗时更长的优化,还会根据性能监控信息进行一些不可靠的激进优化。
实施分层编译后, 解释器、客户端编译器和服务端编译器就会同时工作,热点代码都可能会被多次编译,用客户端编译器获得更高的编译速度,用服务端编译器来获取更好的编译质量,在解释执行的时候也无需额外承担手机性能监控信息的任务,而在服务端编译器采用高复杂度的优化算法时, 客户端编译器可先采用简单优化来为它争取更多的编译时间。
编译对象与触发条件
被编译的热点代码有:被多次调用的方法和被多次执行的循环体。这两种情况编译的目标对象都是整个方法体,而不是单独的循环体。
基于采样的热点探测
周期性的检查线程的调用栈顶,如果发现某个方法经常出现在栈顶,那么这个方法就是热点方法。
优点:简单高效,容易获取方法的调用关系
缺点:不能精确确认方法的忍睹,容易受到线程阻塞或者外界因素影响扰乱热点探测。
基于计数器的热点探测
为每个方法建立计数器,统计方法的执行次数,如果超过一定的阈值就认为它是热点方法。
优点:结果精确严谨
缺点:每个方法建立并维护计数器,不能直接获取到方法的调用关系。
HotSpot为每个方法准备了两类计数器:方法调用计数器和回边计数器(在循环边界往回跳),当调用次数超过设定的阈值,就会触发即时编译。
-XX:CompileThreshold 来设定触发即时编译的阈值。
默认设置下,方法调用计数器统计的是相对执行瓶绿,当一个时间段内调用次数不足以提交即时编时,这个方法的调用计数器就会被减半(衰减),可以使用-XX:-UseCounterDecay关闭热度衰减。可以设置-XX:CounterHalfLifeTime设置半衰周期的时间,单位是秒。
当方法被调用时,首先检查该方法是否被即使编译器编译过,是则优先使用编译后的版本执行,否则将方法调用的计数器加一,然后判断方法调用计数器与回边计数器值之和是否超过阈值,超过则像即时编译器提交该方法的代码编译请求。
如果没有做过任何设置,执行引擎默认不会同步等待编译请求完成,而是继续进入解释器按照解释方式执行字节码,直到提交的请求被即时编译器编译完成。当编译工作完成后,这个方法的调用入口地址会被系统自动改写成新值,下次调用该方法时就会使用已编译版本。
回边计数器:统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令称为“回边”,回边计数器的目的是为了触发栈上的替换编译。回边计数器没有热度衰减过程。
编译过程
一般情况下,方法调用产生的标准编译请求和栈上替换编译请求,在虚拟机编译器未完成编译之前,都将按照解释方式执行代码,编译动作在后台的编译线程完成。
设置-XX:-BackgroundCompilation禁止后台编译,禁止后触发即时编译条件时,执行线程向虚拟机提交编译请求后一致阻塞等待,直到编译过程完成后再开始执行编译器输出的本地代码。
服务端编译器效率较低,但是编译速度远超传统的静态优化编译器,相对于客户端编译器编译输出的代码质量有很大的提交,可以大幅降低本地代码的执行时间,从而抵消掉额外的编译水间开销。
即时编译的缺点
1)占用程序运行时间
2)占用程序运算资源
提前编译器
提前编译器的分支:
与传统C、C++编译器类似,在程序运行之前把程序代码编译成机器码的静态翻译工作;
把原本即时编译器在运行时要做的编译工作提前做好并保存下来,下次运行到这些代码时直接加载进来使用。
编译器优化技术
方法内联
最重要的优化技术之一。
逃逸分析
最前沿的优化技术之一。
栈上分配
支持方法逃逸,但不支持线程逃逸标量替换
如果一个数据已经无法在分解为更小的数据表示时(原始数据类型),这些数据就可以称为标量。
标量替换就是将用到的成员变量恢复为原始类型来访问的过程。
加入逃逸分析可以证明一个对象不会被方法外部访问,并且这个对象可以被拆散,那么程序真正执行的时候可能不去创建这个对象,而改为直接创建它的若干个被这个的方法使用的成员变量来替代,成员变量存储在栈上。同步消除:如果逃逸分析能够确定一个变量不会逃逸出线程,无法被其他线程访问,那么这个变量的读写肯定不会有竞争,对这个变量的同步措施也可以安全地消除掉。
公共子表达式消除
语言无关的经典优化技术。如果一个表达式E之前已经被计算过,并且从先前的计算到现在E中所有变量的值都没有发生变化,那么E的这次出现就称为公共子表达式。
数组边界检查消除
语言相关的经典优化技术。