90% 的 Java 程序员都说不上来的为何 Java 代码越执行越快(1)- JIT编译优化

简介: 90% 的 Java 程序员都说不上来的为何 Java 代码越执行越快(1)- JIT编译优化
麻烦大家帮我 投一票哈,谢谢

经常听到 Java 性能不如 C/C++ 的言论,也经常听说 Java 程序需要预热,那么其中主要原因是啥呢

面试的时候谈到 JVM,也有很多面试官喜欢问,为啥 Java 程序越执行越快呢

一般人都能回答上来,类加载,缓存预热等等,但是深入下去,最重要的几点却没有答上来,今天本系列文章就来帮助大家理解这个问题的关键,首先是 jit 编译优化。

首先,我们从一个简单的例子看起,来感受下程序是否越来越快:

package com.test;
import java.util.concurrent.TimeUnit;
public class CompileTest {
    public static void main(String[] args) throws InterruptedException {
        while (true) {
            test1();
            TimeUnit.SECONDS.sleep(1);
        }
    }
    public static void test1() {
        long time1 = System.nanoTime();
        long count1 = 0;
        for (int i = 0; i < 10000; i++) {
            count1++;
        }
        //为了和编译日志区分,这里输出到error输出
        System.err.println(System.nanoTime() - time1 + "-----" + count1);
    }
}

运行时,加上参数-XX:+PrintCompilation,打印一下编译日志(其实这个参数以后也许就过期了,建议使用 JVM 标准日志参数:-Xlog:jit+compilation=info),可以看到:

432900-----10000
250800-----10000
194600-----10000
197200-----10000
131600-----10000
184000-----10000
   6369  374 %     3       com.test.CompileTest::test1 @ 9 (61 bytes)
162300-----10000
   7369  375       3       com.test.CompileTest::test1 (61 bytes)
68300-----10000
60300-----10000
47200-----10000
48100-----10000
  11371  378 %     4       com.test.CompileTest::test1 @ 9 (61 bytes)
55600-----10000
  11388  374 %     3       com.test.CompileTest::test1 @ 9 (61 bytes)   made not entrant
  12372  379       4       com.test.CompileTest::test1 (61 bytes)
157600-----10000
  12389  375       3       com.test.CompileTest::test1 (61 bytes)   made not entrant
600-----10000
700-----10000
600-----10000
1200-----10000
900-----10000
900-----10000

从输出中可以看出,貌似JVM对test1这段代码做了一些事情,使方法运行越来越快了。这就是JIT做的优化,随着代码的执行,热点代码会被优化,让执行更加迅速。这也是为什么,通过一般方法(javac命令)编译出来java class文件在执行的时候,要预热之后,才能发挥最大性能。接下来,我们来详细介绍下JIT。

OpenJDK Hotspot JVM,是最广泛运用的Java JVM。主要包含两部分,执行引擎(execution engine)和运行时(runtime)。执行引擎包括两部分,一个是垃圾收集器,另一个就是我们今天的主题, JIT(just-in-time)编译器。


什么是JIT


JVM是Java一次编译,跨平台执行的基础。当java被编译为字节码形式的class文件之后,他可以在任意的JVM运行。这里说的编译,主要是指前端编译器。

Java中主要有两种编译器:

  1. 前端编译器,将.java文件编译为JVM可执行的.class字节码文件,即javac,主要职责包括:词法、语法分析,填充符号表,语义分析,字节码生成。输出为字节码文件,也可以理解为是中间表达形式(称为IR:Intermediate Representation)。对应上面的例子就是将CompileTest.java编译成符合Java规范的字节码文件CompileTest.class
  2. 后端编译器,在程序运行期间将字节码转变成机器码,通过解释器和运行时编译器混合模式(现在的 Java 程序在运行时基本都是解释执行加编译执行),如 HotSpot 虚拟机自带的解释器还有 JIT(Just In Time Compiler)编译器(分 Client 端和 Server 端),其中JIT还会将中间表达形式进行一些优化。对应上面的例子就是test1方法执行越来越快。

Java 9中还引入了实验编译器AOT(Ahead-Of-Time)编译器,直接生成机器码。主要用于减少JAVA启动预热时间,比较适用于单次执行时间有限需要高效执行的程序,或者是小集成芯片环境,对效率要求比较高。AOT与Graal我们会在系列的最后着重介绍。对应上面的例子就是,test1方法不用预热就会执行的和上面最会一样那么快。但是相应的,机器码占用的大小比字节码大的多得多,而且不能跨平台。

为什么要这么区分呢?首先,不同机器的机器码是不一样的,编译生成统一的字节码保证了跨平台应用的可能性。然后,将字节码优化(中间表达形式优化)放到运行时优化,这样低版本的java编译出来的字节码,在高版本的JVM运行,仍能享受高版本的JVM新的优化机制带来的性能提升,这是一种很好的向后兼容机制。所以有的时候,我们可以先把JVM升级到新版本来享受更高效的优化算法


