【Java虚拟机】第三章、jvm运行期优化,解释器,编译器(AOT静态编译,JIT动态编译)

2023-11-14

    已经第三章了,看了前两章是不是有点懵?或者开始意识到了什么?或者整个串联起来了?回顾一下

第一张主要讲的是jvm怎么创建

第二章讲的是jvm内存结构

和番外篇class加载过程

    那么我们再结合这一章解释器和编译器,静态和动态编译,把他们串到1起,简单的总结下jvm被创建后是如何运行的。之后我们要开始学习GC优化了。

    都知道写的好的C/C++运行效率很高,殊不知JAVA也在这方面做努力,下面主要讲解的是java是如何编译和解释上做的努力。首先了解下解释器和编译器。

解释器

    java的语言怎么在windows,os平台上识别呢?就用了解释器从中间转换,要不然机器肯定不知道你要干啥,我讲中文,你只能听懂阿拉伯语,这时候就需要解释器了。但是这种方式有几个老哥举了几个很直白的例子:

    牛吃草,一个草原上,三种牛吃不同的用碾碎机碾碎的三种草,解释器就是要根据不同的牛,碾碎不同的草,给特定的牛吃。每天都要先知道都有什么牛,我要什么草,什么草对应什么牛,这种方法很麻烦。

    牛:各种cpu

    草:java,php等语言

   碾碎机:解释器

    不知道我讲的清晰吗?以上讲的是语言,解释器,机器的关系。官方的定义:直接执行用编程语言编写的指令程序,容易让用户实现程序的跨平台,如java,php等。同一套代码可以在多个机器上运行,而无需根据操作系统进行更改。

    从官方的定义来看,一套代码就可以在多个机器上运行,表面看上去很强大,但其实开销很大,因为每次一个程序执行一次就要被解释一次。

    上面讲到了解释器的作用,讲的比较通俗,再讲一下具体为什么需要解释器吧。因为基于堆栈的本地平台很少,所以大多数本地平台不能直接执行java字节码。为了解决这个问题,早期的jre通过解释字节码来运行JAVA程序。即JVM在一个循环中重复操作:

  1. 获取待执行的下一个字节码
  2. 解码
  3. 从操作数栈获取所需的操作数
  4. 按照JVM规范执行操作
  5. 将结果写回堆栈

这种方法的优点是其简单性:JRE开发人员只要编写代码来处理每种字节码就可以了,并且因为用于描述操作的字节码于255个,所以实现的成本较低。同样比较遗憾的是性能也较低。

要解决与C/C++之间的性能差距意味着,使用不会牺牲可移植性的方式开发用于java平台的本地代码编译

编译器

和解释器不同的是,编译器先把代码编译成机器码,然后每次一段程序运行时,都只运行机器码部分。过程为:源代码 (source code) → 预处理器 (preprocessor) → 编译器 (compiler) → 目标代码 (object code) → 链接器(Linker) → 可执行程序 (executables)。

参考:https://blog.csdn.net/sunxianghuang/article/details/52094859

1、动态编译(dynamic compilation)指的是“在运行时进行编译”;与之相对的是事前编译(ahead-of-time compilation,简称AOT),也叫静态编译(static compilation)。

2、JIT编译(just-in-time compilation)狭义来说是当某段代码即将第一次被执行时进行编译,因而叫“即时编译”。JIT编译是动态编译的一种特例。JIT编译一词后来被泛化,时常与动态编译等价;但要注意广义与狭义的JIT编译所指的区别。

3、自适应动态编译(adaptive dynamic compilation)也是一种动态编译,但它通常执行的时机比JIT编译迟,先让程序“以某种式”先运行起来,收集一些信息之后再做动态编译。这样的编译可以更加优化。

在部分商用虚拟机中(如HotSpot),Java程序最初是通过解释器(Interpreter)进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁时,就会把这些代码认定为“热点代码”。为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器(Just In Time Compiler,下文统称JIT编译器)。

即时编译器并不是虚拟机必要的,但是即时编译器的好坏,优化的程度高低决定了jvm的性能优秀与否的关键之一。它也是虚拟机最核心也是最能体现虚拟机技术水平的部分。

