执行引擎、逃逸分析、JIT即时编译

执行引擎是什么?

执行引擎是JVM的一套子系统,当程序运行的时候,执行引擎就开始工作了;在 Java 虚拟机规范中制定了虚拟机字节码执行引擎的概念模型,这个概念模型成为各种虚拟机执行引擎的统一外观(Facade)。在不同的虚拟机实现里,执行引擎在执行 Java 代码的时候可能会有解释执行(通过解释器执行)和编译执行(通过即时编译器产生本地代码执行)两种方式,也可能两者都有,甚至还可能会包含几个不同级别的编译器执行引擎。但从外观上来看,所有 Java 虚拟机的执行引擎是一致的:输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果。

java的两种解释器

java为什么会出现两种解释器,在java之初,java只有字节码解释器,但是字节码解释器是由c++程序完成的,就是我们所编写的程序要通过c++来生成计算机能识别的硬编码,而这样也不能说运行效率很低,肯定没有直接生成硬编码效率高,而这种字节码解释器模式下,很多C++程序员其实是看不起JAVA程序员的,你JAVA程序员写的代码还要我C++帮你完成硬编码的解释执行,其实这种情况在国外比较突出;所以后来有些大牛就想,我是否可以实现自己直接编译成硬编码,不需要通过c++来转换成硬编码,所以后来就诞生了java的模板解释器;那么我现在的java到底默认使用的模板解释器还是字节码解释器,其实我们的java默认使用的是字节码+模板解释器的,下面会有讲到。

即时编译

字节码解释器

java字节码-> C++代码->硬编码(05 06 07)
比如:

public class T0806 {
    
    

    public static void main(String[] args) {
    
    
        T0806_1 t = new T0806_1();
        System.out.println(t);
    }

}

class T0806_1{
    
    

    public T0806_1(){
    
    
        System.out.println("init ...");
    }
}

main方法生成的字节码为:

0 new #2 <com/bml/t0816/T0806_1>
 3 dup
 4 invokespecial #3 <com/bml/t0816/T0806_1.<init>>
 7 astore_1
 8 getstatic #4 <java/lang/System.out>
11 aload_1
12 invokevirtual #5 <java/io/PrintStream.println>
15 return

那么字节码解释器在C++中是如何运行的?
其实C++在为我们解释执行的时候是这样的:
一行一行的解释,也就是一个for或者while循环:

whil(条件){
    
    
    char code = xxx;
    switch(code){
    
    
    case new:
        ....
    break;
    case dup:
        ....
        break;
    }
   
}

其实就是读取我们的每一行代码,根据获取到的字节码指令,执行相应的操作,我这边截取了openjdk底层代码使用字节码解释器的代码片段(c++):

