JVM执行引擎JIT深度剖析

前端编译与后端编译

Java 程序的编译过程是分两个部分的。一个部分是从java文件编译成为class文件,这一部分也称为前端编译。另一个部分则是这些class文件,需要进入到 JVM 虚拟机,将这些字节码指令编译成操作系统识别的具体机器指令。这一部分也称为后端编译。

其中前端编译是在 JVM 虚拟机之外执行,所以与 JVM 虚拟机没有太大的关系。任何编程语言,只要能够编译出满足 JVM 规范的 Class 文件,就可以提交到 JVM 虚拟机执行。至于编译的过程,如果你不是想要专门去研究语言,那么就没有必要太过深入的去了解了。这里就暂时略过。我们更关注JVM 在后端编译过程中如何提升执行的效率。

字节码指令是如何执行的

解释执行与编译执行

Class 文件当中就已经保留了每一行 Java 代码对应的字节码指令,也就是说,执行引擎要如何执行一段 Java 代码,其实早在 Class 文件当中就已经确定了。执行引擎要做的事情,其实就是将这些Class文件中的字节码指令翻译成对应操作系统的机器码,然后扔给服务器执行就行了。 本质上,就相当于是一个翻译。

那么怎么做这个翻译工作呢?最简单的方式,当然就是来一个指令就翻译一次。就像是一个无脑的翻译机器,不用管合不合理,按字翻译就是了。没错,早期的JVM执行引擎其实就是这么做的,这种执行方式,就称为解释执行

但是这种方式需要在上层语言和机器码之间经过中间一层JVM字节码的转换,显然执行效率是比不上 C 和 C那些直接面向本地机器指令编程的语言的,这也是长久以前,Java 被 C 和 C开发者吐槽执行速度慢的根源。

那么要怎么提升JAVA的执行效率呢?

JAVA的基本思想就是维护一个缓存,CodeCache,将那些字节码指令,提前编译出来,放到缓存里。到执行的时候,直接从缓存中查出来就好了。这种先编译,后执行的方式,就称为编译执行

​ 但是JAVA官方也不知道程序员会写出什么样稀奇古怪的代码,所以,自然没办法提前维护出一个完整的字节码缓存。那么就只能退而求其次,将那些运行频率最高的热点代码提前编译出来,放到缓存里。这样,至少最常用的那些方法的调用效率能够提高。完成这个任务的编译器,称为即时编译器 JIT(Just In Time Compiler)。

​ 使用java -version就可以看到当前使用的是哪种执行模式。

 从这里可以看到,HotSpot虚拟机并没有直接选择执行效率更快的编译执行,而是默认采用的一种混合执行的方式。

为什么JVM不直接采用性能明显更高的编译执行模式呢?这是因为虽然编译执行可以将越来越多的代码编译成本地代码,这样可以减少解释器的中间损耗,获得更高的执行效率。但是,这也意味着对内存有更多的资源限制,在很多资源比较紧张的场景,比如客户端应用,嵌入式系统等,使用解释执行就能更节约内存。

另外,编译执行需要较长的预热过程。在 CodeCache 中的代码缓存维护好之前,编译执行相比解释执行需要额外的性能消耗,用来识别热点代码,维护 CodeCache 。同时,编译执行在识别热点代码的过程中,还需要解释执行来帮助提供一些信息支持。在 HotSpot 中,会默认使用混合执行模式,而不是单纯的使用其中一种模式。

热点代码识别

 使用 JIT 实时编译的前提就是需要识别出热点代码。要知道某段代码是不是热点代码,是不是需要触发即时编译,这个行为称为“热点探测”(Hot Spot Code Detection)。热点探测有很多种实现思路,而在 HotSpot 虚拟机中采用的是一种基于计数器的热点探测方法。HotSpot 为每个方法准备了两类计数器: 方法调用计数器(Invocation Counter)和回边计数器(Back Edge Counter)。当虚拟机运行参数确定的前提下,这两个计数器都有一个明确的阈值,计数器阈值一旦溢出,就会触发即时编译。

首先来看看方法调用计数器。顾名思义,这个计数器就是用于统计方法被调用的次数。每次调用一个方法时,就记录一次这个方法的执行次数。当他的执行次数非常多,超过了某一个阈值,那么这个方法就可以认为是热点方法。这个方法对应的代码,自然也就是热点代码了。这时就可以向JIT提交一个针对该方法的代码编译请求了。

