JVM(十三):后端编译优化

JVM(一):源文件的转变 中我们介绍了 Java 中的前端优化,即将 Java 源代码转换为字节码文件。在本文中,我们将介绍字节码文件如何转换为本地机器码,并如何对代码进行优化,以提高性能。因为不同的虚拟机,字节码优化引擎不同,因此本文采用 JIT 来作为例子,其也是 HotSpot 中的默认编译器。

架构

我们都知道将代码转换为机器码有两种方式,而在 HotSpot 中采用了却两者全部都涉及到了,其采用了解释器和编译器并存的架构。那么其这样的目的是什么呢?

首先我们知道解释执行,可以大大提高程序启动时的效率,因为在这个时候需要执行什么代码,才对对应的源码进行翻译,将其变为机器码,因此也提高了启动时效率;

而编译执行的优点则是可以获得更高的执行效率,因为其将中间代码全部编译成了与机器相关的本地代码,并且在这一阶段,有些编译器还会对编译后的代码进行初步的优化,这也使得效率更加的优秀。

因此 Hotspot 开始执行的时候采用解释执行,获得优良的启动效率,而在代码执行过程中,对执行情况进行监控,运用以前所说的 热点代码编译技术 将热点代码编译成本地机器码,并根据执行情况进行优化,以获得两者全部的优点。

可能会有读者问道,我的代码部署在服务器上,第一次慢一点就慢一点,我只采用编译执行不行吗?

其实不然,首先因为编译器需要对代码进行优化,因此肯定是执行过程中根据执行情况进行优化更加的好,此外,在优化的过程中也有一种 激进优化 的方式,在这种情况下,就需要采用解释执行的方式通过 逆优化 的方式来退回到解释状态来执行了。

因此,两种方式并存的架构是合理且必要的,也因此目前主流的虚拟机也大多采取这种架构。

即时编译器

HotSpot 中的即时编译器有两种,分别称为 Client Complier 和 Server Complier,或者简称为 C1 和 C2,目前虚拟机一般采用解释器和一个即时编译器直接配合的方式来运行,这种模式称之为 混合模式

既然是两者合作,那么久需要考虑一个调度的问题,即何时使用编译执行,何时采用解释执行,多少的比例可以获得最佳平衡,得到最高的效率。

在 HotSpot 中是通过 分层编译 的策略来达到最优解的。其本质的思想如下所示:

  • 第0层:程序解释执行,解释器不开启性能监控,触发第一层;
  • 第1层:C1 编译,将字节码编译为本地代码,进行简单、可靠的优化,如果有必要,可以加入性能监控;
  • 第2层:C2 编译,也是将字节码编译为本地代码,但其会启动一些耗时较长的优化,甚至会根据监控的信息采取一些激进的优化措施。

这种分层编译的方式可以达到一定情况的最优解:用 C1 获取更快的编译速度,用 C2 获取更好的编译质量,解释执行的时候也无需增加性能监控的任务,反而拖累了启动效率。

编译对象

因为编译的过程是一个耗时耗力的工作,因此对那么频繁执行的代码进行编译能获得更高的提升。因此如何判断那么代码是 热点代码 呢?

在 JVM 中,热点代码的判断方式有两种:

  • 基于采样,周期性的检查栈顶,如果一段代码频繁出现在栈帧顶部,那么就判断其是热点代码。

    • 优点:实现简单,快;
    • 缺点:探测很容易收到线程阻塞的影响。例如一个方法因为线程阻塞,一直在栈顶,但其实其执行次数并不多,那么将其判定为热点代码就是不合理的。
  • 基于计数器:为每个方法甚至是代码块建立计数器来统计执行次数,如果统计的次数达到了一定的条件则说明是热点代码
    • 优点:结果精确
    • 缺点:实现就比较麻烦了,需要维护计数器

HotSpot 中采取的是第二种方案,因为频繁执行的代码有如下两种:

  • 方法的频繁执行
  • 一段代码的频繁执行

因此 HotSpot 中建立了两种类型的计数器来进行判断,其执行逻辑分别为如下所示:

回边的判断方法与方法基本一致,只是在提交编译请求后,需要把回边计数器的值减小一点,保证代码以解释状态继续执行。

经典优化方案

JIT 中有太多编译的优化技术了,在这里我们就找几个比较经典的介绍一个,剩下的读者感兴趣可以 Google 一下,或者给作者留言,可以再拓展一篇文章单独介绍一下。

方法内联

方法内联应该是 Java 中最重要的几项优化技术之一,其存在的最大意义就是为其他优化手段提供了基础。其使得代码膨胀,因此也提供了更多的优化机会。

表面来看,方法内联只是将代码复制一份到调用的地方,但实现起来真的那么简单吗?

