这篇文章主要介绍“怎么使用java Hotspot虚拟机内的即时编译器”,在日常操作中,相信很多人在怎么使用java Hotspot虚拟机内的即时编译器问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”怎么使用java Hotspot虚拟机内的即时编译器”的疑惑有所帮助!接下来,请跟着小编一起来学习吧!
在部分商用虚拟机(SunHotspot、IBMJ9)中,Java程序最初是通过解释器解释执行的,当虚拟机发现有个方法或代码块运行特别频繁时,就会把这些代码认定为“热点代码”。为了提高热点代码的执行效率,虚拟机会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器被称为即时编译器(JustInTimeCompiler,简称JIT编译器)。即时编译器并不是虚拟机必须的部分,但是即时编译器编译性能的好坏、代码优化程度的高低却是衡量一款商用虚拟机好坏与否最关键的指标之一,是虚拟机中最核心且最能体现虚拟机技术水平的部分。
重点我们需要关注解决以下几个问题:
为何Hotspot虚拟机要使用解释器和编译器并存的架构?
为何Hotspot虚拟机要实现两个不同的即时编译器?
程序何时使用解释器执行?何时使用编译器执行?
哪些程序代码会被编译为本地代码?如何编译为本地代码?
如何从外部观察即时编译器的编译过程和编译结果?
尽管不是所有的Java虚拟机都采样解释器与编译器并存的架构,但是许多主流的虚拟机,比如SunHotspot、IBMJ9,都同时包含解释器与编译器。解释器与编译器有各自的优势:当程序需要快速启动时,解释器可以发挥作用,省去编译时间立即执行。在程序运行后,随着时间的推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码后,可以获取更高的执行效率。
当程序运行环境的内存资源限制较大时,使用解释器执行节省内存,反之可以使用编译执行提升效率。同时,解释器还可以作为编译器激进优化时的一个“逃生门”,让编译器根据概率选择一个大多数时候都能提升运行速度的优化手段,当激进优化的假设不成立时,比如加载了新类后类型继承结构出现变化,出现“罕见陷阱”时可以通过逆优化退回到解释状态继续执行。因此,在虚拟机中解释器和编译器经常配合工作,如下图所示:
图-解释器与编译器的交互
HotSpot虚拟机内置了两个即时编译器:ClientCompiler和ServerCompiler,简称C1编译器和C2编译器。默认采用解释器和其中一个编译器直接配合的方式工作,具体使用哪个编译器,取决于虚拟机工作的模式,用户可以使用-client参数或-server参数指定虚拟机的工作模式,还可以使用-Xint强制虚拟机运行于“解释模式”。
由于即时编译器编译本地代码需要占用程序运行时间,要编译出优化程度更高的代码,所需时间会更长。同时,解释器还要替编译器收集性能监控信息,这对解释执行速度也有影响。为了在程序启动响应速度与运行效率之间达到最佳平衡,HotSpot虚拟机又引入了分层编译的策略。分层编译根据编译器编译、优化的规模与耗时,划分为不同的编译层次,包括:
第0层,程序解释执行,不开启性能监控功能,可触发第1层编译。
第1层,称为C1编译,将字节码编译为本地代码,并进行简单可靠的优化,如有必要将加入性能监控逻辑。
第2层,称为C2编译,也是将字节码编译为本地代码,但是会进行耗时较长的优化,甚至会根据性能监控信息进行一些不完全可靠的激进优化。
实施分层编译后,ClientCompiler和ServerCompiler会同时工作,许多代码可能会被编译多次,用ClientCompiler获得更快的编译速度,用ServerCompiler获取更好的编译质量,在解释执行的时候也无需再承担收集性能监控信息的任务。
在运行过程中,会被即时编译器编译的热点代码有两类:
被多次调用的方法。
被多次执行的循环体。
这两种情况,编译器都会编译整个方法。因为编译发生在方法执行过程中,因此形象地称之为栈上替换(OnStackReplacement,简称OSR,即方法栈帧还在栈上,方法就被替换了)。
判断一段代码是不是热点代码,是否需要触发即时编译,这样的行为称为热点探测(HotSpotDetection),热点探测方式主要有两种:
基于采样的热点探测:采用这种方法的虚拟机会周期性地检查各个线程的栈顶,如果某个方法经常出现在栈顶,那它就是热点方法。其优点是简单、高效,还可以获取方法调用关系;缺点是不够精确,容易受到线程阻塞或其他外接因素的影响。
基于计数的热点探测:采用这种方法的虚拟机会为每个方法(甚至是代码块)建立计数器,统计方法执行次数,次数超过一定阈值就认为是热点方法。这种方法实现起来麻烦,但是其统计结果相对来说更加精确和严谨。
在HotSpot虚拟机里使用的是第二种方法,因此它为每个方法准备了两类计数器:方法调用计数器(InvocationCounter)和回边计数器(BackedgeCounter)。在确定虚拟机运行参数的情况下,这两个计数器都有一定的阈值,超过阈值就会触发JIT编译。
Java虚拟机设计团队几乎对代码的所有优化措施都集中在了即时编译器中,因此一般来说,即时编译器产生的本地代码会比javac产生的字节码更加优秀。下面,我们介绍一些HotSpot虚拟机即时编译器生成代码时采用的代码优化技术。
公共子表达式消除是一个普遍应用于各种编译器的经典优化技术,它的原理是:如果一个表达式E已经计算过了,并且从先前计算到现在E中所有变量的值都没有发生变化,那么E的这次计算就称为公共子表达。对于这种表达式,就没有必要再对其进行计算了,使用之前计算过的值即可。
数组边界检查消除是即时编译器中语言相关的经典优化技术。如果有一个数组foo[],Java语言在访问数组元素foo[i]时,系统会自动进行上下界的范围检查,即i的取值范围是0~foo.length-1,否则将抛出运行时异常java.lang.ArrayIndexOutOfBoundsException。这对开发者来说是好事情,即时程序员没有专门编写防御代码,也可以避免大部分的溢出攻击。但是,对于虚拟机的执行子系统来说,每次数组元素的读写操作都带有一次隐含的条件判定操作,对于拥有大量数组访问的系统,无疑是一种性能上的负担。
编译器会对代码进行分析,如果确定某次数组访问一定不会越界,就可以去掉数组的上下界检查。比如在循环访问数组时,编译器只要通过数据流分析确定循环变量的取值范围一定在[0,foo.length)之间,就可以在整个循环中把数组上下界检查消除。
与数组边界检查消除类似的优化,还有隐式异常处理,Java中空指针检查和除数为零检查都采用了这种思路。
方法内联看起来简单,但实际中很多方法都无法直接进行内联。原因是除了使用invokespecial指令调用的私有方法、实例构造器、父类方法以及使用invokestatic指令调用的静态方法,还有部分final方法能够在编译时唯一确定执行的方法版本,其他都可能存在多于一个版本的方法接收者,需要在运行时才能确定,这一类方法称为虚方法,由于Java语言提倡使用面向对象的编程方式进行编程,而Java语言默认的实例方法就是虚方法,因此内联与虚方法之间产生矛盾。
对于一个虚方法,编译期做内联的时候根本无法确定应该使用哪个方法的版本,为了解决虚方法的内联问题,Java虚拟机引入了一种称为“类型继承关系分析”(ClassHierarchyAnalysis,CHA)的技术,这是一种基于整个应用程序的类型分析技术,它用于确定在目前已加载的类中,某个接口是否有多于一种实现,某个类是否存在子类、子类是否为抽象类等信息。
非虚方法可以直接内联,如果是虚方法且通过CHA分析得知某个方法只有一个版本那也可以进行内联,不过这种内联属于“激进优化”,需要预留一个“逃生门”,称为守护内联。如果程序在执行过程中,虚拟机一直没有加载到令这个类继承关系发生变化的类,那这个内联优化的代码就可以一直使用下去。但如果加载了导致继承关系发生变化的新类,那就需要抛弃已经编译的代码,返回到解释状态执行,或者重新进行编译。
如果CHA查出来某方法有多个版本,则编译器还会进行最后一次努力,使用内联缓存InlineCache来完成方法内联,这是一个建立在目标方法正常入口之前的缓存,其工作原理是:在未发生方法调用之前,内联缓存为空,当第一次调用发生后,缓存记录下方法接收者的版本信息。后续每次执行都检查版本,如果以后进来的每次调用的方法接受者版本都是一样的,那么这个内联还可以一直使用下去,否则查找虚方法表进行方法分派。
逃逸分析是目前Java虚拟机中比较前沿的优化技术,它与类型继承关系分析一样,并不是直接优化代码的技术,而是为其他优化手段提供依据的分析技术。逃逸分析的基本行为是分析对象动态作用域:当一个对象在方法里定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中,称为方法逃逸。甚至还有可能被外部线程访问到,譬如赋值给类变量或其他线程中访问的实例变量,称为线程逃逸。
如果能证明一个对象不会逃逸到方法或线程之外,就可以为这个变量进行一些高效优化:
栈上分配:Java一般是在堆上分配对象的,对象的回收依赖虚拟机的垃圾收集系统,垃圾收集系统回收和整理内存都需要耗费时间。如果一个对象不会逃逸出方法之外,那让这个对象在栈上分配会是一个不错的主意,对象所占用的内存空间可以随着栈帧出栈而销毁,减轻了垃圾收集系统的压力。
同步消除:线程同步本身是一个相对耗时的过程,如果逃逸线程分析确定一个对象不会逃逸出线程,不会被其他线程访问,那这个变量的读写肯定不会有竞争,对这个变量实施的同步措施也就可以消除掉。
标量替换:标量是指一个数据已经无法再分解成更小的数据了,Java中的原始数据类型都不能再进一步分解,就可以称为标量。相对的,如果一个数据可以继续分解,就可以称作聚合量,Java中的对象就是典型的聚合量。如果把一个Java对象拆散,根据程序访问情况,将其使用到的成员变量恢复原始类型来访问就叫做标量替换。如果逃逸分析证明一个对象不会被外部访问,并且这个对象可以被拆散的话,那程序真正执行的时候就可能不创建这个对象,改为直接创建它的若干个成员变量来代替。将对象拆分后,除了可以让成员变量在栈上分配和读写外,还可以为后续进一步优化创建条件。
需要注意逃逸分析的性能收益是否低于它的消耗。如果要完全准确地判断一个对象是否会逃逸,需要进行数据流敏感的一系列复杂分析,从而确定程序各个分支执行时对此对象的影响,这是一个相对耗时的过程,如果分析发现没有几个不逃逸的对象,那这些运行期耗用的时间就白白浪费了,所以目前虚拟机只能采用不那么准确,但时间压力相对较小的算法来完成逃逸分析。
到此,关于“怎么使用java Hotspot虚拟机内的即时编译器”的学习就结束了,希望能够解决大家的疑惑。理论与实践的搭配能更好的帮助大家学习,快去试试吧!若想继续学习更多相关知识,请继续关注亿速云网站,小编会继续努力为大家带来更多实用的文章!
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。