【内功修炼系列2】难啃的JIT

1,404 阅读9分钟

前言

“编译一次,到处运行”。上大学刚接触这个概念的时候,感觉Java特别牛逼,那个时候单纯的自己还以为,只要学会Java,就解决了一切软件开发问题,整个世界都belongs to me。当年第一次写Hello World的时候,打开客户端窗口用javac编译.java文件,生成了我人生第一个.class文件,之后用java命令执行,看到屏幕有HelloWorld输出。这时候心里一笑,嘴角微微上扬,然后对自己说:“热心的朝阳群众,你是软件工程师了!”。你有过这样的经历吗?

那么这种跨平台的特性是如何来理解呢?其实Java语言的跨平台特性与JVM(Java虚拟机)的存在密不可分。JVM屏蔽了与具体操作系统平台相关的信息,JVM会将字节码翻译为相应平台的计算机指令,即:0、1序列。不同的平台要安装该平台的JVM。这就好比你讲中文(.java),Java编译程序相当于翻译软件,帮你翻译为英文文件(.class),这份英文文件传到各个国家之后,再由当地看得懂英文的人(JVM)翻译为当地语言(机器指令)。

编译器和解释器

编译器

Java compiler reads source files written in the Java programming language, and compiles them into bytecode class files.

将Java源文件(.java文件)编译成字节码文件(.class文件),Linux操作系统下的javac.sh可以简单看成是Java编译器。编译分为:动态编译和静态编译。

  • 动态编译(dynamic compilation)指的是“在运行时进行编译”,程序运行的时候才编译。
  • 静态编译(ahead-of-time compilation,简称AOT)指的是“一次性编译”,所有模块一次性编译。

解释器

Java compilers generate machine-independent bytecodes instead of machine instructions. The interpreter is like a CPU implemented in software. It decodes and executes bytecodes, independent of what computer they were compiled on.

Java解释器:Java解释器用来解释执行Java编译器编译后的程序。Linux操作系统下的java.sh可以简单看成是Java解释器。

执行过程

Jvm会将字节码文件交给解释器,翻译成机器码,由解释器执行。解释器是把高级语言一行一行直接翻译再运行,它不会一次性把整个文件都翻译过来,而是翻译一句,执行一句,再翻译,再执行,所以解释器的程序运行起来会比较慢,每次都要解释之后再执行。JVM解释执行字节码文件就是:JVM操作Java解释器进行解释执行字节码文件的过程。这样保证了在任何平台上,都可以成功的执行Java程序。如图所示:

JIT概念

JIT:Just In Time Compiler,一般翻译为即时编译器,这是是针对解释型语言而言的,而且并非虚拟机必须,是一种优化手段,Java的商用虚拟机HotSpot就有这种技术手段,Java虚拟机标准对JIT的存在没有作出任何规范,所以这是虚拟机实现的自定义优化技术。

JIT(即时编译)是用来提高java程序运行效率的,原本字节码由解释器需要经过解释再运行。上面也提到了,这会影响整体运行速度。现在有了JIT技术,将字节码编译成平台相关的原生机器码,并进行各个层次的优化,这些机器码会被缓存起来,以备下次使用。如果JIT对每条字节码都进行编译和缓存,会增加JVM开销.因此JIT只对热点代码进行即时编译,如循环,高频度使用的方法,会将整个方法编译成本地机器码,然后直接运行机器码。完整流程图如下:

面试点:一般我面试的时候,都会问Java类加载的过程,验证、准备、解析这三点也要搞清楚。

JIT原理

在这里也顺便介绍下HotSpot,我们平时说的HotSpot其实是HotSpot VM,大多数码农都应该听说过,它是Sun JDK和OpenJDK中所带的虚拟机,也是目前使用范围最广的Java虚拟机。HotSpot VM的热点代码探测能力可以通过执行计数器找出最具有编译价值的代码,然后通知JIT编译器以方法为单位进行编译。