刚刚提到了JVM使用混合模式来从字节码转换成机器可以运行的机器码,混合模式包括解释器和JIT:


解释器工作机制:


image.png


在编译时,主要是将java源代码文件编译为java统一的字节码,但是编译成的字节码并不能直接运行,而是通过JVM读取运行。JVM中的解释器就是将.class文件一行一行翻译之后再运行,翻译就是转换成当前机器可以运行的机器码,它不会一次性把整个文件都翻译过来,而是翻译一句,执行一句,再翻译,再执行,所以解释器的程序运行起来会比较慢,每次都要解释之后再执行。所以,有些时候,我们想是否可以把解释之后的内容缓存起来,这样不就可以直接运行了?但是,如果每段代码都要缓存起来,例如仅仅执行一次的代码也缓存起来,这样太浪费内存了。所以,引入一个新的运行时编译器,JIT来解决这些问题,加速热点代码的执行。

JIT运行时编译器工作机制:


微信图片_20220625114035.jpg


JIT针对热点代码,进行编译与深度优化,优化后的机器码会被缓存起来,存入CodeCache中。对于非热点代码,例如只运行一次的代码(类构造器等等),直接解释执行,更加快速。JIT不仅花更多时间去编译优化,而且还多耗费了很多内存,并且 CodeCache 发生变化会发生部分或者所有线程进入 Safepoint 导致 Stop the world


字节码转换为可执行的机器码,大小会大很多很多倍。这也是为啥,解释器每次都要翻译并且执行,JIT只针对热点代码进行编译优化的原因。JIT编译器执行的一些常见优化操作包括数据分析,从堆栈操作到寄存器操作的转换,通过寄存器分配减少内存访问,消除常见子表达式等。JIT编译器进行的优化程度越高,在执行阶段花费的时间越多。因此,JIT编译器无法承担所有静态编译器所做的优化,这不仅是因为增加了执行时间的开销,而且还因为它只对程序进行了限制。这也就解释了为什么有些JVM会选择不总是做JIT编译,而是选择用解释器+JIT编译器的混合执行引擎。


对于上面的例子,刚开始的时候,test1方法是解释器执行的,由于多了一步转换,所以比较慢。后面随着代码的运行和JIT优化,test1方法的机器码被优化并且存入代码缓存,下次执行直接从代码缓存读取执行。


JIT的基本工作原理


首先,需要判断一个方法是否是热点方法:在HotSpot虚拟机中使用的基于计数器的热点探测方法,他为每个方法都准备了两个计数器:方法调用计数器和loop-back-edge计数器。

  • 方法调用计数器:顾名思义,这个计数器用于统计方法被调用的次数。在一个方法被调用时,根据前面所述,会先看看是否存在与codecache中,也就是jit编译的版本,如果不存在,则将计数加一,判断是否大于阈值,如果大于,则那么将会向即时编译器提交一个该方法的代码编译请求。如果不做任何设置,执行引擎并不会同步等待编译请求完成,而是继续进行解释器按照解释方式执行字节码,直到提交的请求被编译器编译完成。当编译工作完成之后,这个方法的调用入口地址就会系统自动改写成新的,下一次调用该方法时就会使用已编译的版本。
  • loop-back-edge计数器:专用来统计loop次数的,就是统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令称为loop-back-edge。这个计数器机制与上面的方法调用计数器一致。

有了这些计数器,JIT可以根据这些计数器里面的统计信息,进行优化。当然,不止有这些计数器,还有一些其他更复杂的采集点。JIT编译器在JDK 8之前,例如JDK 7是区分client模式(C1编译器)还是server模式(C2编译器)的,从JDK 8开始,不做这个区分了,都是C1+C2编译器合作,分层优化。C1是一个简单快速的编译器,主要关注点在于局部优化,而放弃许多耗时较长的全局优化手段。C2则是专门面向服务器端的,并为服务端的性能配置特别调整过的编译器,是一个充分优化过的高级编译器。从Java 8开始,JIT编译优化是分层优化,分为5层,每层都会有C1或者C2参与。

  • 第0层(Tier-0),只有解释器参与,解释执行
  • 第1层(Tier-1),执行不带任何采集的的C1优化代码
  • 第2层(Tier-2),执行仅带方法调用计数器和loop-back-edge计数器profiling的C1优化代码
  • 第3层(Tier-3),执行带所有采集的的C1优化代码
  • 第4层(Tier-4),执行C2优化代码
