深入理解java虚拟机—编译

java的编译器有三种,有前端编译器:就是前期将java文件编译成class文件的过程;还有后端编译器:就是在运行时期将字节码转变为机器码的过程;还有可能是静态提前编译器:直接把java文件编译成本地机器代码。

前端编译

javac

javac是一种前端编译器,会将java代码编译为class文件,javac是用java编写的程序。
它的编译过程可分为下列三步:

  • 解析与填充符号表过程。
    解析可分为词法分析和语法分析。
    ①词法、语法分析
    词法分析是将代码的字符流转变为标记集合(例如int虽然是三个字符,但它是个关键字,则转换成一个标记),语法分析是根据标记集合构造出抽象语法树。
    ②填充符号表
    符号表是由符号地址和符号信息构成的。
  • 插入式注解处理器的注解处理过程。
    注解处理器对注解进行处理时可以读取、修改、添加抽象语法树,这时候就会重新回到解析及符号填充的部分,进行循环处理注解,直到处理完成。
  • 分析与字节码生成过程。
    进行语义分析,保证程序符合逻辑。
    ①标注检查
    检查变量使用前是否被声明、变量与赋值之间数据类型是否匹配等;还有常量折叠优化。
    ②数据及控制流分析
    对上下文逻辑进行验证。
    ③解语法糖
    所谓的语法糖就是在编程过程中方便程序员去使用的一些语法。在这个阶段会对这些语法糖进行解析。
    ④字节码生成
    字节码生成是将之前生成的语法树和符号表转化为字节码写到磁盘中,同时编译器会增加一些代码例如构造器方法,以及一些程序优化。最后输出字节码。

前端编译器小结

前端编译器就是将Java代码编译成class字节码的过程,通过了解析生成抽象语法树、符号表填充生成符号表、处理注解、最后进行语义分析验证、生成字节码。

后端编译

在HotSpot中,java在运行期的方式一部分代码通过解释器解释执行,一部分执行频繁的代码通过即时编译器编译成本地机器码执行。当然不是所有的虚拟机都采用这种解释器和编译器并存的架构。

解释器和编译器

解释器可以省去编译时间,立即执行,运行时节约内存;
编译器执行效率更高。
HotSpot默认采用一个解释器搭配一个即时编译器的结构(当然可以用“-Xint”参数使虚拟机运行在解释模式,编译器完全不介入工作或者用“-Xcomp”参数使虚拟机运行在编译模式,优先采用编译,解释器会在编译无法进行时介入),在HotSpot中有两个即时编译器:一个是Client Compiler和Server Compiler。使用哪个编译器看虚拟机的运行模式,是client还是server。
编译器编译出优化程度高的代码需要花的时间长,解释器同时还会替编译器收集性能监控信息,对解释执行的速度也有有影响。为了解释和编译达到平衡,HotSpot逐渐启用分层编译策略。jdk1.7的server模式会采用分层编译策略,一下是分层策略的层次结构:
①第0层,程序解释执行,解释器不开启性能监控功能,可出发第一层编译。
②第1层,也称为C1编译,将字节码编译为本地代码,进行简单、可靠的优化,如有必要加入性能监控的逻辑。
③第2层,也称为C2编译,将字节码编译为本地代码,会启用一些编译耗时长的优化,甚至根据性能监控信息进行激进优化。
分层编译后,client compiler(获取更高编译速度)和server compiler(获取更好编译质量)会同时工作。

编译对象

被即时编译器编译的有两类对象:一类是多次被调用的方法;一类是多次被执行的循环体。 无论是多次被调用的方法还是多次被执行的循环体,即时编译器都是以整个方法作为编译对象。

触发条件

