Java编译(三) Java即时编译(JIT编译):运行时把Class文件字节码编译成本地机器码

18 篇文章 12 订阅
11 篇文章 0 订阅

Java编译(三)Java即时编译(JIT编译):

运行时把Class文件字节码编译成本地机器码


        在《Java三种编译方式:前端编译 JIT编译 AOT编译》中了解到了它们各有什么优点和缺点,以及前端编译+JIT编译方式的运作过程;在《Java前端编译:Java源代码编译成Class文件的过程》了解到javac编译的大体过程。

        下面我们详细了解JIT编译;从官方JDK中的HotSpot虚拟机的JIT编译器入手,先介绍解释器与JIT编译器是如何配合工作的,认识JIT编译器C1/C2;再看看JIT编译的是什么,以及触发条件是什么;而后再简单介绍JIT编译过程,认识几种编译技术;最后对比Java与C/C++的编译器。

1、解释器与JIT编译器

        即时编译功能与虚拟机具体实现有关,下文的编译器、即时编译器指HotSpot虚拟机内的即时编译器。

        HotSpot源码及相关调试可以参考前文《CentOS上编译OpenJDK8源码 以及 在eclipse上调试HotSpot虚拟机源码》。

1-1、解释器与JIT编译器的结合方式

        前面文章介绍过HotSpot是采用前端编译+JIT编译的方式,所以需要解释器来解释执行加载进来的Class字节码,解释器与JIT编译器的结合方式(即后面说明的混合模式)如下

1、解释器

        程序启动时首先发挥作用,解释执行Class字节码;

       省去编译时间,加快启动速度;

        但执行效率较低;

2、JIT编译器

        程序解释运行后,JIT编译器逐渐发挥作用

       编译成本地代码,提高执行效率;    

        但占用程序运行时间、内存等资源;

3、激进优化的"逃生门"

        解释器还可以作JIT编译器激进优化的一个"逃生门";

        激进优化不成立时,可以通过逆优化(Deoptimization)退回到解释状态编译执行

1-2、JIT编译器:Client Compiler与Server Comiler

        HotSpot虚拟机内置两个即时编译器,分别Client Compiler和Server Comiler,如下:

1、Client Compiler

        简称C1编译器

(A)、应用特点

       较为轻量,只做少量性能开销比较高的优化,它占用内存较少,适合于桌面交互式应用;

(B)、优化技术

        它是一个简单快速的三段式编译器;

        主要关注点在于局部性的优化,而放弃了许多耗时较长的全局优化;

        在寄存器分配策略上,JDK6以后采用的为线性扫描寄存器分配算法,其他方面的优化,主要有方法内联、去虚拟化、冗余消除等;

(C)、设置参数

        可以使用"-client"参数强制选择运行在Client模式(Client VM);

2、Server Compiler

        简称C2编译器,也叫Opto编译器;

(A)、应用特点

       较为重量,采用了大量传统编译优化的技巧来进行优化,占用内存相对多一些,适合服务器端的应用;

(B)、优化技术

        它会执行所有经典的优化动作,如无用代码消除、循环展开、循环表达式外提、消除公表达式、常量传播、基本块重排序等;

        还会一些与Java语言特性密切相关的优化技术,如范围检查消除、空值检查消除等;

        另外,还进行一些不稳定的激进优化,如守护内联、分支频率预测等;

(C)、收集性能信息

        由于C2会收集程序运行信息,因此其优化范围更多在于全局优化,不仅仅是一个方块的优化;

        收集的信息主要有:分支的跳转/不跳转的频率、某条指令上出现过的类型、是否出现过空值、是否出现过异常等。

(D)、与C1的不同点

        和C1的不同主要在于寄存器分配策略及优化范围,寄存器分配策略上C2采用传统的全局图着色寄存器分配算法;

        C2编译速度较为缓慢,但远远超过传统的静态优化编译器;

        而且编译输出的代码质量高,可以减少本地代码的执行时间;

