栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

Java - JIT即时编译器

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

Java - JIT即时编译器

Java - JIT即时编译器
  • 前言
  • 一. JVM编译器
    • 1.1 Client Compiler
    • 1.2 Server Compiler
      • 1.2.1 C2 Compiler
      • 1.2.2 Graal Compiler
  • 二. JIT和编译优化
    • 2.1 中间表达形式(IR)
    • 2.2 方法内联
    • 2.3 逃逸分析
    • 2.4 Loop Transformations
    • 2.5 窥孔优化与寄存器分配

前言

我们知道,Java有着“一次编译,处处运行”的特性,会将编译阶段分成两部分:

  1. 前端编译:由javac编译成字节码,该过程会进行词法分析、语法分析、语义分析。
  2. 后端编译:由解释器将字节码解释为机器码来执行。

而后端编译又可以根据执行的方式来分为两种:

  1. 解释执行:一行一行解释成机器码再执行,每次调用时都需要重新逐条解释执行。
  2. 编译执行JIT(Just In Time):将执行的比较多的热点代码编译优化成本地代码,提高执行效率。(好比加了缓存)

热点代码:

当方法或者代码块在一定时间内的调用次数超过JVM规定的阈值,则会被编译,存入codeCache中。

codeCache:

代码缓存区,主要存放JIT所编译的代码,同时还有Java所使用的本地方法代码也会存储在codecache中,例如Object.wait(),Object.notify()等被native修饰的都是所谓的本地方法。

Java编译的整个过程为:

一. JVM编译器

JVM有两种编译器:

  • Client Compiler(C1编译模式):着重于启动速度和局部优化。
  • Server Compiler(C2编译模式):着重于全局优化。性能更好,但是启动速度慢。

对于HotSpot而言,一共有三种编译模式:

  • 混合模式(Mixed Mode):C1和C2编译模式混合起来使用(默认的模式),若希望单独使用C1编译模式或者C2编译模式,分别使用-client,-server参数来打开。
  • 解释模式(Interpreted Mode):
  • 编译模式(Compiled Mode):

输入命令java -vsersion可以查看当前的编译模式:

1.1 Client Compiler

HotSpot VM中的Client Compiler为C1编译器,启动速度快,主要做三件事情:

  • 局部优化,例如字节码上的基础优化、方法内联、常量传播。
  • 将字节码构造成高级中间表示(HIR)。
  • 将HIR转换成低级中间表示(LIR),在LIR基础上进行局部优化,生成机器码。
1.2 Server Compiler

主要关注一些编译比较耗时的全局优化,适用于长时间运行的后台程序,其性能一般比Client Compiler要高30%以上,Server Compiler有两种。

1.2.1 C2 Compiler

C2是Hotspot的默认Server编译器,C2使用一种控制流和数据流相结合的图数据结构(Ideal Graph,后面简称IG),其表示当前程序的数据流向和指令间的依赖关系。而C2则通过这种图结构来优化步骤。

1.2.2 Graal Compiler

启用方式:

-XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler

与C2编译器相比而言,Graal有这么几个关键特性:

  1. 由Java编写,对于lambda表达式、stream流的使用要更加友好。
  2. 会做虚函数的内联、部分逃逸分析等更深层次的优化。
  3. 对于代码的分支预测和选择优化要更好。

总结就是:
Java执行过程有两步:

  1. 前端编译:由javac编译成字节码。
  2. 后端编译:由解释器将字节码解释为机器码来执行。

后端编译有两种执行方式:

  1. 解释执行:一行一行解释成机器码再执行,每次调用时都需要重新逐条解释执行。
  2. 编译执行JIT(Just In Time):将热点代码编译优化成codeCache。

JVM虚拟机有两种编译器:Client / Server Compiler

比较项Client CompilerServer Compiler
启动速度
包含的编译器C1C2,Graal
性能一般高30%
主要的工作字节码优化、方法内联。将字节码转HIR,再转LIR主要关注一些编译耗时较长的全局优化,甚至会还会根据程序运行的信息进行一些不可靠的激进优化
优化范围局部优化全局优化
二. JIT和编译优化

JIT的触发时机:JVM根据某个方法的调用次数和循环回边的执行次数来触发。当两者的次数和超过-XX:CompileThreshold指定的阈值,则触发JIT编译。

C1的默认阈值为1500.
C2的默认阈值为10000.

JIT在进行编译的时候,会做一些优化操作。

2.1 中间表达形式(IR)

首先,编译的阶段由上文我们知道分为了:

  • 前端:通过词法分析、语法分析、语义分析生成中间表达形式(IR),例如字节码。
  • 后端:对IR进行优化,生成目标代码。

那么这些IR优化一般有哪些内容呢?

  • 识别冗余赋值。
  • 删除无用代码。
2.2 方法内联

方法内联的含义:在编译过程中遇到方法调用时,将目标方法的方法体纳入编译范围之中,并取代原方法调用的优化手段。 并且JIT大部分的优化都是在内联的基础上进行的。

以get/set为例,区别如下:

  • 无方法内联:调用时,程序执行时需要保存当前方法的执行位置,创建并压入用于getter/setter的栈帧、访问字段、弹出栈帧,最后再恢复当前方法的执行。
  • 有方法内联:程序执行到对应的位置时,直接进行字段访问即可。

方法内联的优劣势:

  • 优势:内联的方法越多,生成代码的执行效率越高。
  • 劣势:内联的方法越多,JIT编译时间也就越长
2.3 逃逸分析

逃逸分析可以分析在程序的哪些地方可以访问到指针,JIT会对新建的对象进行逃逸分析,判断对象是否逃逸出线程或者方法。 判断对象是否逃逸的依据有两种:

  • 对象是否存入堆中,若存入,则其他线程便能获得这个对象的引用。
  • 对象是否被传入未知代码(未被内联的代码)中,此时可认为方法调用的调用者以及参数是逃逸的。
2.4 Loop Transformations

JIT编译器会对循环做一些转换,其中最重要的是循环展开和循环分离。

循环展开:以牺牲程序二进制码大小为代价来优化程序的执行速度,通过减少或消除控制程序循环的指令,来减少计算开销(空间换时间)。

例如:

public void test(){
  for(int i = 0;i<100;i++){
    run(i);  
  }
}

经过循环展开后得到:

public void test(){
  for(int i = 0;i<100;i+=5){
    run(i);  
    run(i+1);  
    run(i+2);  
    run(i+3);  
    run(i+4);  
  }
}

效果:减少循环次数


循环分离:把循环中一次或多次的特殊迭代分离出来,在循环外执行。

例如:
原本的代码如下:

int a = 10;
for(int i = 0;i<10;i++){
  b[i] = x[i] + x[a];
  a = i;
}

经过循环分离后:(将特殊情况第一次a=10分离了出来)

b[0] = x[0] + 10;
for(int i = 1;i<10;i++){
  b[i] = x[i] + x[i-1];
}
2.5 窥孔优化与寄存器分配

窥孔优化作为最后一步JIT优化。将编译器所生成的中间代码的某些组合替换为效率更高的指令组。

例如:

y=x*3 

变成:(移位的效率更高)

y=(x<<1)+x

寄存器分配(C2):把频繁使用的变量保存在寄存器中,CPU访问寄存器的速度比内存快得多,就可以提升程序的运行速度。

在窥孔优化和寄存器分配结束后,程序就会被转换成机器码保存到codecache中了。

参考:(详细介绍的请看第一篇文章)

  • 基本功 | Java即时编译器原理解析及实践
  • JVM学习笔记之CodeCache
转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/354002.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号