jvm 执行引擎

执行引擎是jvm执行java程序的一套子系统。

1 解释器

jvm中又两种解释器,字节码解释器、模板解释器,字节码解释器,解释执行,模板解释器执行编译后的硬编码(机器码)。

1.1 字节码解释器

java初期只有字节码解释器,字节码解释器是逐个将字节码指令翻译成硬编码然后执行,所以是
java字节码 -> c++ -> 硬编码过程;非常低效。

 0 new #2 <java/lang/StringBuilder>
 3 dup
 4 invokespecial #3 <java/lang/StringBuilder.<init>>
 7 ldc #4 <子牙>
 9 invokevirtual #5 <java/lang/StringBuilder.append>
12 new #6 <java/lang/String>
15 dup
16 ldc #7 <真帅>
18 invokespecial #8 <java/lang/String.<init>>
21 invokevirtual #5 <java/lang/StringBuilder.append>
24 invokevirtual #9 <java/lang/StringBuilder.toString>
27 astore_1
28 goto 28 (0)

字节码解释器实现模板:

// c++ 代码
while(true) {
    
    
for() {
    
    
  char code = 
  switch(code) {
    
    
    case NEW:
    	// 解释执行
    break;
    case DUP:

    break;
  }
}

可以看出是字节码解释器是用c++代码逐个将字节码解释成硬编码后执行。

1.2 模板解释器

模板解释器直接执行编译过后的硬编码。
模板解释器底层执行流程:
1.申请一块内存,可读可写可执行
2.拿到代码的硬编码
3.将硬编码写入申请的内存
4.申请一个函数指针,指向这块内存
5.调用这个函数指针

1.2.1 即时编译

为了解决字节码解释器效率问题,jvm支持了即时编译技术,将整个函数体编译成机器码,模板解释器直接执行。目前有两种即时编译器,C1、C2。即时编译的最小单位不是一个函数,而是代码块(for、while)

1.2.1.1 jvm server模式和client模式的区别

Server模式启动时,速度较慢,但是一旦运行起来后,性能将会有很大的提升.原因是:当虚拟机运行在-client模式的时候,使用的是一个代号为C1的轻量级编译器, 而-server模式启动的虚拟机采用相对重量级,代号为C2的编译器. C2比C1编译器编译的相对彻底,服务起来之后,性能更高.

所以通常用于做服务器的时候我们用服务端模式,如果你的电脑只是运行一下java程序,就客户端模式就可以了。

1.2.1.2 C1编译器

C1编译器是client模式下的的即时编译器。
1、触发的条件相对C2比较宽松:需要收集的数据较少。
2、编译的优化比较浅:基本运算在编译的时候运算掉了。
3、C1编译器编译生成的代码执行效率较C2低。

1.2.1.2 C2编译器

C2编译器是server模式下的即时编译器
1、触发的条件比较严格,一般来说,程序运行了一段时间以后才会触发。
2、优化比较深。
3、编译生成的代码执行效率较C1更高。

1.2.1.3 混合编译

程序运行初期触发C1编译器。
程序运行一段时间后触发C2编译器。

1.2.1.4 编译优化

jvm在不同的编译器上还做了不同程度的优化,使效率更快

1.2.1.4.1 C1编译器优化

方法内联:将引用的函数代码编译到引用点处,这样可以减少栈帧的生成,减少参数传递以及跳转过程
去虚拟化:对唯一的实现类进行内联
冗余消除:在运行期间把一些不会执行的代码折叠掉

1.2.1.4.2 C2编译器优化

对于C2编译器的优化,是建立在逃逸分析的基础上的。

1.2.1.4.2.1 逃逸分析

逃逸分析是分析对象的作用域是否是局部的,会不会逃到方法外、线程外。
共享变量、返回值,参数就是逃逸的,对象的作用域是局部的就叫不逃逸
基于逃逸分析,C2编译器有3种优化技术,栈上分配、标量替换、锁消除。如果对象是逃逸的那么对象将变得比较复杂,优化无法实施。
逃逸分析默认是开启的,可通过下面命令开启和关闭:
开启:-XX:+DoEscapeAnalysis
关闭:-XX:-DoEscapeAnalysis

1.2.1.4.2.2 栈上分配

对象在虚拟机栈上分配。
证明栈上分配存在

public class AllocationOnStack {
    
    

    public static AllocationOnStack allocation() {
    
    

        return new AllocationOnStack();
    }

    //-Xmx100m -Xms100m -XX:-DoEscapeAnalysis -XX:+PrintGC 关闭逃逸分析
    // 逃逸分析默认开启
    public static void main(String[] args) {
    
    
        long begin = System.currentTimeMillis();
        for (int i = 0; i < 1000000; i++) {
    
    
            allocation();
        }
        long end = System.currentTimeMillis();
        System.out.println("耗时:"+(end-begin)+"ms");
        while (true);
    }
}

以上代码如果开启了逃逸分析,在没有进行垃圾回收的情况下那么在堆中就没有100w个AllocationOnStack对象。
执行后通过jstat查看未促发GC,通过HSDB查看队中对象如下图只有178646个,所以对象未逃逸的情况下是存在栈上分配的。
在这里插入图片描述

在这里插入图片描述
关闭逃逸分析后执行,通过HSDB查看堆上有1000000个AllocationOnStack对象。
在这里插入图片描述

1.2.1.4.2.3 标量替换

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

public class Demo1 {
    
    
    public static void main(String[] args) {
    
    
        Demo1_A demo1_a = new Demo1_A(1,2);
        System.out.println(demo1_a.a);// 编译时标量替换直接将demo1_a.a替换成1
        System.out.println(demo1_a.b);// 编译时标量替换直接将demo1_a.b替换成2

    }
}

class Demo1_A {
    
    
    public int a;
    public int b;
    public Demo1_A(int a, int b) {
    
    
        this.a = a;
        this.b = b;
    }
}

以上代码因为demo1_a未发生逃逸,所以编译时会直接将demo1_a.a替换成1,直接将demo1_a.b替换成2。

1.2.1.4.2.4 锁消除
public class Demo2 {
    
    
    public void test () {
    
    
        Demo2 demo2 = new Demo2();
        synchronized (demo2) {
    
      // 销除同步代码块
            System.out.println("锁销除");
        }
    }
}

以上代码因为demo2并未发生逃逸所以编译时将同步代码块销除

1.2.1.5 热点代码缓存区

热点代码经过即时编译后的硬编码需要缓存起来,即热点代码缓存区,存放在方法区。
可通过下面命令查看缓存区大小:
java -client -XX:+PrintFlagsFinal -version | grep CodeCache

1.3 如何理解java时半编译半解释型语言

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

2 运行模式

2.1 三种运行模式

因为jvm有两种解释,所以有三种运行模式:
-Xint 纯字节码解释器
-Xcomp 纯模板解释器
-Xmixed 字节码解释器+模板解释器
默认时混合模式。
当虛拟机启动的时候,字节码解释器可以首先发挥作用,而不必等待即时编译器全部编译完成再执行,这样可以省去许多不必要的编译时间;随着程序运行时间的推移,即时编译器逐渐发挥作用,根据热点探测功能,将有价值的字节码编译为本地机器指令,以换取更高的程序执行效率。

2.1 触发即时编译的条件

什么情况下才会促发即时编译呢?
一段时间内,Client 编译器模式下, 连续执行 1500次促发即时编译;Server 编译器模式下,连续执行 10000次促发即时编译。
可通过下面命令查看:
java -client -XX:+PrintFlagsFinal -version | grep CompileThreshold

2.2 热度衰减

当超过一定的时间限度(这个一定时间就是半衰周期), 如果程序的调用次数仍然不足以让它提交给即时编译器编译,那这个程序的调用计数器就会被减少一半,这个过程称为热度的衰减 。可以通过-XX:-UseCounterDecay命令关闭热度衰减。可通过-XX:CounterHalfLifeTime指定半衰周期。

2.3 即时编译如何运行

触发即时编译后不是马上执行,jvm是把这个即时编译任务加入队列,VM_THREAD从这个队列中读取任务,并运行。
查看执行即时编译的线程:
java -client -XX:+PrintFlagsFinal -version | grep CICompilerCount
可以通过-XX:CICompilerCount=N 设置。

参考:
https://www.cnblogs.com/yanl55555/p/13334713.html?utm_source=tuicool
https://www.cnblogs.com/grasp/archive/2004/01/13/10184383.html

猜你喜欢

转载自blog.csdn.net/qq_36706941/article/details/112402989
今日推荐