java中的即时编译(JIT)简介
Java发展这么多年一直长青,很大一部分得益于开发人员长期对其坚持不懈的优化:写得更少,跑得更快!JIT就是其中一项十分重要的优化。
JIT全程Java Intime Compiler,即Java即时编译器。咦为啥Java的编译器是一项优化呢?Java本来不就是编译型语言吗?听我细细道来。
从我们最早接触Java编程开始,学习到的就是手写java文件,然后javac编译、java运行主方法。
如果这里都看不懂可能不适合阅读本文
javac会把.java文件编译成.class文件,所以我们说Java是编译型语言。当然Java是强类型的语言,通常我们说强类型的是编译型的,弱类型的脚本语言(也叫动态语言,相对应的强类型语言叫静态语言)。.class文件格式就是“字节码”。编译的过程见《Java文件的编译》。
为了实现“一次编写,随处运行”的目标,字节码会被jvm运行。而这里的运行就是解释执行,jvm是一行一行阅读字节码文件中的jvm指令,并把它翻译成机器的cpu指令。这个过程就比较慢了(相对中低级语言而言)。
Java为了提高开发和运行效率,已经对语言和jvm在多方面做了优先,包括垃圾回收器、各种锁机制,甚至最简单的分支预测都大力优化。解释执行的效率自然也被纳入优化范围。在1996年10月25号,当时的Java东家Sun发布了第一款JIT编译器。那时还是java 2刚出来(Java1 和Java2差异较大,我们现在使用的jdk都是Java2上的迭代),离现在已经20多年了。目前JIT已经是默认开启的,因为它带来的效果明显。除非通过参数指定不使用。
JIT的动机基于“二八定律”,20%的热点代码占据了程序80%的执行时间
即使开启了JIT,也少不了代码编译和字节码解释的过程。JIT处理的是热点代码(hotspot code,或叫热门代码)。热点代码就是频繁执行的代码块,比如循环里面的代码。JIT有一套逻辑判断是否热点代码。
既然JIT处理后的是机器能够快速执行的代码,为啥还要解释执行呢,干嘛不把全部代码编译成机器代码呢?这是由于编译本地代码比较费时间,而且编译后还要进行进一步的优化导致耗时更久;而解释器是能够立即解释字节码文件的,毕竟我们的应用放到服务器上的时候就已经是字节码文件了,解释器可以拿来直接用。而且解释器执行的时候占用的内存更小,在内存受限的场景难以使用编译器(比如手机上)。编译器会概率性地选择多数时候都能提升运行效率的手段进行优化,如果“优化”后发现还不如不优化(甚至执行有问题)就得“逆优化”,回退到解释执行状态。
我们可以通过最简单的查看Java版本的命令查看Java是否使用了编译器:
~ > java -version
java version "1.8.0_191"
Java(TM) SE Runtime Environment (build 1.8.0_191-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.191-b12, mixed mode)
最后输出的“mixed mode”代表是混合模式,也就是先解释执行,并逐步将热点代码代替为机器代码。不使用编译器的模式叫“interpreted mode”;优先使用编译器的模式叫“compiled mode”,compiled mode会优先采用编译方式执行程序,如果编译执行有问题就回退到解释执行。
谷歌的V8是没有解释器的(没错,就是那个执行JS的)。V8的原理可以参阅这个 Quora问答
理论上讲,经过JIT的Java程序运行效率要高于C++。因为C++是静态编译,而JIT在运行中可以参考运行时数据。
HotSpot虚拟机中有两个编译器,一个是给客户端用的叫client Compiler,另一个是服务器用的叫Server Compiler。一般的,把Client Compiler也叫C1编译器,Server Compiler叫C2编译器或Opto编译器。虚拟机会根据自身版本与宿主机的硬件性能自动选择运行模式,也可以使用 “-client”或“-server”参数去强制指定虚拟机运行在Client模式或Server模式。
热点探测
热点代码有两类:
- 被多次执行的方法
- 多次执行的循环体
怎么统计“多次”呢?虚拟机为每个代码块和方法设置了计数器,执行一次就加1。超过限定次数就认为是热点代码,开始JIT处理。给JIT去处理只是一个请求,并不会立即同步等待结果。因为JIT编译比较耗时,在编译完成前会继续解释执行。编译器处理都是以方法为单位,所以第一类热点代码是标准的JIT编译方式;对于第二种热点代码,JIT编译器会处理包含该循环的方法。流程很简单,细节很复杂。下图来自极客学院:javac 编译与 JIT 编译

