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() ...
随机推荐
- css :not()选择器使用
前言:这是一个vue的项目,引入了一个reset.css,重写了几乎所有标签的默认样式.项目中需要渲染富文本,里面包含了很多标签,例如:<h1>这是一个大标题</h1>,这个时 ...
- java程序,如何打印详细报错堆栈信息
try { System.out.println(1/0); } catch (final Exception e) { log.error("ERROR", "Erro ...
- Java基础 韩顺平老师的 常用类 的部分笔记
459,八大Wrapper类 包装类的分类 1) 针对八种基本数据类型相应的引用类型-包装类 2) 有了类的特点, 就可以调用类中的方法. 460,装箱和拆箱 package com.hspedu.W ...
- 更改wsl中系统的安装位置
wsl默认安装位置是C盘,众所周知C盘总是不够用的,所以才有了把wsl的系统迁移到其它位置的需求.官网文档 首先查看所有分发版本 wsl -l --all -v 导出分发版为tar文件到D盘 wsl ...
- 将外部jar打入本地maven仓库
1.将jar包放入某不含中文的路径下 ,例如:E:\file\zip4j-1.3.2.jar 2.在命令行输入操作命令 mvn install:install-file -DgroupId=zip4j ...
- windows下IPv6组播(C++、MFC)
Server #include <stdio.h> #include <Ws2tcpip.h> #include <winsock2.h> #pragma comm ...
- 7.13晚考试总结(NOIP模拟14)[队长快跑·影魔·抛硬币]
樱花满地集于我心,楪舞纷飞祈愿相随 前言 终于没有令人头疼的数学了,总感觉这次考试的题目比较良心. 尤其是对于部分分的明细就非常到位(不像上一场的凭感觉给出部分分). 这就令我这种靠部分分暴力的菜鸡选 ...
- vue 实现商品列表的添加、删除,搜索
大江东去,浪淘尽,千古风流人物.故垒西边,人道是,三国周郎赤壁.乱石穿空,惊涛拍岸,卷起千堆雪.江山如画,一时多少豪杰.遥想公瑾当年,小乔初嫁了,雄姿英发.羽扇纶巾,谈笑间,樯橹灰飞烟灭.故国神游,多 ...
- golang interface 和 struct 添加方法的区别
在 Go 语言中,struct 和 interface 都可以关联方法,但它们的方式不同: 1. struct 添加方法: 结构体(struct)本身不直接包含方法,但可以通过定义一个指向该结构体类型 ...
- ABC321题解
E: problem LCA题. 我们枚举向上跳 \(t\) 步,跳到了 \(y\). 假如说 \(t = 0\) 那么我们计算 \(\text{clac}(x,k)\) 即可.(\(\text{cl ...