CASE(_new): {
    
    
        u2 index = Bytes::get_Java_u2(pc+1);
        ConstantPool* constants = istate->method()->constants();
        if (!constants->tag_at(index).is_unresolved_klass()) {
    
    
          // Make sure klass is initialized and doesn't have a finalizer
          Klass* entry = constants->slot_at(index).get_klass();
          assert(entry->is_klass(), "Should be resolved klass");
          Klass* k_entry = (Klass*) entry;
          assert(k_entry->oop_is_instance(), "Should be InstanceKlass");
          InstanceKlass* ik = (InstanceKlass*) k_entry;
          if ( ik->is_initialized() && ik->can_be_fastpath_allocated() ) {
    
    
 ……

其实原理就是上述的例子所述;我们这里不是要研究opdnjdk源码,贴出源码是为了把原理搞清楚。

模板解释器

要做的事情就是:java字节码->硬编码
1.申请一块内存,这块内存可读可写可执行(mac底层虽然是uninx,但是为了保护作用,mac是无法运行JIT的,也就是说mac无法申请到可执行的内存空间);
2.如果遇到我们的new关键字或者dup,那么这是字节码指令,那么模板解释器会直接将new或者dup相应的硬编码拿过来;
3.将处理new或者其他字节码指令的硬编码指令直接写入申请到的内存空间中;
4.申请一个函数指针,用这个函数指针来执行这块内存(函数指针->执行函数的指针);
5.程序调用的时候,直接通过这个函数的指针就可以调用了。

两种解释器执行流程

在这里插入图片描述
上图中就是对两种解释器的一个图形表示,和之前的描述区别用图形来表示,如果是字节码解释器,那么会通过C++中的代码来生成硬编码,而模板解释器会直接生成硬编码。

JIT(即时编译)的三种运行模式

-Xint: 纯字节码解释器模式
-Xcomp: 纯模板解释器模式
-Xmixed:字节码解释器+模板解释器 模式
那么我的的jdk默认是用的那种解释器呢?那种解释器的效率高呢?
其实jdk默认使用的是-Xmixed解释器,就是混合解释器,既有字节码解释器也有模板解释器,程序运行的初期使用的是字节码解释器,当运行后期,使用的是模板解释器,其实是由参数控制的,这个后面说;
我们在cmd中执行:

$ java -version
java version "1.8.0_11"
Java(TM) SE Runtime Environment (build 1.8.0_11-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.11-b03, mixed mode)

看到我们的jdk是以server模式运行的,并且是mixed mode,所以我们的jvm是以字节码解释器+模板解释器运行的
上面的三种模式中,纯模板解释器运行效率更高,但是核mixed差不不大,而且程序很小的时候,用字节码更快,而纯模板解释器用于程序非常大的情况下,所以混合解释器在整体性能上最优;
如果需要使用非默认的解释器,在启动参数中加入相应的解释器参数即可。

java中两种即时编译器

1、C1编译器

-client模式启动,默认启动的是C1编译器。有哪些特点呢?
1、需要收集的数据较少,即达到触发即时编译的条件较宽松
2、自带的编译优化优化的点较少
3、编译时较C2,没那么耗CPU,带来的结果是编译后生成的代码执行效率较C2低

2、C2编译器

-server模式启动。有哪些特点呢?
1、需要收集的数据较多
2、编译时很耗CPU
3、编译优化的点较多
4、编译生成的代码执行效率高

3、混合编译

目前的-server模式启动,已经不是纯粹只使用C2。程序运行初期因为产生的数据较少,这时候执行C1编译,程序执行一段时间后,收集到足够的数据,执行C2编译器

Mac中是无法使用JIT的!因为Mac无法申请一块可读可写可执行的内存块
即时编译生成的代码就是给模板编译器使用的

即时编译启动触发条件

即时编译的最小单位不是一个函数或者方法,而是代码块(如for while等)
我们可以通过命令来查看我们的及时编译器执行次数的配置:

java -XX:+PrintFlagsFinal -version|grep CompileThreshold
     intx CompileThreshold                          = 10000           {
    
    pd product}
    uintx IncreaseFirstTierCompileThresholdAt       = 50              {
    
    product}
     intx Tier2CompileThreshold                     = 0               {
    
    product}
     intx Tier3CompileThreshold                     = 2000            {
    
    product}
     intx Tier4CompileThreshold                     = 15000           {
    
    product}
java version "1.8.0_11"
Java(TM) SE Runtime Environment (build 1.8.0_11-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.11-b03, mixed mode)

Client 编译器模式下, CompileThreshold 默认的值 1500
Server 编译器模式下,CompileThreshold 默认的值则是 10000
这个是jdk默认的值,我们给jvm调优可以调这个参数,但是要特别注意,不是非常清楚最好别去修改默认值。

在这种模式下,比如我们的某个代码块,达到即时编译的条件执行了,但是如果长时间没有执行的情况下,会推出模板解释的条件,这个就是热度衰减的概念;
很简单的例子就是我们的开通了某app的会员,如果你到期了没有续费,那么它的成长值会衰减,一样的道理,所以如果我们设置的程序执行达到了3000次执行,那么退出了模板解释器过后,会往下以2倍的速度衰减,要达到之前的条件,会多执行一倍才行。

热点代码缓冲区

热点代码是什么?热点代码就是我们的硬编码,也就是模板解释器生成的硬编码,那么模板解释器启用过后不可能每次都为我们在生成一个硬编码,那么它会把它缓存起来,那么缓存到哪里呢?
热点代码缓存在我们的方法区中;那么在方法区,那么会不会溢出呢?就是出现oom的异常呢?
其实不会出现,它内部有一种淘汰机制,和redis的淘汰机制类似,LRU,所以不会出现OOM的异常。
那么热点代码缓存区到底多大呢?jvm默认的大小如下:
server 编译器模式下代码缓存大小则起始于 2496KB
client 编译器模式下代码缓存大小起始于 160KB
注:jdk在64为下默认为server模式,没有client模式,client模式在32位机器下才会有
指定用 java -client启用client模式

$ java -XX:+PrintFlagsFinal -version|grep CodeCache
    uintx CodeCacheExpansionSize                    = 65536           {
    
    pd product}
    uintx CodeCacheMinimumFreeSpace                 = 512000          {
    
    product}
    uintx InitialCodeCacheSize                      = 25559042496k)         {
    
    pd product}
     bool PrintCodeCache                            = false           {
    
    product}
     bool PrintCodeCacheOnCompilation               = false           {
    
    product}
    uintx ReservedCodeCacheSize                     = 251658240       {
    
    pd product}
     bool UseCodeCacheFlushing                      = true            {
    
    product}
java version "1.8.0_11"
Java(TM) SE Runtime Environment (build 1.8.0_11-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.11-b03, mixed mode)

所以CodeCache这个参数也是我们调优的一种,但是也是要非常了解系统架构的情况下去调优,否则还是用默认比较好,调优可以调这两个参数:
InitialCodeCacheSize
ReservedCodeCacheSize

即时编译是如何运行的?

其实即时编译和GC都是使用的jvm的内部线程VM_THREAD,它内部维护了一个队列,当队列中有请求线程加入的时候,就会去运行,并且即时编译是异步执行的,那么执行即时编译的线程有多少个,怎么去调优呢?

$ java -XX:+PrintFlagsFinal -version|grep CICompilerCount
     intx CICompilerCount                           = 2               {
    
    product}
     bool CICompilerCountPerCPU                     = true            {
    
    product}
java version "1.8.0_11"
Java(TM) SE Runtime Environment (build 1.8.0_11-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.11-b03, mixed mode)

CICompilerCount看这个参数,也就是我们的即时编译的内部线程只有2个,我们可以通过这个参数进行调优;
这边分享一个多年前遇到的一个案例,那个时候我们的银行系统采用的是热冷模式,也就是我们的系统一直运行在热机上,冷机作为灾备,当有天热机出现问题,切换到冷机机房时,出现了大量的请求缓慢以至于系统卡机,导致交易的失败率非常高,导致于最后系统直接挂掉;
这个问题我当时查了好久好久都没有解决,两边机器的配置一模一样,程序配置一模一样,系统参数已调至最优,但是一直没有查到,后面无意间看到jvm的的热点代码缓存区才恍然大悟;
因为我们的热机已经缓存大量的热点代码,可抗住很大的并发,而冷机还未执行过请求,所以突然大并发交易进来就会出现执行缓慢,当并发达到一定的程度,无法即时处理而出现宕机。

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

1.javac编译,java运行;
2.字节码解释器解释执行,模板解释器编译执行

逃逸分析

逃逸分析这个词可要拆成两个词来理解:逃逸、分析(逃逸分析默认是开启的)

什么情况下有逃逸,什么情况下需要分析?
什么叫逃逸?
说白了就是共享变量、返回值、参数等,一句话概括就是这个变量不是局部的
比如我们的方法返回值,当这个方法执行完成过后,那么这个返回值就逃逸了,我们没有办法控制它了,共享变量也是一个道理。总结出来就是两种情况:
方法外
线程外
这两种存在逃逸
什么叫不逃逸
就是对象的作用域是局部的,就没有办法逃逸
分析:
分析是一种技术手段,为什么要对对象的逃逸进行分析?
因为需要对其进行优化,优化代码的执行
基于逃逸分析,JVM开发了三种优化技术

标量替换

标量:不可再分,java中的基本数据类型(8中基本数据类型)就是标量
聚合量:可再分,对象

class T0806_1{
    
    
     public static int x = 2;
     public static int y =3;
    public T0806_1(){
    
    
        System.out.println("init ...");
    }
}


class T2{
    
    
    public static void main(String[] args) {
    
    

        System.out.println(T0806_1.x);
        System.out.println(T0806_1.y);
    }
}

我们注意看

System.out.println(T0806_1.x);
System.out.println(T0806_1.y);

这两行代码,在逃逸分析中进行优化了,在运行的时候其实我们看到的是

System.out.println(2);
System.out.println(3);

这就是标量替换

锁消除

什么是锁消除,就是这个锁根本没什么用,jvm在做逃逸分析时,进行了锁消除,
比如我们看以下代码:

synchronized (new Object()){
    
    
    System.out.println(T0806_1.x);
    System.out.println(T0806_1.y);
}

其实在方法中,这个synchronized根本就没有起作用,是一个无用的锁,所以,代码在执行的时候就进行锁消除,就是根本就没有synchronized的代码了,在运行的时候其实是:

System.out.println(T0806_1.x);
 System.out.println(T0806_1.y);

这就是锁消除,jvm在逃逸分析的时候进行消除

栈上分配

逃逸分析如果是开启的,栈上分配就是存在的
通过代码测试

对象在堆区分配
对象在虚拟机栈上分配

-XX:+/-DoEscapeAnalysis(这个参数就是开启栈上分配)

空了可以测试下栈上分配,我们可以创建10w个对象,通过HSDB这个工具通过开启栈上分配和关闭栈上分配来查看是否存在

猜你喜欢

转载自blog.csdn.net/scjava/article/details/108603633