jjzjj

【jvm系列-07】深入理解执行引擎,解释器、JIT即时编译器

huisheng_qaq 2024-03-12 原文

JVM系列整体栏目


内容链接地址
【一】初识虚拟机与java虚拟机https://blog.csdn.net/zhenghuishengq/article/details/129544460
【二】jvm的类加载子系统以及jclasslib的基本使用https://blog.csdn.net/zhenghuishengq/article/details/129610963
【三】运行时私有区域之虚拟机栈、程序计数器、本地方法栈https://blog.csdn.net/zhenghuishengq/article/details/129684076
【四】运行时数据区共享区域之堆、逃逸分析https://blog.csdn.net/zhenghuishengq/article/details/129796509
【五】运行时数据区共享区域之方法区、常量池https://blog.csdn.net/zhenghuishengq/article/details/129958466
【六】对象实例化、内存布局和访问定位https://blog.csdn.net/zhenghuishengq/article/details/130057210
【七】执行引擎,解释器、JIT即时编译器https://blog.csdn.net/zhenghuishengq/article/details/130088553

深入理解执行引擎,解释器、JIT即时编译器

一,深入理解执行引擎

1,执行引擎的概述

在JVM整个体系中,执行引擎属于第三层,主要用来执行具体的字节码文件。本文主要探讨的就是这个执行引擎。

执行引擎是Java虚拟机核心组成的一部分,“虚拟机” 是一个相对于 “物理机” 的一个概念,这两种机器都有执行代码的能力,其区别是物理机的执行引擎是直接建立在处理器、缓存、指令集和操作系统的层面上的,而虚拟机的执行引擎是由软件自行实现的,因此可以不受物理条件制约的指令集与执行引擎的结构体系,能够执行那些不被硬件直接支持的格式。java虚拟机可以理解成一个抽象的计算机,相较于真正的物理机而言,java虚拟机的执行效率会略慢于物理机。

JVM的主要任务是负责装载字节码到其内部,但字节码并不能够直接运行在操作系统上面,因为字节码指令并非等价于本地机器指令,他内部包含的仅仅是一些能够被JVM识别的字节码指令等信息。如下图所示,这些字节码指令不能直接在操作系统上解释执行,而是需要现通过jvm虚拟机来执行这些字节码指令。

因此,执行引擎的主要作用就是:将字节码指令解释成或者编译成对应平台上面的本地机器指令 ,简单的来说,JVM中的执行引擎充当了将高级语言翻译成机器语言的翻译者

执行引擎在执行过程中,其需要的具体的字节码指令完全依赖于程序计数器,每当完成一项操作指令之后,程序计数器就会更新下一条需要被执行的指令地址。在方法的执行全过程中,执行引擎有可能会通过存储在局部变量表的对象引用准确的的获取存储在Java堆中的对象实例信息,以及通过对象头中的元数据指针定位到目标对象的类型信息。

2,Java代码编译和执行的过程

大部分的程序代码在转换成物理机的目标代码或者虚拟机能执行的指令集之前,都需要经历过几下几个步骤

🧢 前面的黄线流程代表的就是将 .java 文件编译成 .class 文件,属于是前端编译;

🧢 绿色部分属于解释器解释执行的过程,即逐行翻译、解释、执行的过程;

🧢 蓝色部分属于是JIT即时编译器编译性阶段,属于是后端编译。

2.1,解释器和编译器

解释器:当Java虚拟机启动的时候,会根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件中的内容翻译成对应平台的本地机器指令

JIT编译器:jit,又名Just In Time Compiler , 就是直接将源代码编译成和本地平台相关的机器语言。

在java语言中,是既可以通过解释器来执行代码,也可以通过编译器来执行代码的,这二者都可以达到相同的目的,并且这二者以合作的方式相辅相成,取长补短,以最合适的方法让Java内部执行的效率更高。JVM虚拟机不仅仅是针对于Java语言,只要遵循Jvm虚拟机规范的语言,都可以使用JVM虚拟机解释执行。

