JIT原理
本文转载自JVM杂谈之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 %rbp
40052e: 48 89 e5 mov %rsp,%rbp
400531: 89 7d fc mov %edi,-0x4(%rbp)
400534: 8b 45 fc mov -0x4(%rbp),%eax
400537: 83 c0 01 add $0x1,%eax
40053a: 5d pop %rbp
40053b: 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 rbp
0x48, 0x89, 0xe5, // mov rsp, rbp
0x89, 0xf8, // mov edi, eax
0x83, 0xc0, 0x01, // add $1, eax
0x5d, // pop rbp
0xc3 // 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, eax
0x83, 0xc0, 0x01, // add $1, eax
0xc3 // ret
};
可以看到,指令更加精简了。我们重新编译运行,还是能成功打印出8。
根据这个问题:为什么 lea 会被用来计算?
我们还可以写出更优化的代码来:
char code[] = {
0x8d, 0x47, 0x01, // lea 0x1(rdi), rax
0xc3 // 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),%eax
4005f3: c3 retq
这和我们手写的优化的机器码是完全一样的了。
实际上,C1和C2所要做的和gcc的优化编译是一样的,就是使用特定的方法生成更高效的机器码。但是从原理上来说,运行时生成机器码这个技术,大家都是相通的。
最后,补充一句,iOS禁掉了JIT编译,所用的手段就是无法申请一块同时具有写权限和执行权限的内存。那么,JIT的核心基石,运行时生成可执行的机器码就无法存在了。
JIT原理的更多相关文章
- 浅谈MES系统SMT的JIT功能(一):JIT原理
前段时间帮忙客户实现了MES系统的SMT线上的JIT功能(JIT功能只适合电子行业的生产线),今天就来谈谈JIT功能是什么,为什么工厂车间需要用到JIT等等一些经验 首先说说JIT: 准时制生产方式( ...
- 国际制造执行系统(MES)应用与发展
某些专家认为,当今制造业的生存三要素是信息技术(IT).供应链管理(SCM)和成批制造技术.使用信息技术就是由依赖人工的作业方式转变为作业的快速化.高效化,大量减少人工介入,降低生产经营成本:供应链管 ...
- JIT动态编译器的原理与实现之Interpreter(解释器)的实现(三)
接下来,就是要实现一个虚拟机了.记得编码高质量的代码中有一条:不要过早地优化你的代码.所以,也本着循序渐进的原则,我将从实现一个解释器开始,逐步过渡到JIT动态编译器,这样的演化可以使原理看起来更清晰 ...
- JIT动态编译器的原理与实现之Interpreter3
JIT动态编译器的原理与实现之Interpreter(解释器)的实现(三) 接下来,就是要实现一个虚拟机了.记得编码高质量的代码中有一条:不要过早地优化你的代码.所以,也本着循序渐进的原则,我将从实现 ...
- CoreCLR源码探索(七) JIT的工作原理(入门篇)
很多C#的初学者都会有这么一个疑问, .Net程序代码是如何被机器加载执行的? 最简单的解答是, C#会通过编译器(CodeDom, Roslyn)编译成IL代码, 然后CLR(.Net Framew ...
- 转载 CoreCLR源码探索(七) JIT的工作原理(入门篇)
转载自:https://www.cnblogs.com/zkweb/p/7687737.html 很多C#的初学者都会有这么一个疑问, .Net程序代码是如何被机器加载执行的? 最简单的解答是, C# ...
- CoreCLR源码探索(八) JIT的工作原理(详解篇)
在上一篇我们对CoreCLR中的JIT有了一个基础的了解, 这一篇我们将更详细分析JIT的实现. JIT的实现代码主要在https://github.com/dotnet/coreclr/tree/m ...
- JIT——即时编译的原理
介绍 java 作为静态语言十分特殊,他需要编译,但并不是在执行之前就编译为本地机器码. 所以,在谈到 java的编译机制的时候,其实应该按时期,分为两个部分.一个是 javac指令 将java源码 ...
- jit编译原理
jit用以把程序全部或部分翻译成本地机器码,当需要装载某个类[通常是创建第一个对象时],编译器会先找到其.class文件,然后将该类的字节码装入内存. hotspot采用惰性评估法: 如果一段代码频繁 ...
随机推荐
- HBase的Write Ahead Log (WAL)
HBase的Write Ahead Log (WAL) 一.预写日志WAL(Write-Ahead-Log) HLog HLogKey LogFlusher LogRoller Replay 问题 二 ...
- socket套接字编程(1)——基本函数
TCP交互流程: 服务器:1. 创建socket:2. 绑定socket和端口号:3. 监听端口号:4. 接收来自客户端的连接请求:5. 从socket中读取字符:6. 关闭socket. 客户端:1 ...
- K8s client 使用
使用的k8s client包: <dependency> <groupId>io.fabric8</groupId> <artifactId>kuber ...
- 图的深度优先遍历算法(DFS)
搜索算法有很多种,本次文章主要分享图(无向图)的深度优先算法.深度优先算法(DFS)主要是应用于搜索中,早期是在爬虫中使用.其主要的思想有如下: 1.先访问一个节点v,然后标记为已被访问过2.找到第一 ...
- javascript脚本何时会被执行
javascript脚本可以嵌入在html内的任意地方,但它何时被调用呢?当浏览器打开HTML文件后,会直接运行不是声明函数的脚本或通过事件调用脚本函数,下面分析这几种情况. 1.浏览器在打开页面时执 ...
- C++ Socket 入门
Socket 入门 前置知识 :计算机网络基础(TCP/IP四层模型) Socket 原意是"插座",在计算机通信领域被翻译为"套接字",以\(\{IP:Por ...
- 【POJ 2411】【Mondriaans Dream】 状压dp+dfs枚举状态
题意: 给你一个高为h,宽为w的矩阵,你需要用1*2或者2*1的矩阵填充它 问你能有多少种填充方式 题解: 如果一个1*2的矩形横着放,那么两个位置都用二进制1来表示,如果是竖着放,那么会对下一层造成 ...
- Educational Codeforces Round 97 (Rated for Div. 2) D. Minimal Height Tree (贪心)
题意:有一个从根节点\(BFS\)得来的序列(每次\(bfs\)子节点的时候保证是升序放入队列的),现在让你还原树(没必要和之前相同),问能构造出的最小的树的深度. 题解:不看根节点,我们从第二个位置 ...
- vi、wc、gzip、bzip2、tar、yum安装、dpek、用户信息操作等命令
命令模式 输入"dd"即可将这一行删除 按下"p"即可粘贴 插入模式: a:从光标这个位置之后插入 A:在行尾插入 i:从光标之前插入 I:行首插入 o:在光标 ...
- Chapter Zero 0.1.3 其他单元设备以及运作流程
其他单元设备 五大单元中的控制单元.算数逻辑段元都被整合到CPU的封装中, 但其实系统单元中,不止有CPU(控制单元.算数逻辑单元), 计算机单元还有哪些? 系统单元:系统单元包括CPU.主存储器(内 ...