前面我们说过方法的多态调用,介绍了只有 非虚方法 可以在编译期间知道调用的是哪个版本的方法,但是像虚方法这种,是可能会存在多个版本可以选择的,那么编译器在进行方法内联的时候,该复制哪里的代码呢?

为了解决这个问题,JVM 团队引入了 类型继承关系分析 的技术。这种技术的执行逻辑如下所示:

公共子表达式消除

如果一个表达式,在两次计算过程中,其内所有变量的值并没有发生变化,那么则将其称为公共子表达式。

举个栗子:

int a = (b*c)*4+(c*b+d)+d

上面这段代码在计算 b*c 的两次中并没有变化,因此可以将其简写为int a = E * 4 + (E + d) + d,再进一步还可以进行 代数化简 优化,将其优化为:

int a = E * 5 + 2 * d;

数组范围检查消除

Java 语言中为了保持代码的健壮和安全性,在每次数组访问的时候需要判断其是否在 0 ~ length-1 的范围内,如若不然,将抛出异常。这样做有个显而易见的好处是可以提高程序的健壮性,但这对于拥有大量数组访问的程序来说,就是一个灾难了。

因此,如果可以确保数组访问不会越界的情况下,JVM 则可以做出相应的优化,例如可以使用隐式异常处理。栗子如下:

if(object != null){
return object.value;
}else{
throw new Exception();
}

在确定如果 object 在大多数情况下不会为空后,可以做出以下优化:

try{
return object.value;
}catch(segment_fault){
exception_execute;
....
}

这样就可以减少大量的判断开销。

逃逸分析

逃逸分析可以说是目前最前沿的优化技术。其是指当分析对象作用域时,如果一个对象在被定义后,其不会外部方法和线程访问到,那么就可以说明其是不会逃逸的,即其生命周期只有在被定义的块中,因此就可以对其进行优化。

  • 栈上分配,Java 对象大家都知道是分配在堆上的,但通过前面的学习,我们知道栈上的对象在管理时,是十分地影响性能的。因此我们考虑,既然其不会逃逸的话,那么直接将其分配到栈上不是更好吗。这样其可以随着线程的消亡而消亡,减少垃圾收集的压力;
  • 同步消除,如果对象不会逃逸,就别谈线程不安全的访问了,也就不会被多个线程访问,因此没有必要对其进行同步,直接可以把同步消除掉;
  • 标量替换,Java 中的对象分为 标量聚合量 ,其中标量是不能再被拆分的变量,如 int、long 等。而聚合量中最典型的就是对象,现在如果能判断对象不会逃逸,因此结合栈上分配,将其拆分为标量然后分配到栈上是一个很好的优化方式。

前面说了那么多逃逸分析的优点,但目前逃逸分析技术还并不是十分的成熟,其能够带来的优化效果还不好说。

例如下面这种极端情况,JVM 在经过逃逸分析后,发现所有的对象都是可以逃逸出去的,那么就带来的性能消耗就十分的不值了,因为毕竟逃逸分析是一个相对高耗时的过程,耗费了大量的时间和运算资源,结果发现全部白费了。

不过虽然如此,但笔者相信逃逸分析一定是一个优化的技术发展路线。因为其经过优化后的代码大大提升了性能。

总结

在本文中,我们对后端编译的方方面面进行了分析,包括其编译器架构,分层编译的思想,如何判断一段代码值得被编译为本地代码,以及采取哪些方式来优化代码。

对这些内容的深入理解,有助于我们在工作中分出哪些代码是可以被编译器优化的,哪些是需要自己处理的,提高自身编码效率。

文章在公众号「iceWang」第一手更新,有兴趣的朋友可以关注公众号,第一时间看到笔者分享的各项知识点,谢谢!笔芯!

本系列文章主要借鉴自《深入分析 JavaWeb 技术内幕》和《深入理解 Java 虚拟机-JVM 高级特性与最佳实践》。