方法调用技术器的默认阈值是10000次,这个阈值可以通过虚拟机参数-XX:CompileThreshold来设定。当一个方法被调用时,虚拟机会先检查该方法是否存在被即时编译过的版本,如果存在,则优先使用编译后的本地代码来执行。如果不存在已被编译过的版本,则将该方法的调用计数器值加一,然后判断方法调用计数器与回边计数器值之和是否超过方法调用计数器的阈值。一旦已超过阈值的话,将会向即时编译器提交一个该方法的代码编译请求。整体流程如下图:

比如这个方法计数器的默认阈值,就可以使用 java -XX:+PrintFlagsInitial -version 指令查询。

方法计数器以方法为维度,自然是不够精细的。所以,要更精细的识别热点代码,还需要配合接下来的回边计数器

回边计数器,它的作用是统计一个方法中循环体代码执行的次数。在字节码中遇到控制流向后跳转的指令就称为“回边(Back Edge)”,很显然建立回边计数器统计的目的是为了发现一个方法内部频繁的循环调用。回边计数器在服务端模式下默认的阈值是 10700

当解释器遇到一条回边指令时,会先查找将要执行的代码片段是否有已经编译好的版本,如果有的话,它将会优先执行已编译的代码,否则就把回边计数器的值加一,然后判断方法调用计数器与回边计数器值之和是否超过回边计数器的阈值。当超过阈值的时候,将会提交一个编译请求,并且把回边计数器的值稍微降低一些,以便继续在解释器中执行循环,等待编译器输出编译结果,整个执行过程如下图。

C1、C2与Graal编译器

 在JDK1.8中 HotSpot 虚拟机中,内置了两个 JIT,分别为 C1 编译器和 C2 编译器。

 C1编译器

C1 编译器是一个简单快速的编译器,主要的关注点在于局部性的优化,适用于执行时间较短或对启动性能有要求的程序,例如,GUI 应用对界面启动速度就有一定要求,C1也被称为 Client Compiler。

C1编译器几乎不会对代码进行优化

C2编译器

C2 编译器是为长期运行的服务器端应用程序做性能调优的编译器,适用于执行时间较长或对峰值性能有要求的程序。根据各自的适配性,这种即时编译也被称为Server Compiler。

但是C2代码已超级复杂,无人能维护!所以才会开发Java编写的Graal编译器取代C2(JDK10开始)

分层编译

分层编译根据编译器编译、优化的规模与耗时,划分出不同的编译层次

等级描述性能
0程序纯解释执行,并且解释器不开启性能监控功能(Profiling)1
1使用C1编译器将字节码编译为本地代码来运行,进行简单可靠的稳定优化。不开启性能监控功能。4
2仍然使用C1编译器执行,仅开启方法及回边次数统计等有限的性能监控功能。3
3仍然使用C1编译器执行,开启全部性能监控,除了第2层的统计信息外,还会收集如分支跳转、虚方法调用版本等全部的统计信息。2
4使用C2编译器将字节码编译为本地代码,相比起C1编译器,C2编译器会启用更多编译耗时更长的优化,还会根据性能监控信息进行一些不可靠的激进优化。5

后端编译优化技术

方法内联 Inline

方法内联的优化行为就是把目标方法的代码复制到发起调用的方法之中,避免发生真实的方法调用。这样就可以减少频繁创建栈帧的性能开销。

例如以下方法:

最终会被优化为:

JVM 会自动识别热点方法,并对它们使用方法内联进行优化。

我们可以通过 -XX:CompileThreshold 来设置热点方法的阈值。

但要强调一点,热点方法不一定会被 JVM 做内联优化,如果这个方法体太大了,JVM 将不执行内联操作。

而方法体的大小阈值,我们也可以通过参数设置来优化:

经常执行的方法,默认情况下,方法体大小小于 325 字节的都会进行内联,我们可以通过 -XX:FreqInlineSize=N 来设置大小值;

 

不是经常执行的方法,默认情况下,方法大小小于 35 字节才会进行内联,我们也可以通过 -XX:MaxInlineSize=N 来重置大小值。

 

逃逸分析 Escape Analysis

当一个对象在方法里面被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中,这种称为方法逃逸;甚至还有可能被外部线程访问到,譬如赋值给可以在其他线程中访问的实例变量,这种称为线程逃逸;从不逃逸、方法逃逸到线程逃逸,称为对象由低到高的不同逃逸程度。

左侧的代码中,t对象,不会被外部引用,只会在方法中使用,所以不会发生逃逸。而右侧的代码中,t对象就很明显被其他方法使用了,这就会产生逃逸。JDK8 中默认开启了逃逸分析,可以添加参数 -XX:-DoEscapeAnalysis 主动关闭逃逸分析。