(E)、设置参数

        可以使用"-server"参数强制选择运行在Server模式(Server VM);

3、C1与C2的默认选择

       默认根据机器的硬件性能自动选择运行模式,称为Ergonomics机制;

        JDK 6开始定义服务器级别的机器是至少有两个CPU和2GB的物理内存,才开启C2

        更多关于自适应选择说明请参考:http://docs.oracle.com/javase/8/docs/technotes/guides/vm/server-class.html

1-3、JIT编译器与解释器的工作模式

              可以通过“-version”参数显示当前的工作模式,各工作模式说明如下:

1、混合模式(Mixed Mode)

       JIT编译器(无论C1还是C2)与解释器配合工作的方式;

       这是默认的方式,也可通过“-Xmixed”参数设定;

2、解释模式(Interpreted Mode)

       全部代码由解释器解释执行,JIT编译器不介入工作;

       可以通过“-Xint”参数设定;

3、编译模式(Compiler Mode)

       优先采用编译方式执行程序,但解释器仍要在编译无法时行时介入执行过程;

       可以通过“-Xcomp”参数设定;

       该参数强调的是首次调用方法时执行编译,并不是不用解释器

       一般情况下(不开启分层编译),一个方法需要解释执行一定次数后才编译(详见后面“热点探测“);

        但JDK7/8作为默认开启 分层编译策略

1-4、分层编译

       为了在程序启动响应速度与运行效率之间达到最佳平衡,会启用分层编译(Tiered Compilation)策略;

1、编译层次

       根据编译器编译、优化的规模与耗时,划分出不同的编译层次,包括:

(I)、第0层

       程序解释执行,解释器不开启性能监控功能(Profiling),可触发第1层编译;

(II)、第1层

       也称为C1编译,将字节码编译为本地代码,进行简单、可靠的优化,如有必要加入性能监控的逻辑;

(III)、第2层

       也称为C2编译,也是将字节码编译为本地代码,但进行一些编译耗时较长的优化,甚至会根据性能监控信息进行一些不可靠的激进优化;

2、优点

       这时C1和C2同时进行工作,许多代码都可能被编译多次;

       用C1获取更高的编译速度,用C2获取更好的编译质量;

       解释器执行时也无须再承担收集性能监控信息的任务(如果不开启分层编译,又工作在Server模式,解释器提供监控信息给C2使用);

       最终在程序启动响应速度与运行效率之间达到最佳平衡;

3、设置参数

       JDK6开始出现,需要“-XX:+TieredCompilation”指定开启;

       JDK7/8作为默认的策略,可以通过“-XX:-TieredCompilation”关闭策略

       注意,只能在Server模式下使用;

       关于分层编译的一些信息:http://docs.oracle.com/javase/8/docs/technotes/guides/vm/performance-enhancements-7.html#tieredcompilation

4、源码分析

       源码中对“TieredCompilation”参数的解析过程,如下:

       可以看到设置了"AdvancedThresholdPolicy"对象作为分层编译策略的实现,"AdvancedThresholdPolicy.cpp"中有关于分层编译的更详细的说明,如下:

       可以看到它里面更详细的分为了5层(上面的第1层C1编译包括了里面的1、2、3层);

       另外,不同层有一些不同参数可以设置,如下:

2、JIT编译对象与触发条件

2-1、热点代码

       JIT编译对象为"热点代码",包括两类:

1、被多次调用的方法

       由方法调用触发的编译,以整个方法体为编译对象;

       JVM中标准的的JIT编译方式

2、被多次执行的循环体

       由循环体触发,仍然以整个个方法体为编译对象;

       发生在方法执行过程中,方法栈帧还在栈上,方法就被替换;

       称为栈上替换(On Stack Replacement),简称OSR编译

2-2、热点探测(Hot Spot Deection)方法

       判断一段代码是不是热点代码,是不是需要编译,目前主要有两种方法:

1、基于采样的热点探测(Sample Base Hot Spot Detection)

       JVM周期性检查各个线程的栈顶,看某些方法是否经常出现在栈顶;

       优点:简单高效,且容易获得方法调用关系;

       缺点:不精确,容易受到线程阻塞或外界因素影响;

2、基于计数器的热点探测(Counter Base Hot Spot Detection)

       为每方法(代码块)建立计数器,统计执行次数,超过一定阈值就认为是"热点代码";

       优点:更加精确和严谨;

       缺点:比较复杂;                

       当然还有其他的方法,如Android Dalvik中的JIT编译器使用的基于"踪迹"(Trace)的热点探测;

HotSpot虚拟机使用第二种--基于计数器的热点探测。

2-3、基于计数器的热点探测

       注意,下面的交互过程都是以Client模式JIT编译为例子说明,如果Server模式比较复杂一些,而启用分层编译模式则更复杂。

       HotSpot虚拟机为每个方法准备了两类计数器,来统计执行次数,如下:

1、方法调用计数器(Invocation Counter)

(1)、交互过程

       Client模式时的交互过程,如图:

(A)、当方法执行,先检查是否存在被JIT编译的版本

       如果存在,则用编译的本地代码执行;

       如果不存在,则计数器值加一;

(B)、判断两个计数器(加上回边计数器)之和是否超过阈值

       如果没超过,则以解释方式执行方法;

       如果超过,则向编译器提交编译请求;

(C)、提交编译请求后的执行方式

       如果以默认设置,提交编译请求后,继续以解释方式执行方法;

       如果通过"-Xbatch "或"-XX:-BackgroundCompilation",设置成同步等待方式,则等待编译完成,以编译后代码执行

(2)、阈值设置

       默认C1时为1500次(sparc平台才是1000),C2时为10000次;

       可以通过"-XX:CompileThreshold"参数设定

       启用分层编译时将忽略此选项,请参阅选项"-XX:+ TieredCompilation"

(3)、阈值表示

(A)、执行频率

       默认设置下,方法调用计数器不是统计调用的绝对次数,而是执行频率:一段时间内方法被调用的次数

       如果一段时间内没达到阈值触发编译请求,就会在JVM进行垃圾回收时,把计数器值减半,这个过程称为方法调用计数器热度的衰减(Counter Decay),这段时间称为方法调用计数的半衰周期(Counter Half Life Time);

       可以通过"-XX:CounterHalfLifeTime"参数设置半衰周期的时间(秒)

(B)、绝对次数

       可以通过"-XX:-UseCounterDecay"参数关闭热度衰减

       这时方法调用计数器统计的就是方法调用的绝对次数

2、回边计数器(Back Edge Counter)

       字节码中遇到控制流向后中转的指令,称为"回边"(Back Edge)

       回边计数器作用:统计一个方法中循环体代码执行次数,为触发OSR编译;

(1)、交互过程

       Client模式时的交互过程,如图:

(A)、当执行中遇到回边指令,先检查将要执行的代码片段是否存在被JIT编译的版本:

       如果存在,则用编译的本地代码执行;

       如果不存在,则计数器值加一;

(B)、判断两个计数器(加上方法调用计数器)之和是否超过阈值:

       如果没超过,则以解释方式执行方法;

       如果超过,则向编译器提交编译请求,调整减少回边计数器值;

(C)、提交编译请求后的执行方式

       如果以默认设置,提交OSR编译请求后,继续以解释方式执行方法;

       如果通过"-Xbatch "或"-XX:-BackgroundCompilation",设置成同步等待方式,则等待编译完成,以编译后代码执行

(2)、阈值设置

       简单策略下,并没有使用"-XX:BackEdgeThreshold"参数设置阈值;

       而是使用OnStackReplacePercentage,该值参与计算是否触发OSR编译的阈值;

       可以通过"-XX:OnStackReplacePercentage"来设置,然后通过一定规则计算,如下;

(i)、Client模式                    

       计算规则方法调用计数器阈值(CompileThreshold)*OSR比率(OnStackReplacePercentage)/100

       默认:OnStackReplacePercentage=933, CompileThreshold=1500,计算阈值为14895