JVM(十三):后端编译优化的更多相关文章

  1. JVM编译优化

    在部分的商用虚拟机中,Java 程序最初是通过解释器(Interpreter )进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁的时候,就会把这些代码认定为“热点代码”.为了提高热点代码的执 ...

  2. JVM性能优化系列-(5) 早期编译优化

    5. 早期编译优化 早起编译优化主要指编译期进行的优化. java的编译期可能指的以下三种: 前端编译器:将.java文件变成.class文件,例如Sun的Javac.Eclipse JDT中的增量式 ...

  3. JVM性能优化系列-(6) 晚期编译优化

    6. 晚期编译优化 晚期编译优化主要是在运行时做的一些优化手段. 6.1 JIT编译器 在部分的商用虚拟机中,java程序最初是通过解释器(Interpreter) 进行解释执行的,当虚拟机发现某个方 ...

  4. 小师妹学JVM之:深入理解JIT和编译优化-你看不懂系列

    目录 简介 JIT编译器 Tiered Compilation分层编译 OSR(On-Stack Replacement) Deoptimization 常见的编译优化举例 Inlining内联 Br ...

  5. 90% 的 Java 程序员都说不上来的为何 Java 代码越执行越快(1)- JIT编译优化

    麻烦大家帮我投一票哈,谢谢 经常听到 Java 性能不如 C/C++ 的言论,也经常听说 Java 程序需要预热,那么其中主要原因是啥呢? 面试的时候谈到 JVM,也有很多面试官喜欢问,为啥 Java ...

  6. 15个问题自查你真的了解java编译优化吗?

    摘要:为什么C++的编译速度会比java慢很多?二者运行程序的速度差异在哪? 了解了java的早期和晚期过程,就能理解这个问题了. 本文分享自华为云社区<你真的了解java编译优化吗?15个问题 ...

  7. java多线程02-----------------synchronized底层实现及JVM对synchronized的优化

    java多线程02-----------------synchronized底层实现及JVM对synchronized的优化 提到java多线程,我们首先想到的就是synchronized关键字,它在 ...

  8. JVM(9) 程序编译及代码优化

    一.早期(编译器)优化 1.编译期 Java 语言的 “编译期” 其实是一段 “不确定” 的操作过程,因为它可能是指 一个前端编译器(其实叫 “编译器的前端” 更准确一些)把 *.java 文件转变成 ...

  9. Android编译优化系列-kapt篇

    作者:字节跳动终端技术---王龙海 封光 兰军健 一.背景 本文是编译优化系列文章之 kapt 优化篇,后续还会有 build cache, kotlin, dex 优化等文章,敬请期待.本文由Cli ...

随机推荐

  1. [小米OJ] 7. 第一个缺失正数

    思路: 参考这个思路 即:将每个数字放在对应的第几个位置上,比如1放在第1个位置上,2放在第2个位置上. 注意几个点:将每个数放在它正确的位置,前提是该数是正数,并且该数小于序列长度,并且交换的两个数 ...

  2. 【MySQL】(五)索引与算法

    本篇文章的主旨是对InnoDB存储引擎支持的索引做一个概述,并对索引内部的机制做一个深入的解析,通过了解索引内部构造来了解哪里可以使用索引. 1.InnoDB存储引擎支持以下几种常见的索引: B+树索 ...

  3. WSASocket()创建套接字不成功解决方法

    这几天我在写一个模仿windows自带的ping程序,可是套接字总是创建不成功,在网上找了一些资料最后总算把问题解决了,现在总结一下. 解决方法:以管理员运行VS就行了我的是vs2013,vs2010 ...

  4. myeclipse中更改默认jdk版本出错( Target is not a JDK root. System library was not found)

    原因是我的本地jdk版本是9.0,将jdk版本更改至8.0即可导入成功. jdk9.0导入myeclipse中去会有此类问题的发生,因此没有必要使用最新的jdk版本.

  5. 如何实现Excel多人共享与协作

    1.写在前面的话 本人从事信息化工作多年,对Excel等电子表格的多人共享与协作接触较早,帮助客户实施的方案也较多,因此有些体会和认识.正好看到网上这方面的讨论较多,但都不完整,我就进一步做了专题调研 ...

  6. Python装饰器 (转)

    多个装饰器执行的顺序就是从最后一个装饰器开始,执行到第一个装饰器,再执行函数本身. #多个装饰器 import time def deco01(func): def wrapper(*args, ** ...

  7. Could not load NIB in bundle: 'NSBundle.....

    学习NSNotification时遇到了这个问题,错误日志如下: 2015-08-28 17:47:24.617 NSNotificationDemo[7158:786614] *** Termina ...

  8. EditText 使用详解

    极力推荐文章:欢迎收藏 Android 干货分享 本篇文章主要介绍 Android 开发中的部分知识点,通过阅读本篇文章,您将收获以下内容: 一.EditText 继承关系 二.EditText 常用 ...

  9. vue-cli项目下引入vant组件

    前言 Vant是有赞前端团队基于有赞统一的规范实现的 Vue 组件库,提供了一整套 UI 基础组件和业务组件.通过 Vant,可以快速搭建出风格统一的页面,提升开发效率.目前已有近50个组件,这些组件 ...

  10. U盘重装MacOS-Sierra系统

    Mac系统重新安装两种方法: 1.在线远程重装. 2.制作启动U盘进行重装. 理论上第一种比较简单,但是会比较耗时,实际操作中,由于网上下载的系统版本低于我现在MacOS的版本,导致无法安装,因此只能 ...