如果能证明一个对象不会逃逸到方法或线程之外,那么 JIT 就可以为这个对象实例采取后续一系列的优化措施。 

第一个是标量替换(Scalar Replacement)。若一个数据已经无法再分解成更小的数据来表示了,Java虚拟机中的原始数据类型(int、long等数值类型及reference类型等)都不能再进一步分解了,那么这些数据就可以被称为标量。如果把一个Java对象拆散,根据程序访问的情况,将其用到的成员变量恢复为原始类型来访问,这个过程就称为标量替换。假如逃逸分析能够证明一个对象不会被方法外部访问,并且这个对象可以被拆散,那么程序真正执行的时候将可能不去创建这个对象,而改为直接创建它的若干个被这个方法使用的成员变量来代替。将对象拆分后,除了可以让对象的成员变量在栈上分配和读写之外,还可以为后续进一步的优化手段创建条件。标量替换对逃逸程度的要求更高,它不允许对象逃逸出方法范围内。JDK8 中默认开启了标量替换,可以通过添加参数 -XX:-EliminateAllocations 主动关闭标量替换。

第二个是栈上分配( Stack Allocations)。正常情况下,JVM 中所有对象都应该创建在堆上,并由 GC 线程进行回收。如果确定一个对象不会逃逸出线程之外,那让这个对象在栈上分配内存将会是一个很不错的主意,对象所占用的内存空间就可以随栈帧出栈而销毁。在一般应用中,完全不会逃逸的局部对象和不会逃逸出线程的对象所占的比例是很大的,如果能使用栈上分配,那大量的对象就会随着方法的结束而自动销毁了,垃圾收集子系统的压力将会下降很多。栈上分配可以支持方法逃逸,但不能支持线程逃逸。

这三种优化措施中,逃逸分析是基础。因为虚拟机栈是对应一个线程的,而堆内存是对应整个Java进程的。如果发生了线程逃逸,那么堆中的同一个对象,可能隶属于多个线程,这时要将堆中的对象挪到虚拟机栈中,那就必须扫描所有的虚拟机栈,看看在这个虚拟机栈对应的线程中是否引用了这个对象。这个性能开销是难以接受的。

​ 而栈是一个非常小的内存结构,他也不可能像堆中那么豪横的使用内存空间,所以,也必须要对对象进行最大程度的瘦身,才能放到栈中。而瘦身的方式,就是去掉对象的mark标志位中的补充信息,拆分成最精简的标量。所以,要开启栈上分配,标量替换也是不可或缺的。

可以用下面示例方法进行一下验证:

public class EscapeAnalysisTest {
    public static void main(String[] args) throws InterruptedException {
        long start = System.currentTimeMillis();
        for (int i = 0; i < 10000000; i++) {
            allocate();
        }
        System.out.println("运行耗时:"+(System.currentTimeMillis()-start));
        Thread.sleep(6000000);
    }

    static void allocate(){
        MyObject myObject = new MyObject(2024,2024.6);
    }

    static class MyObject {
        int a;
        double b;

        MyObject(int a,double b){
            this.a = a;
            this.b = b;
        }
    }
}

以我的测试环境来看,默认情况下, 运行时间大概 2 毫秒,而关闭逃逸分析或者关闭标量替换后,运行时间就扩大到了 44毫秒左右。

锁消除 lock elision

这也是经过逃逸分析后可以直接进行的优化措施。

​ 这个优化措施主要是针对 synchronized 关键字。当 JVM 检测到一个锁的代码不存在多线程竞争时,会对这个对象的锁进行锁消除。

 比如下面的示例代码:

public class LockElisionDemo {
    public static String BufferString(String s1,String s2){
        StringBuffer sb = new StringBuffer();
        sb.append(s1);
        sb.append(s2);
        return sb.toString();
    }

    public static String BuilderString(String s1, String s2){
        StringBuilder sb = new StringBuilder();
        sb.append(s1);
        sb.append(s2);
        return sb.toString();
    }

    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();
        for (int i = 0; i < 100000000; i++) {
            BufferString("aaaaa","bbbbbb");
        }
        System.out.println("StringBuffer耗时:"+(System.currentTimeMillis()-startTime));

        long startTime2 = System.currentTimeMillis();
        for (int i = 0; i < 100000000; i++) {
            BuilderString("aaaaa","bbbbbb");
        }
        System.out.println("StringBuilder耗时:"+(System.currentTimeMillis()-startTime2));
    }
}

 其中分别测试了 StringBuffer 和 StringBuilder 的字符串构建方法。这两个方法功能上没有什么区别,最大的区别在于,StringBuffer 是线程安全的,他的append和toString都是加了 synchronized 同步锁的,而 StringBuilder 则没有加。之前介绍过,synchronized 关键字其实是在Class文件中添加了monitorenter和monitorexit两个字节码指令的,所以,StringBuffer显然要比 StringBuilder 更慢。