如上图,将不同的语言通过统一处理,生成对应的字节码文件,然后通过虚拟机中的解释器或者JIT即时编译器对这些字节码进行解释执行,然后翻译成对应的字节码指令,最后将这些指令全部存储在方法区的CodeCache中。

2.2,机器码、指令、汇编语言、高级语言

1,机器码

各种用二进制编码方式表示的指令,叫做 机器指令码 ,如通过01010101 这种二进制的方式进行编码,最开始人们就用它编写程序,这就是 机器语言。机器语言虽然可以被计算机接收,但是和人们的语言差别太大,不易被人家理解和记忆,用它变成也容易出错。用它编写的程序,一经输入计算机,CPU直接读取运行,因此和其他语言的程序,执行速度最快。机器指令和CPU紧密相关,因此不同类型的CPU所对应的机器指令也就不同。

2,指令

由于机器码是由0和1的二进制组成,可读性实在是太差,于是人们发明了指令。指令就是把机器码特定的 0和1 序列,简化成了对应的指令,如mov和inc等,可读性好。但是由于不同的硬件平台,执行同一个操作,其对应的字节码可能会不同,所以不同硬件平台的同一种指令,对应的机器码也可能不同。在不同的硬件平台,各自支持各自的指令,每个平台所支持的指令总和,称之为对应平台的 指令集

3,汇编语言

又由于指令的可读性差,于是又发明了这个汇编语言。在汇编语言中,用助记符代替机器指令的操作码,用地址符号代替指令或者操作数的地址。在不同的硬件平台,汇编语言对应着不同的机器语言指令集,通过汇编过程转换成机器指令,由于计算机只认识指令码,所以用汇编语言编写的程序还必须翻译成机器指令码,计算机才能识别。

4,高级语言

高级语言比上述语言接近人的语言,如当今流行的c或者c++,当计算机执行高级语言的时候,仍然需要把程序解释或者编译成机器指令码,完成这个过程的程序就叫做解释程序或者编译程序。因此不管是汇编语言还是这个高级语言,都需要最终生成这个机器指令,然后将这个机器指令放在CPU上面操作,最终解释执行。

字节码属于是一种中间状态的二进制代码,他比机器码更加抽象,需要直译器转译后才能成为机器码,与硬件环境无关,可以直接通过编译器或者虚拟机器,将源码编译成字节码。

2.3,解释器和编译器工作机制(重点)

解释器真正意义上所承担的角色就是一个 “运行时的翻译者”,就是将字节码中的内容翻译成对应平台的本地机器指令执行。每当一条字节指令被解释执行完成后,接着再根据 程序计数器 中记录的下一条需要被执行的字节码指令执行解释操作。

在JVM平台中,也对解释器进行了优化,采用了一种JIT 的即时编译的技术,目的是避免函数被解释执行,而是将整个函数体编译成机器码,每次函数执行时,只执行编译后的编译码即可,这种方式大大的提升了执行效率。

在hotspot虚拟机中,JIT即时编译器的速度远快于解释器,并且将字节码指令直接生成机器指令,存储在这个方法区的CodeCache中缓存起来,比这个解释器逐行翻译的效率高很多。因此在今天,Java程序的运行性能早以脱胎换骨,已经可以达到和c/c++程序一较高下的地步。

但是即使这个jit即时编译器的速度很快,在HotSpot虚拟机中,依旧保留了这个解释器,原因是JIT即时编译器虽然效率很高,但是需要一定的时间编译成机器码,才能继续工作。但是这个编译器在程序启动之后,可以立马进行工作,省去编译的时间,立即执行。

所以综上两点,在程序启动的时候JIT需要编译,那么就由解释器来执行程序,待JIT即时编译器编译成机器码之后,再由这个JIT即时编译器来完成,这样就能让整个执行引擎发挥最大的效率。因此二者合作共存才能让效率最大化。

