执行引擎、JIT、逃逸分析

5 篇文章 0 订阅
1 篇文章 0 订阅

image-20201212225428463

执行引擎就是 JVM运行Java程序的一套子系统

Java是半编译半解释型语言

如果面试官问你这个问题,要分成两个角度来讲解

​ 1、javac编译,java运行

​ 2、运行期即时编译+解释执行(字节码解释器解释执行,模板解释器编译执行)

两种解释器的底层实现

JVM中目前来说有两种解释器

具体细节见课堂上操作实战

1、字节码解释器

做的事情是:java字节码->c++代码->硬编码

根据不同的字节码指令,执行不同的操作。比如下面代码

0 new #4 <com/luban/test/Test_4>
3 dup
4 invokespecial #5 <com/luban/test/Test_4.<init>>
7 astore_1
8 goto 8 (0)

执行的伪代码如下


while(true) {
for() {
  char code = 
  switch(code) {
    case NEW:

    break;
    case DUP:

    break;
  }
}
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() ) {
 ……
2、模板解释器

做的事情:java字节码->硬编码

先看一个C程序:模拟的就是模板解释器的底层实现

​ 1、申请一块内存:可读可写可执行

​ JIT在Mac是无法运行的, Mac无法申请可执行的内存块

​ 2、将处理new字节码的硬编码拿过来(硬编码怎么拿到?) lldb 解析可执行文件

​ 3、将处理new字节码的硬编码写入申请的内存

​ 4、申请一个函数指针,用这个函数指针执行这块内存

​ 5、调用的时候,直接通过这个函数指针调用就可以了

image-20201212173500496

有对硬编码的生成规则感兴趣的同学吗?

void TemplateTable::_new() {
  transition(vtos, atos);

  Label slow_case;
  Label done;
  Label initialize_header;
  Label initialize_object;  // including clearing the fields

  Register RallocatedObject = Otos_i;
  Register RinstanceKlass = O1;
  Register Roffset = O3;
  Register Rscratch = O4;

  __ get_2_byte_integer_at_bcp(1, Rscratch, Roffset, InterpreterMacroAssembler::Unsigned);
  __ get_cpool_and_tags(Rscratch, G3_scratch);
  // make sure the class we're about to instantiate has been resolved
  // This is done before loading InstanceKlass to be consistent with the order
  // how Constant Pool is updated (see ConstantPool::klass_at_put)
  __ add(G3_scratch, Array<u1>::base_offset_in_bytes(), G3_scratch);
  __ ldub(G3_scratch, Roffset, G3_scratch);
  __ cmp(G3_scratch, JVM_CONSTANT_Class);
  __ br(Assembler::notEqual, false, Assembler::pn, slow_case);
……    

image-20201212162350308

​ 字节码解释器是解释执行的,是一步一步执行的,比如执行了new,执行硬编码,执行到dup,再执行硬编码。模版解释器前面已经触发了即时编译,把字节码对应的c++代码已经全部编译生成硬编码,所以他直接执行硬编码,所以它的执行效率比字节码解释器高。

三种运行模式

JIT为什么能提升性能呢?原因是运行期的热点代码编译与缓存

JVM中有两种即时编译器,就诞生了三种运行模式

1、-Xint:纯字节码解释器模式

2、-Xcomp:纯模板解释器模式

3、-Xmixed:字节码解释器+模板解释器模式(默认)

java默认的是混合模式,可以通过 java -Xit -version修改对应的模式。

image-20201212204024378

这三种模式,哪种效率最高呢?jvm默认为什么要混合模式

​ 首先要知道,如果一个程序很大,如果纯模版解释器(运行的就是编译好的硬编码)的话,那么运行初期就要编译很长时间,这段时间程序时不运行的。所以如果程序很小的话,那么第二种比较合适。反之采用第三种。

两种即时编译器

​ jdk6以前是没有混合编译的,后来根据两种编译器的使用场景组合起来使用进一步提升性能

1、C1编译器

​ -client模式启动,默认启动的是C1编译器。有哪些特点呢?

  • 需要收集的数据较少,即达到触发即时编译的条件相对C2比较宽松

  • 自带的编译优化的点较少(编译的优化比较浅,基本运算在编译的时候运算掉了,比如final)

  • 编译时较C2,没那么耗CPU,带来的结果是编译后生成的代码执行效率较C2低

2、C2编译器

​ -server模式启动。有哪些特点呢?

  • 触发的条件比较严格,一般来说,程序运行了一段时间以后才会触发。需要收集的数据较多

  • 编译时很耗CPU

  • 编译优化的点较多

  • 编译生成的代码执行效率较C1更高


Server模式和client模式

​ 在64位机上只有Server模式,在32位机上可以java -client -version指定成client模式。

image-20201212205800363

3、混合编译

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

​ Mac中是无法使用JIT的!因为Mac无法申请一块可读可写可执行的内存块


​ 字节码解释器是解释执行的,跟即时编译器无关。模板解释器执行的硬编码就是即时编译器给编译的。即时编译器有C1,C2。

即时编译触发条件

​ 目前的64bit机器上只有server模式。大家现在谈执行引擎,说的都是server模式启动的JVM中的执行引擎

​ 触发即时编译的最小单位是代码段(for,while…),最大单位是方法,比如循环个数N:

Client 编译器模式下,N 默认的值 1500,即达到1500时才触发

Server 编译器模式下,N 默认的值则是 10000

java -client -XX:+PrintFlagsFinal -version | grep CompileThreshold

image-20201212211959451

热度衰减

​ 比如现在一个线程调用某一个方法,已经调用了7000次了,随后很长一段时间又没调用了,这时候就会2倍数往下掉,比如掉到3500,原来的话我需要再执行3001次就可以触发即时编译,但是现在我就需要6501次了。


​ 编译器编译后就生成了硬编码,在JVM中也叫热点代码。

热点代码缓存区

​ 热点代码缓存是保存在方法区的,这块也是调优需要调的地方

​ server 编译器模式下代码缓存大小则起始于 2496KB

​ client 编译器模式下代码缓存大小起始于 160KB

java -XX:+PrintFlagsFinal -version | grep CodeCache

image-20201212212927188

​ 调优的话一般将InitialCodeCacheSize,ReservedCodeCacheSize这俩调成一样大。

即时编译器时如何运行的

​ 其实在JVM中很多系统性的操作,像GC,即时编译都是通过VM_THREAD出发的,可以把它理解成一个队列,当达到出发条件,有线程向这个队列里面推送任务,然后其他线程异步去执行任务。

比如System.gc

​ 1、将这个即时编译任务写入队列QUEUE

​ 2、VM_THREAD从这个队列中读取任务,并运行

执行即时编译的线程有多少,以及如何调优

java -client -XX:+PrintFlagsFinal -version | grep CICompilerCount

-XX:CICompilerCount=N //调优参数 

image-20201212213546646

逃逸分析

理解含义

​ 逃逸分析这个词可要拆成两个词来理解:逃逸、分析;逃逸是一种现象,分析是一种技术手段。

逃逸

​ 外部能访问到就叫逃逸,比如共享变量,返回值,参数。。。

​ 如果对象的作用域不是局部的,也就是逃逸。

​ 逃到哪里呢,可以理解为逃到方法外,线程外。

不逃逸

​ 对象的作用域是局部变量

分析

先想想:为什么要做逃逸分析?

​ 如果对象发生了逃逸,那情况就会变的非常复杂,优化无法实施。基于逃逸分析,JVM开发了三种优化技术(不逃逸的情况)。

​ 以下优化技术只有不逃逸的情况下才能做。

1、栈上分配

​ 逃逸分析默认是开启的,栈上分配就是存在的,对象在虚拟机栈上分配

如何证明栈上分配的存在?

public class StackAlloc {

    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        long start = System.currentTimeMillis();

        for (int i = 0; i < 1000000; i++) {
            alloc();
        }

        long end = System.currentTimeMillis();

        System.out.println((end - start) + " ms");

        while (true);
    }

    public static void alloc() {
        StackAlloc obj = new StackAlloc();
    }
}

​ 可以这样证明,生成了一个对象100w次,不发生gc的情况下,利用HSDB看堆区是不是有100w个,如果没有,就存在栈上分配。如下图:

image-20201212222024578

可以看到远远不够100w。然后在启动参数加上-XX:+/-DoEscapeAnalysis(-关闭,+开启),进行开启和关闭关闭栈上分配后如下图:

image-20201212222313196
2、标量替换

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

聚合量:可再分,对象

看一个例子

public class ScalarReplace {

    public static void main(String[] args) {

    }

    public static void test() {
        Position position = new Position(1, 2, 3);

        System.out.println(position.x);
        System.out.println(position.y);
        System.out.println(position.z);
    }
}

class Position {
    int x;
    int y;
    int z;

    public Position(int x, int y, int z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }
}

​ Position对象的x,y,z就是一个标量,所以postion就是有效的局部变量此时jvm在做逃逸分析的时候就回把输出代码替换为

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

​ 这就是标量替换。

3、锁消除

举个例子

public void noEscape1(){
    synchronized (new Object()){
        System.out.println("hello");
    }
}

​ 此时在局部变量中加锁,jvm在做逃逸分析后就会变为这样

public void noEscape1(){
    System.out.println("hello");
}
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值