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

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 虚拟机要使用解释器与编译器并存的架构?

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

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

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

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,193评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,306评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,130评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,110评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,118评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,085评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,007评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,844评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,283评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,508评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,667评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,395评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,985评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,630评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,797评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,653评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,553评论 2 352