2.4,JIT编译器的热点代码和热点探测

Java语言的编译器其实是一段不太确定的操作过程,因为他可能是一指前端编译器(编译器的前端,.java文件编译成 .class文件)的过程,也可能是指后端的编译器(JIT编译器,将字节码转换成机器码)的过程,还有可能是指静态提前编译器,直接把 .java 文件编译成本地机器代码的过程。

在使用这个JIT编译器的时候,需要判断代码被调用执行的频率,对于需要被编译为本地代码的字节码,被称为热点代码 ,JIT编译器在运行时对那些频繁被调用的热点代码会做出深度优化,将其直接编译为对应平台的本地机器指令,以提升Java程序的执行性能。

热点代码 :指的是一个被多次调用的方法,或者是一个方法体内部循环次数较多的循环体,都可以被称为"热点代码"。因此可以通过JIT编译器译为本地机器指令,由于这种编译方式发生在方法的执行过程中,因此也被称为栈上替换。

热点探测方式:而是否可以成为这个热点代码,主要是依靠这个热点探测功能,HotSpot虚拟机主要采用的热点探测方式是基于计数器的热点探测。HotSpot虚拟机又将每个方法建立两个不同类型的计数器,分别是方法调用计数器和回边计数器,方法调用计数器用于统计方法的调用次数,回边计数器用于统计循环体的执行次数。

2.5,方法调用计数器和回边计数器

在JIT的热点探测中,主要是通过计数器的方式来实现对代码的探测,计数器主要分为方法调用计数器和回边计数器。

2.5.1,方法调用计数器

这个计数器主要用于统计方法被调用的次数,它的默认阈值在Client模式下是1500次,在Server模式下是10000次,超过这个阈值,就会触发JIT编译。这个阈值也可以通过虚拟机参数 -XX:CompileThreshold进行设置。当一个方法被调用的时候,会先检查这个方法是否存在被JIT编译过的版本,如果存在,则优先使用编译后的本地代码来执行,如果不存在,则将此方法的调用计数器值加1,然后判断 方法调用计数器和回边计数器 值的和是否超过方法调用计数器的阈值,如果已经超过阈值,那么将会向即时编译器提交一个该方法的代码编译请求。

如上图所示,在调用方法时,会先判断该代码是否已经编译,如果已经编译,则直接通过这个JIT即时编译器将机器码生成对应的本地机器码指令;如果未编译,则将方法调用计数器加1,随后回去判断是否超过阈值,如果超过阈值,则会提交编译请求,通过JIT即时编译器进行动态编译,然后将编译后的机器指令缓存在CodeCache中,如果未超过阈值,那么继续通过解释器解释执行。

在JVM内部对调用的次数也做了一定的限制,并不是说一直对调用的次数进行类加,而是在一段时间内记录方法调用的次数,当超过一定的时间限度,如果方法调用的次数依旧没有达到这个阈值,那么方法的调用计数器就会进行一个 衰减 的过程,每次衰减一半,这段衰减的过程被称为方法统计的 半衰周期

进行衰减的动作是虚拟机在垃圾收集的时候顺便进行的,可以使用虚拟机参数 -XX:-UseCounterDecay 来关闭或者开启热度衰减,因此只要系统运行的时间足够长,那么绝大多数的方法都会编译成本地代码。同时也可以通过参数 -XX:CounterHalfLifeTime 设置半衰周期的时间,单位是s

2.5.2,回边计数器

主要是统计一个方法中的循环体的执行次数,在字节码中遇到流控流向后跳转的指令称为 “回边” 。

和方法调用计数器一样,会先判断一下该代码是否已经编译,如果未编译,则回边计数器的值加1,然后去判断将当前累加的值和方法调用计数器的值进行累加是否超过阈值,如果超过,则使用JIT编译器,否则依旧使用解释器执行。

2.6,编译器和解释器设置