在当前代码中,BufferString 方法只是在main这一个线程里调用,不存在线程竞争,所有这个synchronized 同步锁是没有作用的,因此,在触发了 JIT 后,JVM 会在编译时就会将这个无用的锁消除掉。这样,两个方法的耗时是差不多的。

StringBuffer耗时:1521
StringBuilder耗时:1039

与之形成对比,如果给这个示例代码添加一个JVM 参数: -XX:-EliminateLocks 主动关闭锁清除后,再执行这个案例,两个方法的耗时差距就明显更大了。

StringBuffer耗时:2461
StringBuilder耗时:1049

 

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

五分钟学会如何在GitHub上自动化部署个人博客(hugo框架 + stack主题)

上一篇文章&#xff1a; 10分钟学会免费搭建个人博客&#xff08;Hugo框架 stack主题&#xff09; 前言 首先&#xff0c;想要实现这个功能的小伙伴需要完成几个前置条件&#xff1a; 有一个GitHub账号安装了git&#xff0c;并可以通过git推送commit到GitHub上完成第一篇文章…

OpenHarmony的分布式服务框架介绍与实现解析

OpenHarmony的分布式服务框架是一个用于实现设备间高效协作与资源共享的重要架构&#xff0c;以下是其详细介绍&#xff1a; 框架概述 OpenHarmony的分布式服务框架基于分布式软总线、分布式数据管理、分布式Profile等技术特性&#xff0c;构建了统一的分布式服务管理机制&am…

360pika—弹性 KV 数据存储系统入门安装使用

一、简介 github官网说Pika 是一个高性能、大容量、多租户、数据持久化的弹性 KV 数据存储系统,使用 RocksDB 作为存储引擎。它完全兼容 Redis 协议,并支持其常用的数据结构,如字符串/哈希/列表/有序集合/集合/地理位置/HyperLogLog/发布-订阅/位图/数据流等。 二、对标啥能干…

springboot中使用gdal将表中的空间数据转shapefile文件

springboot中使用gdal将表中的空间数据转shapefile文件 代码&#xff1a; // 样本导出-将样本表导出为shapefile&#xff0c;复制样本shp文件到临时目录下 sampleDir是文件夹pathpublic void setYbShapeFile(Yb yb, File sampleDir) {// 创建 前时项 和 后时项 文件夹File y…

【学习笔记】蒙特卡洛与强化学习

视频链接&#xff1a;https://www.bilibili.com/video/BV1SV4y1i7bW 文章目录 [蒙特卡洛方法] 02 重要性采样&#xff08;importance sampling&#xff09;及 python 实现Basics实现重要性采样 [蒙特卡洛方法] 03 接受/拒绝采样&#xff08;accept/reject samping&#xff09;初…

查看MySQL存储引擎方法,表操作

修改数据库表存储引擎 show create table dept; show table status from itpux where name s2\G; select * from information_schema.TABLES where table_schemaitpux and table_names3; 查询整个mysql里面存储引擎是innodb/myisam的表 建表时候要写好存储引擎 -- 创建表 -- 表…

项目亮点案例

其实对我来说是日常操作&#xff0c;但是如果在面试的时候面试者能把日常的事情总结好发出来&#xff0c;其实足矣。 想让别人认同项目&#xff0c;选取的示例需要包含以下要素&#xff1a; 亮点项目四要素&#xff1a;明确的目标&#xff0c;问题点&#xff0c;解决方法和结果…

MyBatis通过注解配置执行SQL语句原理源码分析

文章目录 前置准备流程简要分析配置文件解析加载 Mapper 接口MapperAnnotationBuilder解析接口方法注解parseStatement 方法详解MapperBuilderAssistant 前置准备 创建一个mybatis-config.xml文件&#xff0c;配置mapper接口 <mappers><!--注解配置--><mapper…

蓝桥杯物联网开发板硬件组成

第一节 开发板简介 物联网设计与开发竞赛实训平台由蓝桥杯大赛技术支持单位北京四梯科技有限公司设计和生产&#xff0c;该产品可用于参加蓝桥杯物联网设计与开发赛道的竞赛实训或院校相关课程的 实践教学环节。 开发板基于STM32WLE5无线微控制器设计&#xff0c;芯片提供了25…

常用矢量图标库

