JVM--解析运行期优化与JIT编译器

news2024/11/24 23:58:08

本篇博客,我们来谈一谈JVM(HotSpot)为了提高Java程序的运行效率,都实现了哪些激动人心的技术~

1 JIT编译器的引入

首先我们这篇文章中所说的编译器都是指JVM的组成部分之一---即时编译器(JIT),与生成Java字节码的javac编译器要区分开来。

你也许想说,为什么要引进JIT编译器?很好的问题。

我们知道,javac将程序源代码编译,转换成Java字节码,解释器对字节码进行解释执行。而虚拟机传统的解释器,就是要将字节码中的操作指令和真正的平台体系结构之间的指令做映射。比如把Java的load指令换成native code的load指令。

JIT的出现,是为了补强虚拟机边运行边解释的低性能。它会智能地对热点代码进行优化且重复利用,最终将这些代码编译为与本地平台相关的机器码。

2 解释器与编译器

2.1 并存架构

刚才说明了引入JIT编译器的好处,那么HotSpot JVM为什么不完全采用编译器模式而是采用解释器与编译器并存的架构呢?

解释器与编译器各有优势:当程序需要迅速启动和执行时,解释器可以首先发挥作用,省去编译的时间,立即执行。当程序运行后,随着事件的推移,JIT编译器逐渐发挥作用,把越来越多的代码编译成本地代码之后,可以获取更高的执行效率。

引入编译器之后整个JVM的工作流程如下:

2.2 Client模式与Server模式

再来说一点其他的事情。

在HotSpot中还内置了两个即时编译器,分别是Client Compiler和Server Compiler,也称为C1编译器与C2编译器,在目前的HotSpot JVM中默认采用的是解释器与其中一个编译器直接配合的方式工作。我们可以使用“-client”或“-server”参数去指定解释器与具体的某个编译器配合工作。这也就是Client模式和Server模式的本质---指定了不同的JIT编译器进行工作。

网上有很多关于Java Client模式与Server模式的讲解博客,总结起来都一句话:Client版本(C1编译器)启动快,Server版本(C2编译器)运行快。至于为什么会产生这样的效果却鲜有人说明,其实就是因为两种编译器之间的差异。

因为编译器编译本地代码也是需要占用程序运行时间的。C1编译器主要是进行简单、可靠的优化,C2编译器为了编译出优化程度更高的代码主要是进行一些编译耗时较长的优化,甚至会进行激进优化。因此Client模式加载速度较快而Server模式运行起来较快。

不断追求完美是人类的天性,JVM团队为了在程序启动响应速度和与运行效率之间达到最佳平衡,设计出了分层编译。在JDK1.7的Server模式中,分层编译被作为默认编译策略开启,对于分层编译有兴趣的同学可以下去找一些资料,我在这里不再进行描述。

2.3 激进优化

上面提到了激进优化,既然是激进优化,那么这种优化即是不可靠的,是有可能优化失败的。那么为什么会存在激进优化呢?无非是为了再次提升运行时的效率。

在激进优化的时候,让编译器根据概率选择一些大多数时候都能提升运行速度的优化手段,但是当激进优化失败的时候,有一种称为逆优化的技术,可以退回到解释状态继续执行(这也是选用并存架构的另一个意义)。

3 编译对象与触发条件

在了解了为什么需要引入JIT编译器之后,现在需要讨论的就是哪些代码会被JIT编译器进行编译。

会被JIT编译器编译的“热点代码”有两类:

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

这两种情况都会使编译器以整个方法作为编译对象,不同的是,对于第二种情况,由于编译是发生在方法执行的过程中,因此会产生“栈上替换”(OSR编译)的行为,也就是方法栈帧还在栈上,方法就被替换了。

3.1 热点探测

那么如何判断一份代码是否是“热点代码”呢?主要有两种方法。

3.1.1 基于采样的热点探测

JVM周期性的检查各个线程的栈顶,如发现某个方法经常出现在栈顶,那这个方法就是“热点方法”。这个方法的劣势很明显,如果发生线程阻塞,那将会扰乱热点探测。

3.1.2 基于计数器的热点探测

HotSpot虚拟机采用这种方法。它会为每个方法建立计数器,统计方法的执行次数,如果执行次数超过了一定的阀值,就可以认为它是“热点方法”。

3.2 HotSpot中的两种计数器

“热点探测”技术给我们提供了寻找“热点方法”的途径,而计数器则是这条途径的具体实现。

HotSpot虚拟机为每个方法提供了两种计数器,这两个计数器都有一定的阀值,当计数器超过这个阀值溢出了,就会触发JIT编译。

3.2.1 方法调用计数器

这个计数器用于统计方法被调用的次数,对应“热点代码”中“被多次调用的方法”。有兴趣的同学可以查查它的默认阀值。阀值可以通过虚拟机参数-XX:CompileThreshold进行设定。