上述可知在HotSpot虚拟机中存在解释器和编译器,如通过以下命令可以得知,当前虚拟机采用的是一种混合的方式共同执行程序。

java -version

除了这种之外,也可以通过显式的命令为Java虚拟机指定只由其中一种执行程序,如可以通过以下这个命令设置只使用解释器执行程序

java -Xint -version

或者可以通过以下命令只设置使用编译器来执行程序,但是如果编译出现问题,解释器会接入执行

java -Xcomp -version

当然上面这两种需要在特殊的场景下使用,需要变回混合使用

java -Xmixed -version

除了可以通过这个命令行设置之外,也可以通过这个虚拟机参数就行设置,其代码如下,通过虚拟机的不同参数设置,可以得到以下答案,纯解释需要花8666ms,纯编译只需要花2ms,混合使用也是1-2ms,因此选择这个混合是最佳的,同时也可以知道使用这个纯编译器的时间远远小于这个纯解释型。

/**
 *
 * -Xint : 8666ms
 * -Xcomp:2ms
 * -Xmixed: 2ms
 * @author zhenghuisheng
 * @date : 2023/4/11
 */
public class C {
    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        test();
        long end = System.currentTimeMillis();
        System.out.println((end - start) + "ms");
    }

    public static void test(){
        int k = 0;
        for (int i = 0; i < 1000000; i++) {
            for (int j = 0; j < 1000; j++) {
                k = i + j;
            }
        }
    }
}

在虚拟机设置那里修改对应的参数即可。

而在HotSpot虚拟机中内嵌有两个JIT的编译器,分别是Client Compiler和Server Compiler,但是在绝大多数的情况下,这两个编译器被称为C1编译器和C2编译器。
🧢 -client :运行在Client模式下,对字节码进行可靠和简单的优化,耗时短
🧢 -server:运行在Server模式下,对字节码进行耗时长的优化、激进优化,效率更高

C1编译器的优化策略主要有:方法内联、去虚拟化、冗余消除
C2编译器的优化策略主要有:标量替换、栈上分配、同步消除