常用矢量图标库 1. iconfont 阿里巴巴旗下的矢量图标素材库&#xff1b;很强大且图标内容很丰富的矢量图标库,提供矢量图标下载&#xff08;AI / SVG / PNG / 代码格式&#xff09;、在线存储等功能&#xff0c;支持按路径改变 icon 颜色。 iconfont 网址 设备图标 2. IconP…

Unity动态读取外部图片转Texture2D,内存过大问题解决方案

问题描述 加载原始图片2.63M的图片,分辨率为3023*4032,占用内存108.5M 加载原始图片12.6 M的图片,分辨率为6000*8000,占用内存427.2M 太恐怖了吧 解决方案 1.加载完图片,等比缩放,宽高改为1024或者512以下 1024占用5.2M,512占用1.3M,相比小了很多 2.原始Texture2…

在Visual Studio 2022中配置C++计算机视觉库Opencv

本文主要介绍下载OpenCV库以及在Visual Studio 2022中配置、编译C计算机视觉库OpenCv的方法 1.Opencv库安装 ​ 首先&#xff0c;我们需要安装OpenCV库&#xff0c;作为一个开源库&#xff0c;我们可以直接在其官网下载Releases - OpenCV&#xff0c;如果官网下载过慢&#x…

【AIGC】ChatGPT 结构化 Prompt 的高级应用

博客主页&#xff1a; [小ᶻ☡꙳ᵃⁱᵍᶜ꙳] 本文专栏: AIGC | ChatGPT 文章目录 &#x1f4af;前言&#x1f4af;标识符的使用&#xff08;Use of Identifiers&#xff09;1. #2. <>3. - 或 4. [] &#x1f4af;属性词的重要性和应用应用场景 &#x1f4af;具体模块…

git使用和gitlab部署

1.ci,cd,DevOps ci&#xff1a;持续集成&#xff1a;开发的代码集成到代码仓库 cd&#xff1a;持续交互&#xff1a;从代码仓库拉取代码到部署到测试环境 cd&#xff1a;持续部署&#xff1a;从代码仓库拉取代码到部署到生产环境 DevOps:开发写完的代码自动集成&#xff0c…

学习因子异步化的粒子群优化算法(AsyLnCPSO)——源码

目录 1. 学习因子异步化的概念 2. 算法步骤 2.1 初始化 2.2 迭代过程 3.优势 4. 与传统粒子群算法的区别 5.代码下载&#xff1a; 学习因子异步化的粒子群优化算法&#xff08;AsyLnCPSO&#xff09;是一种改进的粒子群优化&#xff08;PSO&#xff09;算法&#xff0c;…

BEVFormer论文总结

BEVFormer: Learning Bird’s-Eye-View Representation from Multi-Camera Images via Spatiotemporal Transformers BEVFormer&#xff1a;利用时空变换从多相机图像中学习鸟瞰表示 研究团队&#xff1a;南京大学、上海AI实验室、香港大学 ​ 代码地址&#xff1a;https://g…

ABAQUS纤维混凝土冲击破坏三维模型

纤维混凝土作为土木工程领域常用的复合材料具备良好的抗裂性及抗冲击性能&#xff0c;纤维混凝土在荷载下的破坏行为及本构关系对其应用范围具有重要影响。本案例通过AutoCAD随机三维纤维插件建立随机投放的圆柱体纤维模型&#xff0c;并将模型导入ABAQUS内&#xff0c;通过混凝…

selenium学习笔记(一)

文章目录 前言一、selenium的简介java使用seleniumPython使用selenium常用的浏览器selenium的功能 二、chromeDriver的安装查看本机的chrome版本&#xff1f;匹配对应的chromedriver并下载在服务器上例如Centos如何安装Chrome 三、selenium内容详解chrome启动chrome启动参数元素…

如何利用Java爬虫按关键字搜索苏宁易购商品

在信息技术飞速发展的今天&#xff0c;数据的获取和分析对于企业决策、市场研究和用户行为分析至关重要。本文将介绍如何使用Java编写爬虫程序&#xff0c;通过关键字搜索苏宁易购的商品&#xff0c;并获取搜索结果。 1. 爬虫简介 爬虫是一种自动化程序&#xff0c;用于从互联…

网页生成鸿蒙App

如何网页生成鸿蒙App 纯鸿蒙发布后&#xff0c;鸿蒙App需求上升。如何快速生成鸿蒙App。变色龙云(http://www.appbsl.cn)推出了鸿蒙App打包服务。可以在线自动打包鸿蒙App。 第一步 创建应用 输入网站网址&#xff0c;上传图标。 第二步 生成鸿蒙证书 打开华为开发者管理中…