HotSpot同时包含解释器和JIT,两者各有优势:当程序需要迅速启动和执行的时候,解释器可以首先发挥作用,省去编译的时间,立即执行。在程序运行后,随着时间的推移,JIT逐渐发挥作用,把越来越多的代码编译成本地代码之后,可以获取更高的执行效率。当程序运行环境中内存资源限制较大,可以使用解释器执行节约内存,反之可以使用编译执行来提升效率。我们平时所说到的JIT比解释器快,其实说的是执行编译后的代码比解释器解释执行要快,并不是说编译比解释这个动作快。

那么说完这个道理后,究竟哪些代码会被认定为热点代码,被编译成本地代码呢?

  • 被多次调用的方法
  • 被多次执行的循环体

面试点:有一些在面试场景下,面试官其实会大概问了一下这个概念,其实能回答出这两点,基本就说明你至少是了解过JIT。

热代码的判断过程

在HotSpot虚拟机中使用的基于计数器的热点探测方法,因此它为每个方法准备了两个计数器:方法调用计数器和回边计数器。在确定虚拟机运行参数的前提下,这两个计数器都有一个确定的阈值,当计数器超过阈值溢出了,就会触发JIT编译。——《深入理解Java虚拟机》

我们可以理解为,方法被调用时,会先检查该方法是否存在被JIT编译过的版本,如果存在,则优先使用编译后的本地代码来执行。如果不存在已被编译过的版本,则将此方法的调用计数器值加1,然后判断方法调用计数器与回边计数器值之和是否超过方法调用计数器的阈值。如果超过阈值,那么将会向即时编译器提交一个该方法的代码编译请求。

回变计数器:它的作用就是统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令称为“回边”。

JIT编译为本地代码

编译分为两种:Server Compiler和Client Compiler。

  • Client Compiler是一个简单快速的编译器,主要关注点在于局部优化,而放弃许多耗时较长的全局优化手段。
  • Server Compiler是专门面向服务器端的,并为服务端的性能配置特别调整过的编译器,是一个充分优化过的高级编译器。

Client Compiler简称C1编译器,较为轻量,只做少量性能开销比较高的优化,它占用内存较少,适合于桌面交互式应用,可以使用"-client"参数强制选择运行在Client模式。

Client Compiler是一个简单快速的三段式编译器,主要关注点在于局部性的优化,而放弃了许多耗时较长的全局优化。在寄存器分配策略上,JDK6以后采用的为线性扫描寄存器分配算法,其他方面的优化,主要有方法内联、去虚拟化、冗余消除等;

Server Compiler简称C2编译器,也叫Opto编译器,它较为重量,采用了大量传统编译优化的技巧来进行优化,占用内存相对多一些,适合服务器端的应用。可以使用"-server"参数强制选择运行在Server模式。

Server Compiler会执行所有经典的优化动作,如无用代码消除、循环展开、循环表达式外提、消除公表达式、常量传播、基本块重排序等。还会一些与Java语言特性密切相关的优化技术,如范围检查消除、空值检查消除等。另外,还进行一些不稳定的激进优化,如守护内联、分支频率预测等。

JIT阈值

JIT只在代码段执行足够次数才会进行优化,在执行过程中不断收集各种数据,作为优化的决策,所以在优化完成之前,对象内存还是在堆上进行分配。可以由-XX:CompileThreshold参数进行设置:

  1. 使用client编译器时,默认为1500;
  2. 使用server编译器时,默认为10000;

设置完之后如果方法调用次数或循环次数达到这个阈值就会触发标准编译,更改CompileThreshold标志的值,将使编译器提早(或延迟)编译。

结尾

JIT这段内容是JVM内容中,相对底层的知识,如果你是负责中间件开发人员,可能会用在一些调优的场景。如果你只是开发业务应用的话,这部分内容可能就。。。。有点难接触到。不管怎样,既然我总结出来了这些内容,还是希望大家能够看一遍,并了解其内容,也许你在未来的某个场景会用到。如果理解有误之处,还请大家指正。希望大家继续支持!

往期文章:

《【内功修炼系列1】线性数据结构(上篇)》

《【内功修炼系列1】线性数据结构(下篇1)》

《【内功修炼系列1】线性数据结构(下篇2)》

《【内功修炼系列2】给JVM把个脉》

特别声明

本文为原创文章,如需转载可与我联系,标明出处。谢谢!