有关【jvm系列-07】深入理解执行引擎,解释器、JIT即时编译器的更多相关文章

  1. python - 如何使用 Ruby 或 Python 创建一系列高音调和低音调的蜂鸣声? - 2

    关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。

  2. ruby-openid:执行发现时未设置@socket - 2

    我在使用omniauth/openid时遇到了一些麻烦。在尝试进行身份验证时,我在日志中发现了这一点:OpenID::FetchingError:Errorfetchinghttps://www.google.com/accounts/o8/.well-known/host-meta?hd=profiles.google.com%2Fmy_username:undefinedmethod`io'fornil:NilClass重要的是undefinedmethodio'fornil:NilClass来自openid/fetchers.rb,在下面的代码片段中:moduleNetclass

  3. ruby-on-rails - 使用一系列等级计算字母等级 - 2

    这里是Ruby新手。完成一些练习后碰壁了。练习:计算一系列成绩的字母等级创建一个方法get_grade来接受测试分数数组。数组中的每个分数应介于0和100之间,其中100是最大分数。计算平均分并将字母等级作为字符串返回,即“A”、“B”、“C”、“D”、“E”或“F”。我一直返回错误:avg.rb:1:syntaxerror,unexpectedtLBRACK,expecting')'defget_grade([100,90,80])^avg.rb:1:syntaxerror,unexpected')',expecting$end这是我目前所拥有的。我想坚持使用下面的方法或.join,

  4. ruby - 在没有 sass 引擎的情况下使用 sass 颜色函数 - 2

    我想在一个没有Sass引擎的类中使用Sass颜色函数。我已经在项目中使用了sassgem,所以我认为搭载会像以下一样简单:classRectangleincludeSass::Script::FunctionsdefcolorSass::Script::Color.new([0x82,0x39,0x06])enddefrender#hamlengineexecutedwithcontextofself#sothatwithintemlateicouldcall#%stop{offset:'0%',stop:{color:lighten(color)}}endend更新:参见上面的#re

  5. ruby - Chef 执行非顺序配方 - 2

    我遵循了教程http://gettingstartedwithchef.com/,第1章。我的运行list是"run_list":["recipe[apt]","recipe[phpap]"]我的phpapRecipe默认Recipeinclude_recipe"apache2"include_recipe"build-essential"include_recipe"openssl"include_recipe"mysql::client"include_recipe"mysql::server"include_recipe"php"include_recipe"php::modul

  6. ruby - 即时确定方法的可见性 - 2

    我正在编写一个方法,它将在一个类中定义一个实例方法;类似于attr_accessor:classFoocustom_method(:foo)end我通过将custom_method函数添加到Module模块并使用define_method定义方法来实现它,效果很好。但我无法弄清楚如何考虑类(class)的可见性属性。例如,在下面的类中classFoocustom_method(:foo)privatecustom_method(:bar)end第一个生成的方法(foo)必须是公共(public)的,第二个(bar)必须是私有(private)的。我怎么做?或者,如何找到调用我的cust

  7. ruby - 为什么 Ruby 的 each 迭代器先执行? - 2

    我在用Ruby执行简单任务时遇到了一件奇怪的事情。我只想用每个方法迭代字母表,但迭代在执行中先进行:alfawit=("a".."z")puts"That'sanalphabet:\n\n#{alfawit.each{|litera|putslitera}}"这段代码的结果是:(缩写)abc⋮xyzThat'sanalphabet:a..z知道为什么它会这样工作或者我做错了什么吗?提前致谢。 最佳答案 因为您的each调用被插入到在固定字符串之前执行的字符串文字中。此外,each返回一个Enumerable,实际上您甚至打印它。试试

  8. ruby-on-rails - Rails 中的推荐引擎 - 2

    我想为我的Rails网络应用程序提供推荐功能。特别是,我想向新注册的用户推荐他可能想要关注的其他用户。Rails中是否有用于此目的的引擎/gem?如果没有,我应该从哪里开始构建它?谢谢。 最佳答案 有Coletivogemhttps://github.com/diogenes/coletivo我试了一下。在MySQL上运行。Neo4jhttp://neo4j.org真的很容易实现一个“跟随谁”。事实上,大多数展示其能力的样本都涉及“跟随谁”。快速提示-只有在JRuby上运行时,Neo4j.rb才会很酷。如果不是-使用Neograph

  9. ruby - 检查是否通过 require 执行或导入了 Ruby 程序 - 2

    如何检查Ruby文件是否是通过“require”或“load”导入的,而不是简单地从命令行执行的?例如:foo.rb的内容:puts"Hello"bar.rb的内容require'foo'输出:$./foo.rbHello$./bar.rbHello基本上,我想调用bar.rb以不执行puts调用。 最佳答案 将foo.rb改为:if__FILE__==$0puts"Hello"end检查__FILE__-当前ruby​​文件的名称-与$0-正在运行的脚本的名称。 关于ruby-检查是否

  10. 世界前沿3D开发引擎HOOPS全面讲解——集3D数据读取、3D图形渲染、3D数据发布于一体的全新3D应用开发工具 - 2

    无论您是想搭建桌面端、WEB端或者移动端APP应用,HOOPSPlatform组件都可以为您提供弹性的3D集成架构,同时,由工业领域3D技术专家组成的HOOPS技术团队也能为您提供技术支持服务。如果您的客户期望有一种在多个平台(桌面/WEB/APP,而且某些客户端是“瘦”客户端)快速、方便地将数据接入到3D应用系统的解决方案,并且当访问数据时,在各个平台上的性能和用户体验保持一致,HOOPSPlatform将帮助您完成。利用HOOPSPlatform,您可以开发在任何环境下的3D基础应用架构。HOOPSPlatform可以帮您打造3D创新型产品,HOOPSSDK包含的技术有:快速且准确的CAD

随机推荐