考虑这个问题:方法在执行时会被放到栈上,对于计算密集型的方法,大量计算任务都在一个方法内循环。这满足第二类热点代码,会被编译。但是方法并没有退出重新执行,编译后的代码怎么能够执行呢?
这个对于早期的JIT的确是个问题,不过现在JVM用到了”栈上替换“的技术:在执行过程中如果编译版本可用了,虚拟机会暂停,把编译版本的方法替换到栈上。反之亦然,上面说过逆优化。
那到底是超过多少次?
HotSpot虚拟机有两种计数器(方法会同时记录这两个计数),它们的阈值并不同。
- 调用次数计数器,可以通过-XX:CompileThreadhold参数指定阈值,不指定默认C1是1500次,C2是1万次。
- 字节码中向之前跳转的指令叫“回边”,回边次数是回边计数器。明显这个针对的是第二类热点代码。它的阈值是算出来的,公式如下
OSR 阈值 = CompileThreshold *
((OnStackReplacePercentage - InterpreterProfilePercentage)/100)
第一个参数CompileThreshold就是调用计数器,后面两个也都可以通过-XX指定。默认InterpreterProfilePercentage是33,而OnStackReplacePercentage的默认值在客户端和服务器模式不一样,分别是933和140,所以阈值分别是13500和10700。
分层编译(Tiered Compilation)
Tiered Compilation是Java7中出现的,目的是整合C1的快速编译和C2的快速执行。因为C2使用了“激进”的优化手段,编译较慢。Java7以前,一般要求快速启动的GUI程序会选择C1,偏好性能的服务器程序使用C2。
Tiered Compilation将编译分为0到4五级,怎么区分呢?看图吧,我并不太懂(出处见水印,侵删):

