美文网首页
为什么说java语言是半解释半编译型语言?

为什么说java语言是半解释半编译型语言?

作者: 奔向学霸的路上 | 来源:发表于2020-08-18 11:09 被阅读0次

    java是一个半解释半编译型语言,早期java是通过解释器来执行,效率低下;后期进行优化,解释器在原本的c++字节码解释器基础上,扩充了模板解释器,效率有了明显提升;后来又加入了JIT(即时编译),效率就更加得到了提升。

    解释器与编译器

    解释器与编译器两者各有优势:当程序需要迅速启动和执行的时候,解释器可以首先发挥作用,省去编译的时间,立即执行。在程序运行后,随着时间的推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码之后,可以获取更高的执行效率。当程序发现运行环境中内存资源限制较大,可以使用解释执行节约内存,反之可以使用编译执行来提升效率。同时,解释器还可以作为编译器激进优化时的一个“逃生门”,让编译器根据概率选择一些大多数时候都能提升运行速度的优化手段,当激进优化的假设不成立,如加载了新类后类型继承结构出现变化、出现“罕见陷阱”(Uncommon Trap)时可以通过逆优化(Deoptimization)退回到解释状态继续执行(部分没有解释器的虚拟机中也会采用不进行激进优化的C1编译器担当“逃生门”的角色),因此,在整个虚拟机执行架构中,解释器与编译器经常配合工作如下图所示:


    img

    利用参数来指定虚拟机处于"解释模式"、"编译模式"还是"混合模式"。

    混合模式:

    java -version
    java version "1.8.0_101"
    Java(TM) SE Runtime Environment (build 1.8.0_101-b13)
    Java HotSpot(TM) 64-Bit Server VM (build 25.101-b13, mixed mode)
    

    解释模式:

    java -Xint -version
    java version "1.8.0_101"
    Java(TM) SE Runtime Environment (build 1.8.0_101-b13)
    Java HotSpot(TM) 64-Bit Server VM (build 25.101-b13, interpreted mode)
    

    编译模式:

    java -Xcomp -version
    java version "1.8.0_101"
    Java(TM) SE Runtime Environment (build 1.8.0_101-b13)
    Java HotSpot(TM) 64-Bit Server VM (build 25.101-b13, compiled mode)
    

    解释器

    字节码解释器

    .java->javac->c++代码->硬编码(机器码)

    模板解释器

    .java->javac->硬编码(机器码)

    编译器

    JIT即时编译器

    图 2. 查看编译模式

    JIT编译器分类

    Client Compiler - C1编译器

    Client:-Client 模式启动时,速度较快,启动之后不如 Server,适合用于桌面等有界面的程序

    Server Compiler - C2编译器

    Server:-Server 模式启动时,速度较慢,但是启动之后,性能更高,适合运行服务器后台程序

    JIT编译过程

    当 JIT 编译启用时(默认是启用的),JVM 读入.class 文件解释后,将其发给 JIT 编译器。JIT 编译器将字节码编译成本机机器代码,下图展示了该过程。

    JIT 工作原理图

    热点代码(Hot)

    理解

    当虚拟机发现某个方法或代码块的运行特别频繁时,就会把这些代码认定为“热点代码”。

    热点代码的分类

    • 被多次调用的方法

    一个方法被调用得多了,方法体内代码执行的次数自然就多,成为“热点代码”是理所当然的。

    • 被多次执行的循环体

    一个方法只被调用过一次或少量的几次,但是方法体内部存在循环次数较多的循环体,这样循环体的代码也被重复执行多次,因此这些代码也应该认为是“热点代码”。

    上面提到的多次是一个不具体的词语,那到底是多少次才能成为热点代码呢?

    如何检测热点代码

    判断一段代码是否是热点代码,是否需要触发即使编译,这样的行为称为热点探测,热点探测并不一定知道方法具体被调用了多少次,目前主要的热点探测判定方式有两种:

    • 基于采样的热点探测:采用这种方法的虚拟机会周期性地检查各个线程的栈顶如果发现某个(或某些)方法经常出现在栈顶,那这个方法就是“热点方法”

    优点:实现简单高效,容易获取方法调用关系(将调用堆栈展开即可)

    缺点:不精确,容易因为因为受到线程阻塞或别的外界因素的影响而扰乱热点探测

    • 基于计数器的热点探测:采用这种方法的虚拟机会为每个方法(甚至是代码块)建立计数器,统计方法的执行次数,如果次数超过一定的阈值就认为它是“热点方法”

    优点:统计结果精确严谨

    缺点:实现麻烦,需要为每个方法建立并维护计数器,不能直接获取到方法的调用关系

    HotSpot使用第二种 - 基于计数器的热点探测方法。

    确定了检测热点代码的方式,如何计算具体的次数呢?

    计数器的种类(两种共同协作)

    • 方法调用计数器:这个计数器用于统计方法被调用的次数。默认阈值在 Client 模式下是 1500 次,在 Server 模式下是 10000 次
    • 回边计数器:统计一个方法中循环体代码执行的次数

    了解了热点代码和计数器有什么用呢?达到计数器的阈值会触发后文讲解的即时编译,也就是说即时编译是需要达到某种条件才会触发的,先写结论,后文讲解什么是即时编译器。

    两个计数器的协作(这里讨论的是方法调用计数器的情况):当一个方法被调用时,会先检查该方法是否存在被 JIT(后文讲解) 编译过的版本,如果存在,则优先使用编译后的本地代码来执行。如果不存在已被编译过的版本,则将此方法的调用计数器加 1,然后判断方法调用计数器与回边计数器之和是否超过方法调用计数器的阈值。如果已经超过阈值,那么将会向即时编译器提交一个该方法的代码编译请求。

    当编译工作完成之后,这个方法的调用入口地址就会被系统自动改成新的,下一次调用该方法时就会使用已编译的版本。


    image.png

    分层编译

    原因:由于即时编译器编译本地代码需要占用程序运行时间,要编译出优化程度更高的代码,所花费的时间可能越长;而且想要编译出优化程度更高的代码,解释器可能还要替编译器收集性能监控信息,这对解释执行的速度也有所影响,为了在程序启动响应速度与效率之间达到最佳平衡,HotSpot虚拟机将会逐渐启用分层编译,该概念在JDK1.6时期出现,在JDK1.7的Server模式虚拟中作为默认编译策略被开启。

    层次:

    • 0:解释代码
    • 1:简单C1编译代码
    • 2:受限的C1编译代码
    • 3:完全C1编译代码
    • 4:C2编译代码

    查看和分析即时编译结果

    一般来说,虚拟机的即时编译过程对用户程序是完全透明的,虚拟机通过解释执行代码还是编译执行代码,对于用户来说没有什么影响(执行结果没有差异,速度上会有很大的差异),大多数情况下用户也没有必要知道,但是虚拟机也提供了一些参数用来输出即时编译行为。

    示例代码:
    
    public class Test {
        public static final int NUM = 15000;
    
        public  static int doubleValue(int i){
            return i * 2;
        }
    
        public static long calcSum(){
            long sum = 0;
            for (int i = 1; i <= 100; i++){
                sum += doubleValue(i);
            }
            return sum;
        }
    
        public static void main(String[] args) {
            for (int i = 0; i < NUM; i++){
                calcSum();
            }
        }
    }
    

    要知道某个方法是否被编译过,可以使用参数-XX:+PrintCompilation要求虚拟机在即时编译时将被编译成本地代码的方法名称打印出来(带%说明是由回边计数器触发的OSR编译)

    我们还可以加上-XX:+PrintInlining来要求虚拟机输出方法内联信息(备注:-XX:+PrintInlining需要加-XX:+UnlockDiagnosticVMOptions)

        323  102       3       com.yirendai.lab.athenschool.jit.Test::doubleValue (4 bytes)
        323  103       1       com.yirendai.lab.athenschool.jit.Test::doubleValue (4 bytes)
        323  102       3       com.yirendai.lab.athenschool.jit.Test::doubleValue (4 bytes)   made not entrant
        323  104       3       com.yirendai.lab.athenschool.jit.Test::calcSum (26 bytes)
                                  @ 12   com.yirendai.lab.athenschool.jit.Test::doubleValue (4 bytes)
        324  105 %     4       com.yirendai.lab.athenschool.jit.Test::calcSum @ 4 (26 bytes)
                                  @ 12   com.yirendai.lab.athenschool.jit.Test::doubleValue (4 bytes)   inline (hot)
        325  106       4       com.yirendai.lab.athenschool.jit.Test::calcSum (26 bytes)
                                  @ 12   com.yirendai.lab.athenschool.jit.Test::doubleValue (4 bytes)   inline (hot)
        327  104       3       com.yirendai.lab.athenschool.jit.Test::calcSum (26 bytes)   made not entrant
    
    第一列含义为时间戳,第二列中的编号是编译标识,第三列为编译级别
    里面字符的参数含义:
    b    Blocking compiler (always set for client)
    *    Generating a native wrapper
    %    On stack replacement (where the compiled code is running)
    !    Method has exception handlers
    s    Method declared as synchronized
    n    Method declared as native
    made non entrant    compilation was wrong/incomplete, no future callers will use this version
    made zombie         code is not in use and ready for GC
    

    @的含义:

    A “place” in a Java method is defined by its bytecode index (BCI), and
    the place that triggered an OSR compilation is called the “osr_bci”.
    An OSR-compiled nmethod can only be entered from its osr_bci; there
    can be multiple OSR-compiled versions of the same method at the same
    time, as long as their osr_bci differ.
    

    要理解made not entrant,不得不提codeCache

    public int method(boolean flag) {
        if (flag) {
            return 1;
        } else {
            return 0;
        }
    }
    

    从解释执行的角度来看,他的执行过程如下:

    img

    但经过即时编译器编译后的代码不一定是这样,即时编译器在编译前会收集大量的执行信息,例如,如果这段代码之前输入的flag值都为true,那么即时编译器可能会将他变异成下面这样:

    public int method(boolean flag) {
        return 1;
    }
    

    即下图这样

    img

    但可能后面不总是flag=true,一旦flag传了false,这个错了,此时编译器就会将他“去优化”,变成编译执行方式,在日志中的表现是made not entrant

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

    解释器与编译器两者各有优势

    解释器:当程序需要迅速启动和执行的时候,解释器可以首先发挥作用,省去编译的时间,立即执行。

    编译器:在程序运行后,随着时间的推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码之后,可以获取更高的执行效率。

    两者的协作:在程序运行环境中内存资源限制较大时,可以使用解释执行节约内存,反之可以使用编译执行来提升效率。当通过编译器优化时,发现并没有起到优化作用,,可以通过逆优化退回到解释状态继续执行。

    相关文章

      网友评论

          本文标题:为什么说java语言是半解释半编译型语言?

          本文链接:https://www.haomeiwen.com/subject/uwuhjktx.html