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中的即时编译(JIT)简介的更多相关文章

  1. JAVA虚拟机25---编译器,解释器,JAVA中的即时编译

    https://www.cnblogs.com/somefuture/p/14272221.html 1.简介 编译器:是一种计算机程序,负责把一种编程语言编写的源码转换成另外一种计算机代码,后者往往 ...

  2. 即时编译(JIT)

    即时编译(JIT : just-in-time compilation): 指计算机领域里,即时编译也被成为动态翻译,是一种通过在运行时将字节码翻译为机器码,从而改善字节码编译语言性能的技术 即时编译 ...

  3. Java 面试-即时编译( JIT )

    当我们在写代码时,一个方法内部的行数自然是越少越好,这样逻辑清晰.方便阅读,其实好处远不止如此,通过即时编译,甚至可以提高执行时的性能,今天就让我们好好来了解一下其中的原理. 简介 当 JVM 的初始 ...

  4. Java中的流(1)流简介

    简介 1.在java中stream代表一种数据流(源),java.io的底层数据元.(比作成水管)2.InputStream 比作进水管,水从里面流向你,你要接收,read3.OutputStream ...

  5. 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   ...

  6. [四] java虚拟机JVM编译器编译代码简介 字节码指令实例 代码到底编译成了什么形式

      前言简介   前文已经对虚拟机进行过了简单的介绍,并且也对class文件结构,以及字节码指令进行了详尽的说明 想要了解JVM的运行机制,以及如何优化你的代码,你还需要了解一下,java编译器到底是 ...

  7. Java 中使用javah编译头文件出现找不到类的情况

    在工程的bin目录下,输入命令: javah -classpath . -jni 类路径.JNI类

  8. java中的反编译

    使用JD-GUI工具  支持mac os 和 windows  地址为:http://jd.benow.ca

  9. Java中对象并不是都在堆上分配内存的

    转(https://blog.51cto.com/13906751/2153924) 前段时间,给星球的球友们专门码了一篇文章<深入分析Java的编译原理>,其中深入的介绍了Java中的j ...

  10. Java中main方面面试题

    1.不用main方法如何定义一个类? 不行,没有main方法我们不能运行Java类. 在Java 7之前,你可以通过使用静态初始化运行Java类.但是,从Java 7开始就行不通了. 2.main() ...

随机推荐

  1. Golang 爬虫01

    目录 学习地址: 目录站: 爬虫概念: 工作流程: 百度贴吧爬虫实现: go实战代码 单进程 实现过程: 并发爬取 实现过程: 学习地址: https://www.bilibili.com/video ...

  2. Google C++ 语言规范

    1. 命名空间 KeyNotes: 鼓励在.cc文件里使用匿名命名空间或者sttic声明 禁止使用内联命令空间,X::Y::foo 等价与X::foo.其主要用于跨版本的ABI兼容问题 namespa ...

  3. DP-Modeler三维修模软件简介

    图像快速建模系统DP-Modeler是天际航自主研发的一款集精细化单体建模及Mesh网格模型修饰于一体的新型软件.通过特有的摄影测量算法,支持航测摄影.无人机影像.地面影像.车载影像.激光点云等多数据 ...

  4. 鸿蒙stage模型

    app.json5全局的配置文件 icon和label是应用列表的 module.json5模块配置文件 中有一个abilities其中的icon和label才是桌面的图标和名称 日志的话就是hail ...

  5. NumPy 数组迭代与合并详解

    NumPy 数组迭代 NumPy 数组迭代是访问和处理数组元素的重要方法.它允许您逐个或成组地遍历数组元素. 基本迭代 我们可以使用 Python 的基本 for 循环来迭代 NumPy 数组. 一维 ...

  6. AIRIOT物联网低代码平台如何配置三菱PLC驱动?

    三菱PLC驱动配置使用三菱Melsec协议(MC协议)从三菱PLC读取数据,仅支持以太网方式.三菱PLC都可以通过此协议访问,但是需要对PLC进行设置. AIRIOT物联网低代码平台如何配置三菱PLC ...

  7. [数字华容道] Html+css+js 实现小游戏

    [数字华容道] Html+css+js 实现小游戏 效果图 代码预览 在线预览地址 代码示例 <!DOCTYPE html> <html> <head> <m ...

  8. 29.4K star! 仅需几行代码快速构建机器学习 Web 应用项目,无需前端技能!

    大家好,我是狂师! 今天给大家推荐一款开源的Python库:Gradio! Gradio是一个开源的Python库,用于创建机器学习和数据科学的交互式应用和演示. 项目地址: https://gith ...

  9. Java21 GA新特性-虚拟线程详解

    本文转载至:虚拟线程 - VirtualThread源码透视 - throwable - 博客园 (cnblogs.com) 一. 前提 JDK19于2022-09-20发布GA版本,该版本提供了虚拟 ...

  10. HTML——input之单选按钮

    在 HTML 中,把 <input> 标签的 type 属性设置为 radio 可以表示单选按钮.具体语法格式如下: <input type="radio" /& ...