那么如何判断这一段代码是不是热点代码,需不需要进行即时编译,那么就要进行热点探测。
热点探测判定有如下两种方式:
①基于采样的热点探测:对栈顶的方法进行周期性采样。
②基于计数器的热点探测:为每个方法建立一个计数器,统计方法的执行次数。
在HotSpot虚拟机中采用第二种计数器的方法,每个方法有两个计数器:方法调用计数器,回边计数器。当计数器达到一定的阈值,就会触发即时编译。方法调用计数器在client模式下默认阈值是1500次,在server模式下默认是10000次。
调用方法时首先会查看时候存在已编译版本,如果存在,那么调用编译后的本地代码,如果不存在,那么方法调用计数器+1,然后继续采用解释执行。让方法调用计数器和回边计数器的阈值超过默认阈值后,就会向即时编译器提交编译请求。方法调用计数器中保存的是相对的执行频率即一段时间内方法调用次数,如果超过时间限度,那么该计数器会减半。
回边计数器是统计循环体执行次数,在字节码中遇到控制流向后跳转的执行叫“回边”。当每次遇到回边执行时,就会检查是否有编译好的版本,具体过程和方法调用时很像。对于循环体触发的编译,因为编译发生在方法执行过程中,被成为栈上替换(简称OSR编译)。
所以这两种情况引起的即时编译,都是先提交编译请求,编译还未结束前都是采用解释的方式执行,当编译结束后采用生成的本地代码,相当于这个编译过程是在后台执行的,当然也可以通过参数将设置为同步编译(当提交编译请求后不执行,然后等待,等编译结束后再执行本地代码)。
client compiler编译过程:
该编译器做的是简单快速的编译,关于局部性的优化。
第一阶段:将字节码构成高级中间代码;
第二阶段:在高级中间代码上完成一些优化,然后产生低级中间代码;
最后阶段:使用线性扫描算法在低级中间代码上分配寄存器,并在低级中间代码上做窥孔优化,然后产生机器代码。
sever compiler编译:
该编译器是面向服务端应用的编译器,会执行所有的经典优化动作,所以编译时间较长,但是生成代码在机器上运行效率高。

优化技术

①公共子表达式消除:
例如在a+b这个表达式在另一个运算式中被用到,例如d=(a+b)*c,那么在这个表达式中会用之前a+b的结果代替这个a+b的子式。
②数组边界检查消除:
java语言是一门安全的语言,在对数据进行操作时会进行数组边界的检查。但是对于大量的数组边界检查会引起效率问题,如果下标是个常量,那么在编译期进行检查数组边界,如果在边界内那么就消除数组边界的检查。
跟数组边界消除相关的还有一些消除技术,也是解决运行期间效率问题。
③方法内联:
方法内联是将目标代码的代码嵌入在调用对象的方法中来,避免了方法调用。不过有些方法是无法进行方法内联的,例如运行时期才能确定的方法。java中很多方法都是虚方法,这样的话方法内联岂不是不起作用。所以为了解决这个问题,引用了类型继承关系分析,如果检查到虚方法只有一个版本,那么可以进行内联。如果寄存关系改变,那么就要重新编译。这种内联属于激进优化,如果出现了小概率隐式异常,那么就会回到解释状态执行。
④逃逸分析:
当一个对象被方法定义后,可能被外部方法访问到,那么成为方法逃逸;被外部线程访问到,那么成为线程逃逸。如果能证明一个对象不会逃逸,那么能对这个对象进行高效的优化。例如下列这些优化:
– 栈上分配:在堆中回收整理对象都很耗时,那么如果一个对象不会逃逸到方法外,那么就进行栈上分配减少垃圾回收压力。
– 同步消除:线程同步操作是很耗时的,如果保证变量不会被其它线程访问到,那么就可以消除同步操作。
– 标量替换:标量指的是一个不能再分割的数据,例如int等这个基础类型的变量,对象则是一种聚合量,如果保证对象不会被外部访问到,且可以拆散,那么程序执行时则不创建该对象,替换成若干个标量,然后可以将这些标量分配在栈上。

后端编译小结

如果一个方法或一个循环体被热点探测判断为热点代码,那么在运行的过程中可能会被编译成本地机器代码。jdk1.7后的server模式下,采用分层策略,client comipler编译器和server comipler编译器一起工作,client compiler编译期编译简单快捷,server comipler编译期对代码优化更好,不过编译时间长。

    原文作者:java虚拟机
    原文地址: https://blog.csdn.net/wz1226864411/article/details/78441164
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