一般情况下解释器和jit编译器是并存的,那么这种方式存在的意义是什么呢?

尽管不是所有的虚拟机都是这种架构,但是许多主流的商用机都是这样实现的。解释器和编译器两者各有优势,当程序需要尽快启动和执行的时候,这时候解释器可以首先发挥作用,省去编译时间,随着程序运行时间越长,当代码中有部分的代码需要不断执行的时候,这时候编译器就发挥了作用,他会把更多的代码编译成本地代码,之后可以获得更高的执行效率。当程序运行环境中内存区域分配限制较大,可以考虑更多的使用解释执行节约内存,反之则使用编译执行提升效率。此外,如果编译后出现“罕见陷阱”,可以通过逆优化退回到解释执行。

解释器与编译器的交互

编译的时间开销

解释器的执行,抽象看是这样的:

输入的代码=>[解释器 解释执行]=>执行结果

而要JIT编译然后再执行的抽象过程是这样的:

输入的代码=>[编译器 编译]=>编译后的代码=>[执行]=>执行结果

说JIT执行的比解释快,一般指的是编译后的运行比解释器运行的块,不包含编译的过程。

JIT编译的再快,至少比解释器执行一次要慢,而要得到最后的结果还需要执行编译后的代码,这一步骤。

所以对只执行一次的代码而言,解释执行总比编译执行要快,那么哪些属于只执行一次的代码呢?下面两个条件同时满足是,那就属于只执行一次的代码:

  1. 只被调用一次的代码,例如类的构造器(class initializer,<client>())
  2. 没有循环

对只执行一次的代码进行编译运行是得不偿失的。对只执行少量次数的代码,JIT编译带来的执行速度的提升也未必能抵消掉最初编译带来的开销。

只有对频繁执行的代码,JIT编译才能保证有正面的收益。

编译的空间开销

对一般的java方法而言,编译后代码的大小相比字节码大小,膨胀比大了10x是很正常的。同上面说的时间开销一样,这里的空间开销也是一样,只有对频繁执行的代码才有编译的意义。如果把所有的代码都进行编译运行,会显著的增大代码所占空间,导致“代码爆炸”。

 

这也就解释了为什么有些JVM会选择不总是做JIT编译,而是选择用解释器+JIT编译器的混合执行引擎。

为何HotSpot虚拟机会实现两种不同方式的即时编译器?

HotSpot中有两种即时编译器:client compile,server compile,简称C1和C2编译器,分别用在客户端和服务端。目前主流的虚拟机采用其中一种编译器和解释器一起工作。程序用哪种编译器取决于虚拟机运行的模式。HotSpot虚拟机会根据自身版本和宿主机器的硬件性能自动选择运行模式。用户也可以使用“-client”或“-server”参数去强制虚拟机使用何种编译器运行。

用client compile的优势为编译效率高C1主要关注点在于局部优化,而放弃许多耗时较长的全局优化手段。

用server compile的优势为编译质量高。C2则是专门面向服务器端的,并为服务端的性能配置特别调整过的编译器,是一个充分优化过的高级编译器。

哪些代码会被编译为本地代码?怎么编译为本地代码?

程序中的代码只要是“热点代码”时,就会编译为本地代码,那么什么叫热点代码?

  1. 被多次调用的方法
  2. 被多次执行的循环体

两种情况,编译器都是以整个方法作为编译对象。这种编译方法因为发生在方法执行过程中,所以形象的称为“栈上替换”(On Stack Replacement,OSR),即方法栈帧还在栈上,方法就被替换了。

热点代码的定义已经清楚了,那么类似于被多次调用这个行为是怎么判定的呢?这种判定的行为被称为“热点探测”。目前主要的热点探测方式有如下两种:

基于采样的热点探测

这个其实很好理解,采用这种方法的虚拟机会周期性的访问栈顶,当一个方法多次出现在栈顶时,会被虚拟机认为这个方法是“热点方法”。这个方法的好处就是简单高效,还很容易的获取方法调用关系(将调用堆栈展开),缺点是很难精确的确认一个方法的热度,容易因为线程阻塞等原因而扰乱热点探测。

