五十七、JIT

2024-05-10 21:32
文章标签 jit 五十七

本文主要是介绍五十七、JIT,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

JIT技术是JVM中最重要的核心模块之一。我的课程里本来没有计划这一篇,但因为不断有朋友问起,Java到底是怎么运行的?既然Hotspot是C++写的,那Java是不是可以说运行在C++之上呢?为了澄清这些概念,我才想起来了加了这样一篇文章,算做番外篇吧。

Just In Time

Just in time编译,也叫做运行时编译,不同于 C / C++ 语言直接被翻译成机器指令,javac把java的源文件翻译成了class文件,而class文件中全都是Java字节码。那么,JVM在加载了这些class文件以后,针对这些字节码,逐条取出,逐条执行,这种方法就是解释执行。

还有一种,就是把这些Java字节码重新编译优化,生成机器码,让CPU直接执行。这样编出来的代码效率会更高。通常,我们不必把所有的Java方法都编译成机器码,只需要把调用最频繁,占据CPU时间最长的方法找出来将其编译成机器码。这种调用最频繁的Java方法就是我们常说的热点方法(Hotspot,说不定这个虚拟机的名字就是从这里来的)。

这种在运行时按需编译的方式就是Just In Time。

主要技术点

其实JIT的主要技术点,从大的框架上来说,非常简单,就是申请一块既有写权限又有执行权限的内存,然后把你要编译的Java方法,翻译成机器码,写入到这块内存里。当再需要调用原来的Java方法时,就转向调用这块内存。

我们看一个例子:

#include<stdio.h>int inc(int a) {return a + 1;
}int main() {printf("%d\n", inc(3));return 0;
}

上面这个例子很简单,就是把3加1,然后打印出来,我们通过以下命令,查看一下它的机器码:

# gcc -o inc inc.c
# objdump -d inc

然后在这一堆输出中,可以找到 inc 方法最终被翻译成了这样的机器码:

  40052d:	55                   	push   %rbp40052e:	48 89 e5             	mov    %rsp,%rbp400531:	89 7d fc             	mov    %edi,-0x4(%rbp)400534:	8b 45 fc             	mov    -0x4(%rbp),%eax400537:	83 c0 01             	add    $0x1,%eax40053a:	5d                   	pop    %rbp40053b:	c3                   	retq 

我来解释一下(读者需要一定的x86汇编语言的知识)。

第一句,保存上一个栈帧的基址,并把当前的栈指针赋给栈基址寄存器,这是进入一个函数的常规操作。我们不去管它。

第三句,把edi存到栈上。在x64处理器上,前6个参数都是使用寄存器传参的。第一个参数会使用rdi,第二个参数使用 rsi,等等。所以 edi 里存的其实就是第一个参数,也就是整数 3,为什么使用rdi的低32位,也就是 edi 呢?因为我们的入参 a 是 int 型啊。大家可以换成 long 型看看效果。

第四句,把上一步存到栈上的那个整数再存进 eax 中。

第五句往后,把 eax 加上 1, 然后就退栈,返回。按照x64的规定(ABI),返回值通过eax传递。

我们看到了,其实第三句,第四句好像根本没有存在的必要,gcc 默认情况下,生成的机器码有点傻,它总要把入参放到栈上,但其实,我们是可以直接把参数从 rdi 中放入到 rax 中的。不满意。那我们可以自己改一下,让它更精简一点。怎么做呢?答案就是运行时修改 inc 的逻辑。

#include<stdio.h>
#include<memory.h>
#include<sys/mman.h>typedef int (* inc_func)(int a); int main() {char code[] = { 0x55,             // push rbp0x48, 0x89, 0xe5, // mov rsp, rbp0x89, 0xf8,       // mov edi, eax0x83, 0xc0, 0x01, // add $1, eax0x5d,             // pop rbp0xc3              // ret};  void * temp = mmap(NULL, sizeof(code), PROT_WRITE | PROT_EXEC,MAP_ANONYMOUS | MAP_PRIVATE, -1, 0); memcpy(temp, code, sizeof(code));inc_func p_inc = (inc_func)temp;printf("%d\n", p_inc(7));return 0;
}