(ii)、Server模式

       前面介绍分层编译时曾说:如果不开启分层编译,又工作在Server模式,解释器提供监控信息给C2使用,所以多了个解释器监控比率(InterpreterProfilePercentage);

       计算规则CompileThreshold*(OnStackReplacePercentage-InterpreterProfilePercentage)/100;

       默认:OnStackReplacePercentage=140, CompileThreshold=10000,InterpreterProfilePercentage=33,计算阈值为10700

       可以看到HotSpot源码定义的计算规则,如下:

if (ProfileInterpreter) {
   //Server模式
   InterpreterBackwardBranchLimit = (CompileThreshold * (OnStackReplacePercentage - InterpreterProfilePercentage)) / 100;
} else {
   //Client模式
   InterpreterBackwardBranchLimit = ((CompileThreshold * OnStackReplacePercentage) / 100) << number_of_noncount_bits;
}

(3)、阈值表示

       统计的就是方法中循环体代码执行的绝对次数

       没有执行频率、热度衰减的概念

3、JIT编译过程

       下面分别对C1、C2编译器的编译过程进行简单介绍。

3-1、C1编译过程

       它是一个简单快速的三段式编译器,主要关注点在于局部性的优化,而放弃了许多耗时较长的全局优化;

       三段式编译过程如下:

(A)、在字节码上进行一些基础优化,如方法内联、常量传播等;

       然后将字节码构造成一种高级中间代码表示(High-Level Intermediate Representaion,HIR);

       HIR使用静态单分配(SSA)的形式表示代码值;

(B)、在HIR基础上再次进行一些优化,空值检查消除、范围检查消除等;

       然后将HIR转换为LIR(低级中间代码表示)

(C)、在LIR基础上分配寄存器、做窥孔优化,然后生成机器码;

3-2、C2编译过程

       C2的编译过程较为复杂,从前面对C2的介绍可以知道,C2编译采用了很多优化技术,后面再对一些优化技术进行介绍。

4、查看及分析JIT编译结果

       如何从外部观察JVM的JIT编译行为?

       最好自己编译Debug版本OpenJDK,有一些参数需要Debug或FastDebug版JVM的支持(Product版本不支持), 可以参考《CentOS上编译OpenJDK8源码 以及 在eclipse上调试HotSpot虚拟机源码》。

       相关参数如下:

       "-XX:+PrintCompilation":要求JVM在JIT编译时将衩编译本地代码的方法名称打印出来;

       "-XX:+PrintInlining":要求JVM输出方法内联信息(Product版本需要"-XX:+UnlockDiagnosticVMOptions"选项,打开JVM诊断模式);

       "-XX:+PrintAssembly":JVM安装反汇编适配器后,该参数使得JVM输出编译方法的汇编代码(Product版本需要"-XX:+UnlockDiagnosticVMOptions"选项,打开JVM诊断模式);

       "-XX:+PrintLIR":输出比较接近最终结果的中间代码表示,包含一些注释信息(用于C1,Debug版本);

       "-XX:+PrintOptoAssembly":输出比较接近最终结果的中间代码表示,包含一些注释信息(用于C2,非Product版本);

       "-XX:+PrintCFGToFile":将JVM编译过程中各个阶段的数据输出到文件中,而后用工具C1 Visualizer分析(用于C1,Debug版本);

       "-XX:+PrintIdealGraphFile":将JVM编译过程中各个阶段的数据输出到文件中,而后用工具IdealGraphVisualizer分析(用于C2,非Product版本);

5、JIT编译优化技术

       从前面对C1、C2编译器的介绍可以知道,它们在编译过程中采用了很多优化技术,HotSpot虚拟机JIT编译采用的优化技术可参考:

       PerformanceTechniques:https://wiki.openjdk.java.net/display/HotSpot/PerformanceTechniques

       PerformanceTacticIndex:https://wiki.openjdk.java.net/display/HotSpot/PerformanceTacticIndex

       下面介绍几种最具代表性的优化技术:

       1、语言无关的经典优化技术之一:公共子表达式消除;

       2、语言相关的经典优化技术之一:数组范围检查消除;

       3、最重要的优化技术之一:方法内联;

       4、最前沿的经典优化技术之一:逃逸分析;

