欢迎您光临本小站。希望您在这里可以找到自己想要的信息。。。

深入理解java虚拟机(九)

java water 868℃ 0评论

晚期(运行期)优化

概述

Java程序最初是通过解释进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁,就会把这些代码认定为“热点代码”,为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器曾为即时编译器(JIT

即时编译器并不是虚拟机必需的部分,但是,即时编译器编译性能的好好、代码优化程度的高低却是衡量一款商用虚拟机优秀与否的最关键的指标之一,它也是虚拟机中最核心最能体现技术水平的部分

HotSpot虚拟机内的即时编译器

为何HotSpot虚拟机要使用解释器与编译器并存的架构?

为何HotSpot虚拟机要实现两个不同的即时编译器?

程序何时使用解释器执行?何时使用编译器执行?

那些程序代码会被编译为本地代码?如何编译本地代码?

如何从外部观察即时编译器的编译过程和编译结果

解释器与编译器:解释器与编译器两者各有优势:当程序需要迅速启动和执行的时候,解释器可以首先发挥作用,把越来越多的代码编译成本地代码之后,可以获取更高的执行效率。当程序运行环境中内存资源限制较大,可以使用解释执行节约内存,反之可以使用编译执行来提升效率。同时,解释器还可以作为编译器激进优化时的一个“逃生门”,让编译器根据概率选择一些大多数时候都能提升运行速度的优化手段,当激进优化的假设不成立,如加载了新类后类型继承结构出现变化、出现“罕见陷阱”时可以通过逆优化退回到解释状态继续执行,解释器与编译器经常是相辅相成配合工作的

HotSpot虚拟机中内置了两个即时编译器,称为Client CompilerServer Compiler,或者简称为C1编译器和C2编译器。目前主流的HotSpot虚拟机中,默认是采用解释器与其中一个编译器直接配合的方式工作。

无论采用的编译器是Client Comiler还是Server Compiler,解释器与编译器搭配使用的方式在虚拟机中被称为混合模式。 用户可以使用参数-Xint强制虚拟机运行于“解释模式”,这时候编译器完全不介于工作,-Xcomp强制虚拟机运行与“编译模式”,这时候优先采用编译方式执行程序。

由于即使编译器编译本地代码需要占用程序运行时间,要编译出优化程序更高的代码,所花费的时间可能越长;而且想要编译出优化程度更高的代码,解释器可能还要替编译器搜集性能监控信息,这对解释执行的速度也有所影响。为了在程序启动响应速度与运行效率之间达到最佳平衡,HotSpot虚拟机将会逐渐启用分层编译的策略,在JDK1.6时期出现,后来一直处于改进阶段,最终在JDK1.7Server模式虚拟机中作为默认编译策略被开启。分层编译根据编译器编译、优化的规模与耗时,划分出不同的编译层次。

分层编译后,Client CompilerServer Compiler将会同时工作。

编译对象与触发条件:

热点代码有两类:被多次调用的方法、被多次执行的循环体

判断代码是不是热点代码,是不是需要触发即时编译,这个行为称为热点探测。

目前主要的热点探测判定

  1. 基于采样的热点探测:虚拟机周期性地检查各个线程的栈顶,如果发下某个方法经常出现在栈顶,那这个方法就是热点方法。优点:实现简单高效,能获取调用关系,缺点:很难精确确认一个方法的热度,容易受到线程阻塞或别的外界因素影响

  2. 基于计数器的:为每个方法(甚至代码块)建立计数器,统计方法的执行次数。超过阀值就是热点方法。缺点:麻烦,不能直接获取方法的调用关系,优点:精确严谨

HotSpot使用的第二种,为每个方法准备两个计数器:方法调用计数器和回边计数器 可以使用-XX:CompileThreshold来设定

默认设置下,执行引擎并不会同步等待编译请求完成,而是继续进入解释器按照解释方式执行字节码,直到提交的请求被编译器编译完成。当编译工作完成之后,这个方法的调用入口地址就会被系统自动改写成新的地址,下一次调用该方法时就会使用已编译的版本。

在默认设置下,方法调用计数器统计的并不是方法被调用的绝对次数,而是一个相对的执行频率,即一段时间之内方法被调用的次数。当超过一定时间限度,如果方法的调用次数仍然不足以让它提交给即时编译器编译,那这个方法的调用计数器就会被减少一半,这个过程称为方法调用计数器的热度衰减,而这段时间就称为此方法统计的半衰周期,进行热度衰减的动作是在虚拟机进行垃圾手机时顺便进行的。可以用-XX:-UserCounterDecay来关闭热度衰减。另外可以使用-XX:CounterHalfLifeTime设置半衰周期

另外一个计数器回边计数器,它用于统计一个方法中循环体代码执行的次数。在字节码中遇到控制流向后跳转的指令就称为“回边”显然建立回边计数器统计的目的就是为了触发OSR编译

编译过程:在默认设置下,无论是方法调用产生的即使编译请求,还是OSR编译请求,虚拟机在代码编译器还未完成之前,都仍然将按照解释方法继续执行,而编译动作则在后台的编译线程中进行

查看与分析即时编译结果

编译优化技术

Java程序员都有一个共同的认知,以编译方式执行本地代码比解释方法更快,其中出去虚拟机解释执行字节码时额外消耗的时间以外,还有一个很重要的原因就是JDK设计团队几乎把对代码的所有优化措施都集中在了即时编译器之中,所以一般来说即时编译器产生的本地代码会比Javac产生的字节码更优秀

公共子表达式消除

数组边界检查消除

方法内联:为了解决虚方法的内联问题,首先引入了“类型继承关系分析(Class Hierarchy Analysis ,CHA)”,这是一种基于整个应用程序的类型分析技术,它用于确定在目前已加载的类中,某个接口是否有多于一种的实现,某个类是否存在子类且子类是否为抽象类等信息。

需要预留一个“逃生门”,如果向CHA查询出来的结果是有多个版本的目标方法可选择,编译器还将会进行最后一次努力,使用内联缓存来完成方法内联

逃逸分析:是目前Java虚拟机中比较前沿的优化技术,它与类型继承关系分析一样,并不是直接优化代码的手段,而是为其他优化手段提供依据的分析技术,逃逸分析的基本行为是分析对象动态作用域

如果证明一个对象不会逃逸到方法或线程之外,也就是别的方法或线程无法通过任何途径访问到这个对象,则可能为这个变量进行一些高效的优化

栈上分配:不会逃逸的局部对象所占比率很大,如果能使用栈上分派,那大量的对象就会随这方法的结束而自动销毁,垃圾搜集系统的压力将会小很多。

同步消除:线程同步本身就是一个相对耗时的过程,如果逃逸分析能确定一个变量不会逃逸出线程,这个变量的读写就不会有竞争,对这个变量实施的同步措施就可以消除

标量替换:标量是指一个数据已经无法在分解成更小的数据来表示,相对的,如果一个数据可以继续分解,那它就被称作聚合量。如果逃逸分析证明一个对象不会被外部访问,并且这个对象可以被拆散,那程序真正执行的时候将可能不创建这个对象,而改为直接创建它的若干个被这个方法使用到的成员变量来代替。将对象拆分后,除了可以让对象的程序变量在栈上分配和读写之外,还可以为后续进一步的优化创建条件。

逃逸分析在JDK1.6中实现,但技术还不成熟,不能保证逃逸分析的性能收益必定高于它的消耗。如果要百分百准确的判断一个对象是否会逃逸,需要进行数据流敏感的复杂分析,来确定程序各个分支执行时对对象的影响,高耗时。

技术扔为完全成熟,它却是即时编译器优化技术的重要发展方向

JavaC/C++的编译器对比

Java语言诞生以来,“执行缓慢”的帽子就应当被扣在头顶,这种观点出现是由于Java刚出现的时候即时编译技术还不成熟,主要靠解释器执行的Java语言性能确实比较低下。

JavaC/C++的编译器对比实际上代表了典型的即时编译器与静态编译器的对比,很大程度上也决定了JavaC/C++的性能对比结果,因为无论C/C++还是java代码,最终编译之后被机器执行的都是本地机器码。

  1. 即时编译器运行时是用户程序的运行时间,具有很大的时间压力。

  2. Java语言是动态的类型安全语言,这就意味这需要由虚拟机来确保程序不会违反语言的语义或访问非结构化内存。

  3. Java语言虽然没有virtual关键字,但使用虚方法的频率却远远大于C/C++语言。

  4. Java语言是可以动态扩展,运行时加载新的类可能改变程序类型的继承关系

  5. Java语言中对象的内存分配都是在堆上,只有方法中局部变量在栈上分配

 

Java程序从源码编译成字节码和从字节码编译成本地机器码的过程,Javac字节码编译器与虚拟机内的JIT编译器的执行过程合并起来其实就等同于一个传统的编译器。

转载请注明:学时网 » 深入理解java虚拟机(九)

喜欢 (0)or分享 (0)

您必须 登录 才能发表评论!