好吧其实图中并没他们的区别,只是有无profiling而已。
java 10中引入了编译更慢的Graal代替C2成为了第五级编译器。C2是用C++编写的,Graal是Java编写的。只是两种语言而已,为什么要用Java重写一个编译器呢?
因为C2中的全部优化能力已经全部移植到了Graal上,而Graal上面有一些算法(比如inlining算法及partial escape analysis)并不能用C++实现。
Inlining被称为优化之母,因为它能引发更深的优化,能将对getter、setter的访问优化成单一内存访问。
常见的逃逸分析针对的就是锁去除。如果对象被单一线程访问,则可去除锁;如果对象是堆分配且仅被单一方法访问,则可转化成栈分配,并伴随将对字段的访问替换成对操作数的访问,从而进一步将栈分配转换成虚拟分配。另外一大逃逸分析场景是for-loop。
Java10默认激进优化器依然是C2,要使用Graal需要使用参数-XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler来开启。
参考文献:
java中的即时编译(JIT)简介的更多相关文章
- JAVA虚拟机25---编译器,解释器,JAVA中的即时编译
https://www.cnblogs.com/somefuture/p/14272221.html 1.简介 编译器:是一种计算机程序,负责把一种编程语言编写的源码转换成另外一种计算机代码,后者往往 ...
- 即时编译(JIT)
即时编译(JIT : just-in-time compilation): 指计算机领域里,即时编译也被成为动态翻译,是一种通过在运行时将字节码翻译为机器码,从而改善字节码编译语言性能的技术 即时编译 ...
- Java 面试-即时编译( JIT )
当我们在写代码时,一个方法内部的行数自然是越少越好,这样逻辑清晰.方便阅读,其实好处远不止如此,通过即时编译,甚至可以提高执行时的性能,今天就让我们好好来了解一下其中的原理. 简介 当 JVM 的初始 ...
- Java中的流(1)流简介
简介 1.在java中stream代表一种数据流(源),java.io的底层数据元.(比作成水管)2.InputStream 比作进水管,水从里面流向你,你要接收,read3.OutputStream ...
- java中正则表达式,编译报错:Invalid escape sequence (valid ones are \b \t \n \f \r \" \' \\ )
转自:https://www.cnblogs.com/EasonJim/p/6561666.html 若出现:Invalid escape sequence (valid ones are \b ...
- [四] java虚拟机JVM编译器编译代码简介 字节码指令实例 代码到底编译成了什么形式
前言简介 前文已经对虚拟机进行过了简单的介绍,并且也对class文件结构,以及字节码指令进行了详尽的说明 想要了解JVM的运行机制,以及如何优化你的代码,你还需要了解一下,java编译器到底是 ...
- Java 中使用javah编译头文件出现找不到类的情况
在工程的bin目录下,输入命令: javah -classpath . -jni 类路径.JNI类
- java中的反编译
使用JD-GUI工具 支持mac os 和 windows 地址为:http://jd.benow.ca
- Java中对象并不是都在堆上分配内存的
转(https://blog.51cto.com/13906751/2153924) 前段时间,给星球的球友们专门码了一篇文章<深入分析Java的编译原理>,其中深入的介绍了Java中的j ...
- Java中main方面面试题
1.不用main方法如何定义一个类? 不行,没有main方法我们不能运行Java类. 在Java 7之前,你可以通过使用静态初始化运行Java类.但是,从Java 7开始就行不通了. 2.main() ...
随机推荐
- 在 WPF 中集成 ASP.NET Core 和 WebView2 用于集成 SPA 应用
背景 我们有些工具在 Web 版中已经有了很好的实践,而在 WPF 中重新开发也是一种费时费力的操作,那么直接集成则是最省事省力的方法了. 修改项目文件 我们首先修改项目文件,让 WPF 项目可以包含 ...
- 痞子衡嵌入式:从JLink V7.62开始优化了手动增加新MCU型号支持方法
大家好,我是痞子衡,是正经搞技术的痞子.今天痞子衡给大家分享的是JLink 7.62优化了手动增加新MCU型号支持方法. JLink 工具可以说是搞单片机开发的必备神器,JLink 包括一个硬件仿真器 ...
- 单体项目使用Spring Security实现登陆认证授权
前端可以根据权限信息控制菜单和页面展示,操作按钮的显示.但这并不够,如果有人拿到了接口,绕过了页面直接操作数据,这是很危险的.所以我们需要在后端也加入权限控制,只有拥有操作权限,该接口才能被授权访问. ...
- Linux之SELinux
1.什么是SELinux? 安全增强型 Linux(Security-Enhanced Linux)简称 SELinux,它是一个 Linux 内核模块,也是 Linux 的一个安全子系统. SELi ...
- WPF ListBox 控件绑定 Binding
当我们需要用到循环的列表内容,并且模板化程度高的时候,建议使用 ListBox 来做绑定.XAML: <Window.DataContext> <local:VMTempTest/& ...
- 深入探讨Function Calling:在Semantic Kernel中的应用实践
引言 上一章我们熟悉了 OpenAI 的 function calling 的执行原理,这一章节我们讲解一下 function calling 在 Semantic Kernel 的应用. 在Open ...
- 拼多多面试:Netty如何解决粘包问题?
粘包和拆包问题也叫做粘包和半包问题,它是指在数据传输时,接收方未能正常读取到一条完整数据的情况(只读取了部分数据,或多读取到了另一条数据的情况)就叫做粘包或拆包问题. 从严格意义上来说,粘包问题和拆包 ...
- Java常用的三个方法 `wait ` `notify` `notifyAll`
常用的三个方法 wait notify notifyAll wait();方法使当前线程进入等待状态,直到另一个线程调用该对象的notify()或notifyAll()方法来唤醒它 notify(); ...
- nodejs加jq来实现下载word文档
先看效果 浏览器上: 下载的效果: 第一步是自己先搭建前端页面把自己写的结构数据全部传到后端 下面就是整个的结构 结构分析后端拿到数据后端解析: 第一层菜单层: { role: '分析报告', //顶 ...
- 阻塞外挂 TCP 端口 让外挂服务器增加无用处理 反攻击 是4个IP 苹果 安卓 pc 域名
using namespace std;#include<stdlib.h>#pragma comment(lib,"WS2_32.lib") #include < ...