5-1、公共子表达式消除

       公共子表达式消除(Common Subexpression Elimination)是语言无关的经典优化技术之一,普遍应用于各种编译器。

1、概述理解

       如果一个表达式E已经被计算过了,并且从先前的计算到现在E中所有变量的值都没有发生变化,那么E的这次出现就称为了公共子表达式

       对于这种表达式,没有必要花时间再对它进行计算,只需要直接用前面计算过的表达式结果代替E就可以了;

       对于不同范围,可分为局部公共子表达式消除全局公共子表达式消除

2、实例说明

       int d = (c*b)*12+a+(a+b*c);

       javac不会优化,JIT编译优化如下:

       1、公共子表达式消除:int d = E*12+a+(a+E);

       2、代数化简(Algebraic Simplification):int d = E*13+a*2;

5-2、数组边界检查消除

       数组范围检查消除(Array Bounds Checking Elimination)是JIT编译器中的一项语言相关的经典优化技术。

1、概念理解

       Java访问数组的时候系统将会自动进行上下界的范围检查;

       这对于虚拟机的执行子系统来说,每次数组元素的读写都带有一次隐含的条件判定操作,对于拥有大量数组访问的程序代码,这无疑也是一种性能负担;

       数组边界检查是必须做的,但数组边界检查在某些情况下可以简化;

2、实例说明

(A)、数组下标是常量

       foo[3];

       编译器根据数据流分析确定foo.length的值,并判断下标"3"没有越界,执行的时候就无须判断了;

(B)、数组下标是循环变量

       for(int i …){

   foo[i];

       }

       如果编译器通过数据流分析就可以判定循环变量"0<=i< foo.length",那在整个循环中就可以把数组的上下界检查消除掉,这可以节省很多次的条件判断操作。

3、隐式异常处理

       大量的安全检查很可能成为一个Java语言比C/C++更慢的因素;

       要消除这些隐式开销,可以:

(A)、尽可能把运行期检查提到JIT编译期完成,如前面的数组边界检查消除

(B)、隐式异常处理如空指针检查和算术运算中的除数为零的检查

(i)、实例说明

 if(foo != null){
   return foo.value;
}else {
   throw new NullPointException();
}

       隐式异常处理后,伪代码变为:

try{
   return foo.value;
}catch(seqment_fault){
   uncommon_trap();
}

       不会进行空值判断,JVM注册异常处理器,为空的时候进入处理器恢复并抛出异常

(ii)、隐式异常处理前后比较

       隐式异常处理不会消耗一次空值判断的开销,但空值时处理经过用户空间->内核空间->用户空间,速度更慢,所以适用于很少为空值的情况

       如果经常为空,还是不用隐式异常处理的好;

       HotSpot运行时会收集Profile信息,自动选择最优方案;

       与语言相关的其他消除操作还有自动装箱消除(Autobox Elimination)、安全点消除(Safepoint Elimination)、消除反射(Dereflection)等。

5-3、方法内联

       方法内联(Method Inlining)是编译器最重要的优化手段之一,普遍适用于各种编译器。

1、概念解理    

       编译器将程序中较小的、多次出现被调用的函数,用函数的函数体来直接进行替换函数调用表达式。

       优点:

       (A)、去除方法调用成本(如建立栈帧);

       (B)、为其他优化建立良好的优化效果,方法内联膨胀后便于在更大范围进行优化;

2、实例说明

        static class B {
            int value;
            final int get() {
                return value;
            }
        }

        public void foo() {
            y = b.get();
            ……
            z = b.get();
            sum = y + z;
        }

(A)、方法内联后

            public void foo() {
                y = b.value;
                ……
                z = b.value;
                sum = y + z;
            }