基于计数器的热点探测

这个也很简单,就是虚拟机会为每个方法或者代码块建立计数器,统计方法的执行次数,如果执行次数超过一定阀值就认为他是热点方法。这种统计方式稍微比采样的方式更加复杂,需要为每个方法建立并维护计数器,而且不能直接获取到方法的调用关系,但是它的统计结果相对更加精确严谨。

HotSpot是采用的哪种热点探测?

使用的是第二种计数器的热点探测,它为每个方法提供了两种不同的计数器:方法调用计数器和回边计数器。在确定的虚拟机运行参数的情况下,这两个计数器都有一个阀值,超过这这两个计数器的阀值之和就是热点方法,就会触发JIT编译。

方法调用计数器

上面已经说过了这个计数器怎么回事。这个再根据这个计数器描述下,整个运行方式是如何的。首先,当一个方法被调用时,会检查这个方法是否存在被JIT编译过的版本。如果存在,则直接执行编译后的本地代码。如果不存在这个已被编译的版本,那么会在这个方法的调用计数器上+1,然后判断方法调用计数器和回边计数器的总和是否超过方法调用计数器的阀值,如果超过阀值,那么将会向即时编译器发送一个此方法需要被编译的请求。

如果不对编译请求做同步设置,那么执行引擎不会同步的等待编译请求完成,而是继续用解释器执行字节码,直到提交的编译请求完成,并且该方法的入口地址已经指向被编译的方法后(系统自动替换),下一次调用该方法就会执行被编译后的版本。

回边计数器

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