相关文章
|
5天前
|
JavaScript NoSQL Java
接替此文【下篇-服务端+后台管理】优雅草蜻蜓z系统JAVA版暗影版为例-【蜻蜓z系列通用】-2025年全新项目整合搭建方式-这是独立吃透代码以后首次改变-独立PC版本vue版搭建教程-优雅草卓伊凡
接替此文【下篇-服务端+后台管理】优雅草蜻蜓z系统JAVA版暗影版为例-【蜻蜓z系列通用】-2025年全新项目整合搭建方式-这是独立吃透代码以后首次改变-独立PC版本vue版搭建教程-优雅草卓伊凡
142 96
接替此文【下篇-服务端+后台管理】优雅草蜻蜓z系统JAVA版暗影版为例-【蜻蜓z系列通用】-2025年全新项目整合搭建方式-这是独立吃透代码以后首次改变-独立PC版本vue版搭建教程-优雅草卓伊凡
|
4天前
|
人工智能 Java 程序员
【AI程序员】通义灵码 AI 程序员全面上线JAVA使用体验
通过 AI 程序编写一个JAVA后台项目登陆页面
119 17
|
1月前
|
SQL Java 数据库连接
如何在 Java 代码中使用 JSqlParser 解析复杂的 SQL 语句?
大家好,我是 V 哥。JSqlParser 是一个用于解析 SQL 语句的 Java 库,可将 SQL 解析为 Java 对象树,支持多种 SQL 类型(如 `SELECT`、`INSERT` 等)。它适用于 SQL 分析、修改、生成和验证等场景。通过 Maven 或 Gradle 安装后,可以方便地在 Java 代码中使用。
234 11
|
1月前
|
JSON Java 数据挖掘
利用 Java 代码获取淘宝关键字 API 接口
在数字化商业时代,精准把握市场动态与消费者需求是企业成功的关键。淘宝作为中国最大的电商平台之一,其海量数据中蕴含丰富的商业洞察。本文介绍如何通过Java代码高效、合规地获取淘宝关键字API接口数据,帮助商家优化产品布局、制定营销策略。主要内容包括: 1. **淘宝关键字API的价值**:洞察用户需求、优化产品标题与详情、制定营销策略。 2. **获取API接口的步骤**:注册账号、申请权限、搭建Java开发环境、编写调用代码、解析响应数据。 3. **注意事项**:遵守法律法规与平台规则,处理API调用限制。 通过这些步骤,商家可以在激烈的市场竞争中脱颖而出。
|
2月前
|
安全 Java 编译器
深入理解Java中synchronized三种使用方式:助您写出线程安全的代码
`synchronized` 是 Java 中的关键字,用于实现线程同步,确保多个线程互斥访问共享资源。它通过内置的监视器锁机制,防止多个线程同时执行被 `synchronized` 修饰的方法或代码块。`synchronized` 可以修饰非静态方法、静态方法和代码块,分别锁定实例对象、类对象或指定的对象。其底层原理基于 JVM 的指令和对象的监视器,JDK 1.6 后引入了偏向锁、轻量级锁等优化措施,提高了性能。
73 3
|
2月前
|
前端开发 Java 测试技术
java日常开发中如何写出优雅的好维护的代码
代码可读性太差,实际是给团队后续开发中埋坑,优化在平时,没有那个团队会说我专门给你一个月来优化之前的代码,所以在日常开发中就要多注意可读性问题,不要写出几天之后自己都看不懂的代码。
78 2
|
监控 Java 编译器
你的Java代码对JIT编译友好么?(转)
JIT编译器是Java虚拟机(以下简称JVM)中效率最高并且最重要的组成部分之一。但是很多的程序并没有充分利用JIT的高性能优化能力,很多开发者甚至也并不清楚他们的程序有效利用JIT的程度。 在本文中,我们将介绍一些简单的方法来验证你的程序是否对JIT友好。
971 0
|
2天前
|
Java 程序员 开发者
Java社招面试题:一个线程运行时发生异常会怎样?
大家好,我是小米。今天分享一个经典的 Java 面试题:线程运行时发生异常,程序会怎样处理?此问题考察 Java 线程和异常处理机制的理解。线程发生异常,默认会导致线程终止,但可以通过 try-catch 捕获并处理,避免影响其他线程。未捕获的异常可通过 Thread.UncaughtExceptionHandler 处理。线程池中的异常会被自动处理,不影响任务执行。希望这篇文章能帮助你深入理解 Java 线程异常处理机制,为面试做好准备。如果你觉得有帮助,欢迎收藏、转发!
33 14
|
5天前
|
安全 Java 程序员
Java 面试必问!线程构造方法和静态块的执行线程到底是谁?
大家好,我是小米。今天聊聊Java多线程面试题:线程类的构造方法和静态块是由哪个线程调用的?构造方法由创建线程实例的主线程调用,静态块在类加载时由主线程调用。理解这些细节有助于掌握Java多线程机制。下期再见! 简介: 本文通过一个常见的Java多线程面试题,详细讲解了线程类的构造方法和静态块是由哪个线程调用的。构造方法由创建线程实例的主线程调用,静态块在类加载时由主线程调用。理解这些细节对掌握Java多线程编程至关重要。
34 13

热门文章

最新文章