(B)、冗余访问消除(Redundant Loads Elimination)

            public void foo() {
                y = b.value;
                ……
                z = y;
                sum = y + z;
            }

(C)、复写传播(Copy Propagation)

            public void foo() {
                y = b.value;
                ……
                y = y;
                sum = y + y;
            }

(D)、无用代码消除(Dead Code Elimination)

            public void foo() {
                y = b.value;
                ……
                sum = y + y;
            }

       实例揭示了方法内联对其他优化手段的意义;

       如果不做内联,后续即使进行了无用代码消除的优化,也无法发现任何"Dead Code";

3、虚方法的内联问题

       Java中只有4种方法可以在编译期进行解析:invokespecial指令调用的私有方法、实例构造器、父类方法以及invokestatic指令调用的静态方法;

       而对于一个虚方法,编译期做内联时无法确定应该使用哪个方法版本,需要在运行时进行方法接收者的多态选择;

       Java程序存在大量虚方法,如默认的实例方法(非fianl修饰),所以为解决虚方法的内联问题,首先引用一种名为"类型继承关系分析"(Class Hierarchy Analysis,CHA)的技术

(A)、如果是非虚方法,则直接进行内联;

(B)、如果是虚方法,则向CHA查询;

(i)、如果查询结果只有一个版本,也可以进行内联;

       不过这属于激进优化,需要预留一个"逃生门",称为守护内联(Guarded Inlining);

       因为运行加载类可能导致继承关系发生变化,需要退回解释执行,或重新编译;

(ii)、如果查询结果有多个版本目标,使用内联缓存(Inline Cache)来完成方法内联;

       当缓存第一次调用方法接收者信息,以后每次调用都比较,不一致时取消内联;

       所以方法内联是一种激进优化;

       激进优化在商用虚拟机中很常见,需要预留"逃生门",可以退回解释执行

5-4、逃逸分析

       逃逸分析(Escape Analysis)是JVM中比较前沿的优化技术;

       并不是直接优化代码的手段,而是为其他优化手段提供依据的分析技术;

1、概念理解

       逃逸分析的基本行为就是分析对象动态作用域:

(A)、方法逃逸

       当一个对象在方法里面被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中,这种行为称为方法逃逸;

(B)、线程逃逸

       甚至还有可能被外部线程访问到,譬如赋值给类变量或可以在其他线程中访问的实例变量,这种行为称为线程逃逸;

2、优化说明

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

(A)、栈上分配(Stack Allocations)

       如果确定一个对象不会逃逸出方法之外,那让这个对象在栈上分配内存将会是一个很不错的主意,对象所占用的内存空间就可以随栈帧出栈而销毁;

       一般应用中,大多局部对象都可以使用栈上分配,这样垃圾收集器的压力就会小很多;

(B)、同步消除(Synchronization Elimination)

       线程同步本身就是一个相对耗时的过程,如果逃逸分析能够确定一个变量不会逃逸出线程,无法被其他线程访问,那这个变量的读写肯定就不会有竞争,对这个变量实施的同步措施就可以消除掉。

(C)、标量替换(Scalar Replacement)

标量(Scalar):

       是指一个数据已经无法再分解成更小的数据来表示了,Java虚拟机中的原始数据类型(int、long等 数值类型及reference类型等)都不能再进一步分解,它们就可以被称为标量;

聚合量 (Aggregate):

       相对的,如果一个数据可以继续分解,那它就被称作聚合量 (Aggregate),Java中的对象就是最典型的聚合量;

标量替换:

       如果把一个Java对象拆散,根据程序访问的情况,将其使用到的成员变量恢复原始类型来访问就叫做标量替换;

       如果逃逸分析证明一个对象不会被外部访问,并且这个对象可以被拆散的话,那程序真正执行的时候将可能不创建这个对象,而改为直接创建它的若干个被这个方法使用到的成员变量来代替。

       将对象拆分后,除了可以让对象的成员变量在栈上(栈上存储的数据,很大机会会被虚拟机分配至物理机器的高速寄存器中存储)分配和读写之外,还可以为后续进一步的优化手段创建条件。

