《深入理解JVM》第11章后端编译与优化——提前编译器与后端优化

提前编译器

优劣得失

由于即时编译不可避免的会占用一些本该属于程序运行的时间。所以这就使得的提前编译有了存在的必要性,不过提前编译就失去了原来的平台性中立性,动态拓展等优势,不过为了性能倒也是值得的(还是得看应用场景)。
实现提前编译有两个方向:

  • 将程序代码编译成直接机器码存于本地(类似C/C++)。(比如安卓里的ART,不过由于会占使得启动变慢,所以在Android7.0之后重新启用解释器与即时编译器,在系统空闲时后台自动进行提前编译)。
  • 把原本即时编译器在运行时要做的编译工作提前做好并保存(即时编译缓存)。这样节约了不少运行时成本,不过实现起来确是有点困难的,因为这种提前编译不仅与目标机器绑定海域虚拟机对应的参数绑定。

不过相比于提前编译器,即时编译器也有它的优势(要不然肯定就被全面取代了)。

  • 性能分析制导优化: 根据不断收集的性能监控信息,可以根据当下实际情况应该如何分配资源或者热点代码集中优化等。
  • 激进预测性优化:会根据监控的信息进行一些基于高概率假设的优化,如果出现错误(走到某个罕见的分支)了再退回解释器或者低程度的编译器,而静态优化需要保证所有程序可见外部影响和优化前一致。对于激进优化,只要正确概率足够高,那么就会大幅度降低目标程序的复杂度。比如方法的内联,Java方法是默认使用的虚方法,而每一个都查虚方法表,那是很慢的,所以需要对方法进行一些内联,激进优化就会通过猜测去做去虚拟化。
  • 链接时优化: Java天生是动态链接的,所以在链接时优化是很有用途的。

编译器优化技术

  1. 方法内联:将组合方法编译到一起,1.取出调用成本,2.为其他优化建立良好的基础。
  2. 进行冗余访问消除,比如y=b.value,x = b.value 就会优化为y = b.value x=y,这样就可以减少去访问b.value了。
  3. 复写传播:用具有相同值的引用去代替另一个引用。
  4. 无用代码消除:消除无意义的代码,如y = y。
内联

内联被称为优化之母 (话说为啥叫优化之妈就会显得奇怪呢) ,没有内联,多数其他优化都无法有效进行。是一种激进优化
不过由于Java默认是虚方法,而只有非虚方法(除了用invokevirtual修饰调用的方法都是非虚方法(final也是非虚方法))才在编译器确定,这就为方法内联带来了一定的困难.
Java引入一种被称为类型继承关系分析的技术,用于确定在目前已加载的类中,是否使用了多态,如果没使用(非虚方法),则直接内联,
如果使用了,且只有一种方案可供选择,则假设当前即为全貌,进行守护内联,如果后期动态改变了,则使用逃生门进行解释运行或者重新编译。
如果有多个版本可供选择,则会使用内联缓存 ,建立在正常方法入口前的一个缓存,调用前该缓存为空,第一次调用后便会把相关信息进行缓存,下次调用时便会先进行比对判断,比较接收者的版本,版本一致时,被称为单态内联缓存,就比非虚方法的内联多了一步判断,如果版本信息与接收者不一致的情况,就说明使用了多态性,则退化成超多态内联缓存

逃逸分析

俺记得《并发编程实战》里有讲过这个问题,提前发布,也叫逸出,这时会出现类型还未成功初始化的情况,而引发一些意想不到的情况,当时书中的例子(在构造方法中将this引用提前发布),这算线程逃逸了吧。不过不是说线程逃逸就是不好的,还是视情况而定
逃逸分析的基本原理:分析对象动态作用域,当一个对象在方法里被定义后,它可能被外部方法所引用。
逃逸从程度可分为,1.从不逃逸,2.方法逃逸(调用参数传递到其他作用域),3.线程逃逸(赋值给可以被其他线程可以访问的实例变量)。

方法逃逸这个,倒是经常使用,比如将数组传入一个方法里,对数组进行修改,又想获取一些其他信息这种情况。线程逃逸的话,有时用一个线程对某变量进行改写后给某线程读取,这也算线程逃逸

编译器会根据不同的逃逸程度来划分优化措施。
如果逃逸程度较低:

  • 栈上分配(HotSpot没有实现): (之前在一个群里听说哪个银行面试问过这个问题,当时群里开启了群嘲模式,居然还真特么有):如果不会出现线程逃逸,那么这样可以减轻垃圾回收的压力,随着栈帧出栈而销毁对象。
  • 标量替换: 特殊的栈上分配,它将对象拆分为一些标量(无法再拆分的量,比如基本类型),这样不仅可以让这些属性入栈(栈上数据更可能被分配到高速运行寄存器上),而且还能进行其他优化,不过逃逸要求更高,不能逃逸出此方法。
  • 同步消除: 如果无法逃逸出线程,那么肯定就不会出现竞争,那么一些同步措施可能被消除掉。

不过逃逸分析,现在尚未成熟,因为需要计算成本比较高。目前虚拟机只采用不那么精准,但是时间压力没那么大的算法进行分析。

可使用-XX+DoEscapeAnalysis来手动开启逃逸分析(JDK7时的服务端已成默认状态),开启后可通过-XX:+PrintEscapeAnalysisksis查看分析结果,还可使用-XX:+EliminateAllocations来开启标量替换,还可使用-XX:+EliminateLocks来开启同步消除,使用-XX:+PrintEliminateAllocations来查看标量替换的情况。

公共子表达式消除

就是即时编译时会将一些重复计算的表达式进行替代。

数组边界检查消除

如果数组索引为一个常量,在编译期间通过数据流分析发现不会越界,那么就会取消隐藏的判断(记得被ArrayIndexOutOfBoundsException支配的恐惧吗),如果在循环中使用数组,就会判断如果取值范围在允许的范围内,那么也不会每次都进行判断。
因为Java会有虚拟机层面抛出的异常,这些都是隐式的判断的,如果每次都执行这个判断,那就比较慢了,而且大多数(正常)情况都不应该抛异常的,所以即时编译器便提供了一种隐式异常处理的机制,在进程层面用一个"try-catch"语句,没触发异常时,无需判断,触发异常后,就需要转到异常处理器中恢复中断并抛出异常,这涉及到进程从用户态转到核心态,再转会用户态的过程,开销可就比直接判断大多了。
不过由于有运行期收集到的性能监控信息,所以会自动选择合适的方式。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值