我们重点来看一下方法调用计数器触发即时编译的整个流程。

当一个方法被调用时,会先检查方法是否存在被JIT编译过的版本,如果存在,则优先使用编译后的本地代码来执行。如果不存在已经被编译的版本,则将此方法的调用计数器加1,然后判断方法调用计数器与回边计数器(稍后说明)之和是否超过方法调用计数器的阀值,如果已经超过阀值,那么将会向即时编译器提交一个该方法的代码编译请求。在下次进行方法调用的时候,重复此流程。

具体流程如下图:

从图中可以看到,在向即时编译器提交编译请求之后,执行引擎并不会进行阻塞,而是继续进入解释器按照解释方式执行字节码,直到提交的请求被编译器编译完成,这样做很明显不会造成程序运行中的阻塞。并且,我们可以判断,即时编译由一个后台线程操作进行。

在方法调用计数器中还有两个特别重要的概念:方法调用计数器的热度衰减与半衰周期

如果不做任何设置,方法调用计数器统计的并不是方法调用的绝对次数,而是一个相对的执行频率。也就是说,如果在一定的时间内,方法调用的次数不足以让它提交给即时编译器编译,那么这个方法的调用计数器就会被减少一半,这个过程就是方法调用计数器的热度衰减。而这段时间,就是此方法统计的半衰周期。

进行热度衰减的动作是在垃圾收集的时候顺便进行的。我们可以通过调节虚拟机参数指定是否进行热度衰减,或者调整它的半衰周期。

3.2.2 回边计数器

用于统计一个方法中循环体代码执行的次数。关于回边计数器的阀值不同的模式有不同的计算方法,不在这里进行讨论。

回边计数器触发JIT编译的流程与方法调用计数器极其类似。

当解释器遇到一条回边指令(编译原理的相关知识,可以粗略理解为循环)时,会先检查将要执行的代码片段是否存在被JIT编译过的版本,如果存在,则优先使用编译后的本地代码来执行。如果不存在已经被编译的版本,则将此方法的回边计数器加1,然后判断方法调用计数器与回边计数器之和是否超过回边调用计数器的阀值,如果已经超过阀值,那么将会向即时编译器提交一个OSR编译请求,并且会把回边计数器的值降低一些,以便继续在解释器中执行循环(说实话,对于这一步操作不是很理解,为什么方法调用计数器在提交编译请求之后不降低值呢?)。在下次进行方法调用的时候,重复此流程。

流程图如下:

回边计数器还有另外值得注意的地方:虽然编译动作是由循环体所触发,但是编译器仍然会编译整个方法,因此在回边计数器溢出的时候,它还会把方法计数器的值也调整到溢出状态。在下次进入该方法的时候就会执行标准编译过程。

4 编译优化技术之方法内联

即时编译器在将字节码翻译成本地机器码之前,还会对字节码进行一系列的优化,因此JIT编译器产生的本地代码会比javac产生的字节码更加优秀。

JVM设计团队采用的优化手段多不胜数,《深入理解Java虚拟机》一书中列举了方法内联、冗余访问消除、复写传播、无用代码消除、公共子表达式消除、数组边界检查消除、逃逸分析等优化手段。

我在这里只说明方法内联,对其他优化方式感兴趣的同学可以下去进行额外了解。

方法内联的重要性要高于其他优化措施,它的目的有二:1.去除方法调用的成本(建立栈帧)。2.为其他优化建立良好的基础。

看一下方法内联产生的效果:

优化前的代码:

 

java

复制代码