我们了解了解释器和JIT编译器,下面来说下JIT编译器和AOT编译器的区别吧。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【Java虚拟机】第三章、jvm运行期优化,解释器,编译器(AOT静态编译,JIT动态编译) 的相关文章

  • 如何在 Windows 中查看正在执行的 java 程序的类路径和 jvm 参数

    在 nix 我只是这样做ps ef grep java查看正在执行的 java 程序的 jvm 参数和类路径 如何在 Windows 命令提示符中看到它 我想看看某些 jar 是否实际上位于正在运行的 weblogic 服务器的类路径中 从
  • Docker 镜像可以在 Intel mac 上运行,但不能在 M1 mac 上运行

    我们有一个在 Docker 容器中运行的 Java Spring Boot 应用程序 它基于 openjdk 13 jdk alpine 我们将其部署到 Linux 计算机上 但我们也可以在 Windows 计算机以及基于 Intel 的
  • Java中的线程何时从内存中删除? [复制]

    这个问题在这里已经有答案了 来自 Java API 文档 Java虚拟机继续执行线程 直到遵循 发生 所有非守护线程的线程都已死亡 或者通过返回 从调用 run 方法或抛出异常 传播到 run 方法之外 我希望我的假设是正确的 一旦线程完成
  • 在消费者线程中使用 JNI 的生产者-消费者程序中无法捕获 SIGINT 信号

    我正在为生产者消费者问题编写一个程序 生产者生产数据并将数据推送到boost spsc queue消费者处理它 在消费者线程中 我正在使用JNI从我用 c 编写的代码中调用一些 java 函数 I am initializing and c
  • -XX:MaxPermSize 带或不带 -XX:PermSize

    我们遇到了一个Java lang OutOfMemoryError 永久代空间错误并查看 tomcat JVM 参数 除了 Xms and Xmx我们还指定了参数 XX MaxPermSize 128m 经过一些分析后 我可以看到 Perm
  • Docker 容器中运行的 JVM 的驻留集大小 (RSS) 和 Java 总提交内存 (NMT) 之间的差异

    设想 我有一个 JVM 在 Docker 容器中运行 我使用两种工具进行了一些内存分析 1 top 2 Java 本机内存跟踪 这些数字看起来令人困惑 我正在尝试找出造成差异的原因 问题 Java 进程的 RSS 报告为 1272MB 总
  • Java中静态字段的确切含义是什么?

    我想在同一类对象的各个实例之间共享一个对象 从概念上讲 当我的程序运行时 A 类的所有对象都访问 B 类的同一个对象 我见过那个static是系统范围的 并且不鼓励使用它 这是否意味着如果我有另一个程序在实例化 A 类对象的同一个 JVM
  • IntelliJ 调试:暂停整个虚拟机,然后进入单线程

    我正在调试一个具有大量线程的应用程序 我的断点设置为暂停整个虚拟机 当线程遇到其中一个断点时 我想使用 Step Over 但这似乎会恢复整个虚拟机 直到该步骤完成 如果我可以只单步执行到达断点的单个线程 那确实会有帮助 在 Intelli
  • Java VM 突然退出且没有明显原因

    我的 Java 程序突然退出 没有抛出任何异常 也没有正常完成 这是一个问题 我正在写一个程序来解决欧拉计划 http projecteuler net s 这就是我得到的 private static final int INITIAL
  • 如果只有完全限定名称,如何获取 java 类的二进制名称?

    反射类和方法以及类加载器等需要使用所谓的 二进制 类名称 问题是 如果只有完全限定名称 即在源代码中使用的名称 如何获得二进制名称 例如 package frege public static class RT public static
  • JVM 规范更新

    JVM 规范第 2 版的日期是 1999 年 自那时以来 我应该考虑学习哪些重要更新 如动态调用 这当然是为了了解现代 JVM 实现的内部原理 特别是 HotSpot 访问此链接http wikis sun com display HotS
  • 显示JVM中当前运行的所有线程组和线程

    所以我的任务是显示所有线程组以及当前在 JVM 中运行的属于这些组的所有线程 输出时应首先显示线程组 然后在下面显示该组中的所有线程 这是针对所有线程组完成的 目前 我的代码将仅显示每个线程组 然后显示每个线程 但我不确定如何达到我所描述的
  • 当 Java 中的集合超出容量时会发生什么?

    我有一个服务 它将所有对其进行的调用暂存在内存中 因为我们不想丢失数据 同时我们需要该服务因任何外部依赖项 例如数据库 而失败 然后 这些分阶段的调用会在后台例行接收和处理 如果出于任何原因 如果调用太多并且内存不足 我们就需要警惕 所以
  • 最近有关于 JVM 的书吗? [关闭]

    就目前情况而言 这个问题不太适合我们的问答形式 我们希望答案得到事实 参考资料或专业知识的支持 但这个问题可能会引发辩论 争论 民意调查或扩展讨论 如果您觉得这个问题可以改进并可能重新开放 访问帮助中心 help reopen questi
  • STS 无法在我的计算机上启动

    我试图在 eclipse 上设置 Spring mvc 项目 基本项目进展顺利 但是使用 Restful 服务 Jersey 等开始出现许多与依赖项相关的错误 所以我打算转到STS 我正在使用 STS 2 9 2 它给我 无法创建java虚
  • 非活动状态下的 Spring Boot 堆使用情况

    我在本地部署了一个非常简单的 spring boot 应用程序 它只有一个类 控制器 差不多就这样了 我注意到堆分配并不稳定 并且有峰值和突然下降 为什么会这样 我没有对应用程序进行过一次调用 A view from VisualVM 事实
  • 什么触发了java垃圾收集器

    我对 Java 中垃圾收集的工作原理有点困惑 我知道当不再有对某个对象的实时引用时 该对象就有资格进行垃圾回收 但是如果它有对实时对象的引用怎么办 可以说我有一个节点集合 它们再次引用更多节点 List 1 gt Node a gt Nod
  • 在intellij中为java启用ssl调试

    从我的问题开始 上一期尝试通过 tls ssl 发送 java 邮件 https stackoverflow com questions 39259578 javamail gmail issue ready to start tls th
  • Java 接口合成方法生成,同时缩小返回类型

    我有 2 个接口和 2 个返回类型 interface interfaceA Publisher
  • Scala 中的多个类型下限

    我注意到tuple productIterator总是返回一个Iterator Any 想知道是否无法设置多个下限 因此它可能是最低公共超类型的迭代器 我尝试并搜索了一下 但只发现this https stackoverflow com q

随机推荐