在这个例子中,我们使用了 mmap 来申请了一块有写权限和执行权限的内存,然后把我们手写的机器码拷进去,然后使用一个函数指针指向这块内存,并且调用它。通过这种方式我们就可以执行这一段手写的机器码了。

运行一下看看:

# gcc -o inc inc.c 
# ./inc
8

再回想一下这个过程。我们通过手写机器码把原来的 inc 函数代替掉了。在新的例子中,我们是使用程序中定义的数据来重新造了一个 inc 函数。这种在运行的过程创建新的函数的方式,就是JIT的核心操作。

解释器,C1和C2

在Hotspot中,解释器是为每一个字节码生成一小段机器码,在执行Java方法的过程中,每次取一条指令,然后就去执行这一个指令所对应的那一段机器码。256条指令,就组成了一个表,在这个表里,每一条指令都对应一段机器码,当执行到某一条指令时,就从这个表里去查这段机器码,并且通过 jmp 指令去执行这段机器码就行了。

这种方式被称为模板解释器。

模板解释器生成的代码有很多冗余,就像我们上面的第一个例子那样。为了生成更精简的机器码,我们可以引入编译器优化手段,例如全局值编码,死代码消除,标量展开,公共子表达式消除,常量传播等等。这样生成出来的机器码会更加优化。

但是,生成机器码的质量越高,所需要的时间也就越长。JIT线程也是要挤占Java 应用线程的资源的。所以C1是一个折衷,编译时间既不会太长,生成的机器码的指令也不是最优化的,但肯定比解释器的效率要高很多。

如果一个Java方法调用得足够频繁,那就更值得花大力气去为它生成更优质的机器码,这时就会触发C2编译,c2是一个运行得更慢,但却能生成更高效代码的编译器。

由此,我们看到,其实Java的运行,几乎全部都依赖运行时生成的机器码上。所以,对于文章开头的那个问题“Java是运行在C++上的吗?”,大家应该都有自己的答案了。这个问题无法简单地回答是或者不是,正确答案就是Java的运行依赖模板解释器和JIT编译器。

多说一点优化

我们这节课所举的例子中,可以做更多的优化,例如,既然我进到inc函数以后,完全没有使用栈,那其实,我就不要再为它开辟栈帧了。所以可以把push rbp, pop rbp的逻辑都去掉。