3、技术实现

       JDK1.6才实现逃逸分析,但至今都尚未足够成熟,因为:

       如果要完全准确判断一个对象是否会逃逸,需要进行数据流敏感的一系列复杂分析;

       这是一个相对高耗时的过程,不能保证逃逸分析的性能必定高于它的消耗;

       所以现在只能采用不是太准确,但时间压力相对较小的算法来完成

       如栈上分配实现起来比较复杂,HotSpot中暂时还没有做这项优化;

4、相关参数

       JDK6 u23的C2才开始默认开启逃逸分析,相关参数如下:

       "-XX:+/-DoEscapeAnalysis":开启/关闭逃逸分析(只有Server VM支持);

       "-XX:+PrintEscapeAnalysis":查看逃逸分析结果(Server VM 非Product版本支持);

       "-XX:+/-EliminateAllocations":在开启逃逸分析情况下,开启/关闭标量替换(只有Server VM支持);

       "-XX:+PrintEliminateAllocations":查看标量替换结果(Server VM 非Product版本支持);

6、Java与C/C++的编译器对比

       Java与C/C++的编译器对比实际上代表了最经典的JIT编译器与静态编译器的对比,很大程度上也决定了Java与C/C++的性能对比的结果;

       Java虚拟机的JIT编译器输出的本地代码质量可能有一些劣势,因为:

       1、JIT编译占用的是用户程序的运行时间,具有很大的时间压力,不敢随便引入大规模的优化技术;

       2、Java语言是动态的类型安全语言,需要由虚拟机来确保程序不会违反语言的语义或访问非结构化内存;虚拟机必须频繁地进行动态检查,消耗时间,如实例方法访问时检查空指针、数组元素访问时检查上下界范围、类型转换时检查继承关系,等等;

       3、Java程序使用虚方法的频率却远远大于C/C++语言,运行时需要对方法接收者进行多态选择,这也意味着即时编译器在进行一些优化时的难度要远远大于C/C++的静态优化编译器;

       4、Java是可以动态扩展的语言,运行时加载新的类可能改变程序类型的继承关系,这使得很多全局的优化都难以进行,只能以激进优化的方式来完成,编译器不得不时刻注意并随着类型的变化而在运行时撤销或重新进行一些优化;

       5、Java语言中对象分配都在堆上,需要垃圾收集器自动回收管理,占用资源;而C/C++的对象可能在栈上分配,并且主要由用户程序代码来回收分配的内存,将减轻内存回收的压力。

       总得来说,Java在性能上劣势都是为换取开发效率上的优势而付出的代价

       动态安全,动态扩展,垃圾回收这些"拖后腿"的特性都是为Java的开发效率做出了很大的贡献;

       何况Java即时编译器能做的,C/C++的静态优化编译器不一定能够做:

       由于C/C++的静态编译,以运行性能监控为基础的优化措施它都无法进行,如调用频率预测,分支频率预测,裁剪未使用分支等,这些都是称为java语言独有的性能优势。

 

       到这里,我们大体了解Java的即时编译技术,更多实现细节可以参考HotSpot源码,更多编译技术原理可以参考《编译原理》第二版(龙书)、《现代编译原理》(虎书)、《高级编译器设计与实现》(鲸书)。

       后面我们将去了解Java内存回收--垃圾收集算法及垃圾收集器……

 

【参考资料】

1、HotSpot源码

2、《编译原理》第二版

3、《深入理解Java虚拟机:JVM高级特性与最佳实践》第二版 第11章

4、《The Java Virtual Machine Specification》Java SE 8 Edition:https://docs.oracle.com/javase/specs/jvms/se8/html/index.html

5、《Java Platform, Standard Edition HotSpotVirtual Machine Garbage Collection Tuning Guide》:http://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/index.html

6、HotSpot虚拟机参数官方说明:http://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html


  • 10
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值