static class B { int value; final int get() { return value; } } public void foo() { y = b.get(); // ...do stuff... z = b.get(); sum = y + z; }

内联后的代码:

 

java

复制代码

public void foo() { y = b.value; // ...do stuff... z = b.value; sum = y + z; }

方法内联看起来很简单,但按照经典编译原理的优化理论,大多数的Java方法都无法进行内联。

还记得我们在前面讲述的方法解析与分派吗?在Java中,大多数的方法都是虚方法(虚方法的定义可以参见之前博客),这就导致了不到运行期JVM根本不知道实际调用的是哪一个方法版本。那么在JIT编译期(晚期优化还是发生在运行期之前)做内联的时候也就无法确定应该使用的方法版本。

例如如果有ParentB与SubB两个具有继承关系的类,并且子类重写了父类的get方法,那么要执行父类的get方法还是执行子类的get方法,需要到运行期才能确定,JIT编译期是无法得出结论的。

4.1 守护内联与内联缓存

为了解决虚方法的内联问题,JVM设计团队引入了一种“类型继承关系分析(CHA)”的技术。它用于确定在目前已加载的类中,某个接口是否有多于一种的实现,某个类是否存在子类,子类是否为抽象类等信息。

编译器在进行内联时,如果是非虚方法,那么直接进行内联就可以了,这时候的内联是有稳定前提保障的。

如果遇到虚方法,则会向CHA查询此方法在当前程序下是否有多个目标版本可供选择,如果查询结果只有一个版本,那也可以进行内联,不过这种内联就属于激进优化,需要预留一个 “逃生门”(解释器或C1编译器),称为守护内联(Guarded Inlining)。如果程序的后续执行过程中,虚拟机一直没有加载到会令这个方法的接收者的继承关系发生变化的类,那这个内联优化的代码就可以一直使用下去。如果加载了导致继承关系发生变化的新类,那就需要抛弃已经编译的代码,退回到解释状态执行,或者重新进行编译。(类文件可动态加载即类关系可能在运行时被修改)

如果向CHA查询出来的结果是有多个版本的目标方法可供选择,则编译器还将会进行最后一次努力,使用内联缓存(Inline Cache)来完成方法内联,这是一个建立在目标方法正常入口之前的缓存,它的工作原理大致是:在未发生方法调用之前,内联缓存状态为空,当第一次调用发生后,缓存记录下方法接收者的版本信息,并且每次进行方法调用时都比较接收者版本,如果以后进来的每次调用的方法接收者版本都是一样的,那这个内联还可以一直用下去。如果发生了方法接收者不一致的情况,就说明程序真正使用了虚方法的多态特性,这时才会取消内联,查找虚方法表进行方法分派。

所以说,在许多情况下虚拟机进行的内联都是一种激进优化,激进优化的手段在高性能的商用虚拟机中很常见,除了内联之外,对于出现概率很小(通过经验数据或解释器收集到的性能监控信息确定概率大小)的隐式异常、使用概率很小的分支等都可以被激进优化“移除”,如果真的出现了小概率事件,这时才会从“逃生门”回到解释状态重新执行。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/571505.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

《三》Git 中的本地仓库

初始化本地 Git 仓库: 通过 git init 初始化,可以把当前目录变成了 Git 管理的本地仓库。目前仅仅是做了一个初始化仓库的操作,项目里的文件还没有被跟踪。 在当前目录下会出现一个名为 .git 的目录,这些文件是 Git 仓库的核心。…

每天一个面试题之==和equals的区别是什么?

==和equals的区别是什么? ""是一个关系运算符,关系运算符可以用来进行数据和数据之间的比较,而在java中数据类型大致可以分为两大类分别是基本数据类型和引用数据类型。 基本数据类型包含 byte&#xff0c…

React学习笔记八-受控与非受控组件

此文章是本人在学习React的时候,写下的学习笔记,在此纪录和分享。此为第八篇,主要介绍非受控组件与受控组件。 目录 1.非受控组件 1.1表单提交案例 1.2案例的总结 2.受控组件 2.1受控组件案例 2.1受控案例总结 1.非受控组件 1.1表单提…

基于LLMs的多模态大模型(Flamingo, BLIP-2,KOSMOS-1,ScienceQA)

前一篇博客已经整理了不训练视觉模型的文章们: 基于LLMs的多模态大模型(Visual ChatGPT,PICa,MM-REACT,MAGIC) 本篇文章将介绍一些需要训练视觉编码器来适配多模态大模型的工作们,这也是目前最…

QT桌面项目(状态栏和导航栏设置)

文章目录 前言一、状态栏二、导航栏三、同时添加状态栏和导航栏总结 前言 为了和我们这个项目做的更加真实,这里为我们的项目添加上状态栏和导航栏让他变成更加接近手机的桌面效果。 一、状态栏 这个状态栏就是显示时间和wifi状态,电池电量的&#xf…

9秒被骗132万元,AI换脸骗术,如何防范?

5月22日,安徽安庆的何先生接到熟人视频电话,让他帮忙转一笔账,但在9秒之后,对方却以“在开会”为由,迅速挂断了电话,还称“微信和电话不能说,加一下QQ”。“因为打了视频电话,又是熟…

数据结构学习记录——如何建立图(邻接矩阵、邻接表-图节点的结构、创建并初始化、插入变、完整图的建立)

目录 邻接矩阵 图节点的结构 创建并初始化 插入边 完整的图的建立 邻接表 图节点的结构 创建并初始化 插入边 完整的图的建立 邻接矩阵 图节点的结构 #include <stdio.h> #include <stdlib.h>#define MaxVertexNum 100 // 最大顶点数typedef int Wei…

Maven介绍与安装和配置

目录 Maven 简介 约定优于配置 Maven 特点 Maven 安装与配置 Maven 下载 配置 Maven 环境变量 Maven 简介 Maven 是一款基于 Java 平台的项目管理和整合工具&#xff0c;它将项目的开发和管理过程抽象成一个项目对象模型&#xff08;POM&#xff09;。开发人员只需要做一…

C语言结构体

C语言结构体 前言1. 结构体的声明1.1 结构体的基础知识1.2 结构体声明1.3 结构体成员的类型1.4 结构体变量的定义和初始化 2. 结构体成员的访问2.1 结构体变量访问成员2.2 结构体指针访问指针变量的成员 3. 结构体传参4. 结尾 前言 C语言结构体是一种自定义数据类型&#xff0…

vite-plugin-pwa配置详解

vite-plugin-pwa配置详解 前提&#xff1a;前端域名和后端服务域名相同时&#xff0c;用window.open新开页面下载或者导出文件&#xff0c;项目中导出和下载功能失效&#xff0c;原因是&#xff0c;域名相同走缓存 实现service worker离线缓存以前需要自己编写sw.js文件内容&…

基于SpringBoot+Vue的闲一品交易平台设计与实现

博主介绍&#xff1a; 大家好&#xff0c;我是一名在Java圈混迹十余年的程序员&#xff0c;精通Java编程语言&#xff0c;同时也熟练掌握微信小程序、Python和Android等技术&#xff0c;能够为大家提供全方位的技术支持和交流。 我擅长在JavaWeb、SSH、SSM、SpringBoot等框架下…

MT4电脑版交易软件使用技巧有哪些?

MT4交易软件作为连接券商平台与投资者之间的纽带&#xff0c;不仅是外汇金融机构的首选交易平台&#xff0c;也因其显著的优势成为了外汇投资者进行网上交易的重要平台。而MT4交易软件又分为电脑版和手机版&#xff0c;因为大多数投资者进行外汇投资时使用的是MT4电脑版软件&am…

Gradio的web界面演示与交互机器学习模型,接口自动刷新或连续刷新数据流《5》

通过在接口中设置liveTrue&#xff0c;可以使接口自动刷新。现在&#xff0c;一旦用户输入发生变化&#xff0c;界面就会重新计算。依然使用计算器的示例&#xff1a; 实时接口 import gradio as grdef calculator(num1, operation, num2):if operation "add":ret…

浏览器原理+跨域+解决方案

原网址&#xff1a;浏览器部分笔记_浏览器不同窗口cookie共享吗_JackieChan_的博客-CSDN博客 一、浏览器存储对象 1.cookie cookie是一种纯文本文件&#xff0c;大小只有4kb&#xff0c;每次发送非跨域html请求时都会自动携带。特性如下&#xff1a; cookie一旦创建&#xff…

华为开源自研AI框架昇思MindSpore应用案例:Pix2Pix实现图像转换

目录 一、环境准备1.进入ModelArts官网2.使用CodeLab体验Notebook实例 在实际应用场景中&#xff0c;由于训练数据集不足&#xff0c;所以很少有人会从头开始训练整个网络。普遍的做法是&#xff0c;在一个非常大的基础数据集上训练得到一个预训练模型&#xff0c;然后使用该模…

Java程序设计入门教程--主函数

情形 在Java中&#xff0c;主函数就是主方法&#xff0c;即main()方法。它是Java应用程序的入口方法&#xff0c;也就是说&#xff0c;程序在运行的时候&#xff0c;第一个执行的方法就是main()方法&#xff0c;这个方法和其他的方法有很大的不同&#xff0c;比如方法的名字必…

Python100天:01.初识python

❝ 本教程计划通过100天的时间&#xff0c;每天分享一篇关于python的知识点&#xff0c;与大家一起学习python这门编程语言。 ❞ Python 对初学者来说是一门很棒的语言&#xff1a; 容易学 有一个积极的支持社区 在网络开发、游戏、数据科学方面提供多种机会。 Python的应用领域…

PMP课堂模拟题目及解析(第13期)

121. 项目经理、团队成员以及若干干系人共同参与一次风险研讨会。已经根据风险管理计划生成并提供一份风险报告。若要为各个项目风险进行优先级排序&#xff0c;现在必须执行哪一项分析&#xff1f; A. 定量风险分析 B. 根本原因分析 C. 偏差分析 D. 定性风险分析 122. …

Yarn资源调度详解

第1章 Yarn资源调度器 思考&#xff1a; 1&#xff09;如何管理集群资源&#xff1f; 2&#xff09;如何给任务合理分配资源&#xff1f; Yarn是一个资源调度平台&#xff0c;负责为运算程序提供服务器运算资源&#xff0c;相当于一个分布式的操作系统平台&#xff0c;而MapRe…

什么是产品操作手册?企业该怎样制作产品操作手册页面?

产品操作手册是一种用于指导用户如何正确使用和维护产品的文档。它通常包括产品的基本信息、操作步骤、安全警告、故障排除、维护方法等内容。产品操作手册对于企业来说非常重要&#xff0c;它不仅可以提高用户的使用体验&#xff0c;还可以为企业节省售后服务成本。本文将介绍…