进一步优化成这样:

    char code[] = { 0x89, 0xf8,       // mov edi, eax0x83, 0xc0, 0x01, // add $1, eax0xc3              // ret}; 

可以看到,指令更加精简了。我们重新编译运行,还是能成功打印出8。

根据这个问题:为什么 lea 会被用来计算?

我们还可以写出更优化的代码来:

    char code[] = { 0x8d, 0x47, 0x01,    // lea 0x1(rdi), rax0xc3                 // ret};

如果开启 gcc 的优化编译,我们也可以得到这样的代码,例如,还是针对这个方法:

int inc(int a) {return a + 1;
}

使用 -O2 优化:

# gcc -o inc inc.c -O2
# objdump -d inc

就可以看到,inc 的机器码变成这样了:

00000000004005f0 <inc>:4005f0:	8d 47 01             	lea    0x1(%rdi),%eax4005f3:	c3                   	retq 

这和我们手写的优化的机器码是完全一样的了。

实际上,C1和C2所要做的和gcc的优化编译是一样的,就是使用特定的方法生成更高效的机器码。但是从原理上来说,运行时生成机器码这个技术,大家都是相通的。

最后,补充一句,iOS禁掉了JIT编译,所用的手段就是无法申请一块同时具有写权限和执行权限的内存。那么,JIT的核心基石,运行时生成可执行的机器码就无法存在了。

这篇关于五十七、JIT的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/977613

相关文章

java开发面试:AOT有什么优缺点/适用于什么场景/AOT和JIT的对比、逃逸分析和对象存储在堆上的关系、高并发中的集合有哪些问题

JDK9引入了AOT编译模式。 AOT 有什么优点?适用于什么场景? JDK 9 引入了一种新的编译模式 AOT(Ahead of Time Compilation) 。 和 JIT 不同的是,这种编译模式会在程序被执行前就将其编译成机器码,属于静态编译(C、 C++,Rust,Go 等语言就是静态编译)。 AOT 避免了 JIT 预热等各方面的开销,可以提高 Java 程序的启动速度。并且

1、Java简介+DOS命令+java的编译运行(字节码/机器码、JRE/JVM/JDK/JIT的区别)+一个简单的Java程序

前言:本文属于黑马程序员和javaguide的混合笔记,仅作学习分享使用,建议感兴趣的小伙伴去看黑马原视频或javaguide原文。如有侵权,请联系删除。 Java类型: JavaSE 标准版:以前称为J2SE,主要用来开发桌面应用程序或简单的服务器应用程序。JavaEE 企业版:建立在 Java SE 的基础上,包含了支持企业级应用程序开发和部署的标准和规范(如Servlet、Jsp、

Python从0到100(五十七):机器学习-主成分分析机

主成分分析是⼀种常⽤的降维技术,⽤于将⾼维数据集投影到低维空间中,同时保留数据集的主要特征。PCA通过寻找数据中最重要的⽅向(主成分),并将数据投影到这些⽅向上来实现降维。 1.基本原理 1、数据中心化:⾸先,对原始数据进⾏中⼼化处理,即将每个特征的均值减去每个数据点的对应特征值,以确保数据的均值为零。 2、协方差矩阵:然后,计算数据的协⽅差矩阵,该矩阵表示了不同特征之间的关联性。 3、特征值

思考(五十七):一处 string 字段竞态问题引发的 crash

string 字段多协程竞态 通常写代码比较注意一些数据结构、容器的多协程竞态,比如 slice 、 map 对于 string 字段的多协程竞态,非常容易忽视 这里举例说明,项目中遇到的问题 竞态代码 代码片段1 (协程1 中执行) func (s *Server) loginOnWindows(p *common.Proto, ch *Channel) (err

【JVM】执行引擎、JIT、逃逸分析(二)

执行引擎、JIT、逃逸分析 JIT(Just-In-Time,即时编译) 针对的是热点代码(触发JIT的条件) Client模式:32bit才有 Server模式:64bit 触发条件后,谁来编译,编译线程 C1:Client模式下 C2: Server模式下 JDK6之后,混合在一起, 热点代码((统计的并不是被调用的绝对次数,而是一个相对的执行频率,一段时间内方法被调用的次数))其中包

刺激、环境、响应-系统架构师(五十七)

1需求管理是 CMM可重复级中的6个关键过程域之一,其主要目标是()。 A对于软件需求,必须建立基线以进行控制,软件计划、产品和活动必须与软件需求保持一致 B客观的验证需求管理活动符合规定的标准、程序和要求 C策划软件需求管理的活动,识别和控制已获取的软件需求 D跟踪软件需求管理的过程、实际结果和执行情况 解析: A 2商业智能是企业对商业数据的搜集、管理和分析的系统过程,主

意外的内存分配:JIT编译抖动

我在 ByteWatcher (见我最后一篇文章)工作时,碰到了一些奇怪的事情。 这是一段用来查找在特殊线程上分配了多少内存的真实代码片段。 return (long) mBeanServer.invoke( name, GET_THREAD_ALLOCATED_BYTES, PARAMS, SIGNATURE ); 全部上下文参见这里。 https://gith

RK3568笔记五十七:基于UNetMultiLane的多车道线等识别部署

若该文为原创文章,转载请注明原文出处。 此篇记录在正点原子的ATK-DLRK3568上复现山水无移大佬的UNetMultiLane 多车道线、车道线类型识别。 数据是基于开源数据集 VIL100。其中数据标注了所在的六个车道的车道线和车道线的类型。 8条车道线(六个车道),对应的顺序是:7,5,3,1,2,4,6,8。其中1,2对应的自车所在的车道,从左往右标记。 车道线的类别(10个

【个人学习】JVM(9): 执行引擎、解释器、JIT编译器、其他编译器

执行引擎 执行引擎概述 执行引擎概述 执行引擎是Java虚拟机核心的组成部分之一。“虚拟机”是一个相对于“物理机”的概念,这两种机器都有代码执行能力,其区别是物理机的执行引擎是直接建立在处理器、缓存、指令集和操作系统层面上的,而虚拟机的执行引擎则是由软件自行实现的,因此可以不受物理条件制约地定制指令集与执行引擎的结构体系,能够执行那些不被硬件直接支持的指令集格式。JVM的主要任务是负责装