麻烦大家帮我投一票哈,谢谢

经常听到 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:

解释器工作机制:



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

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

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优化代码

每日一刷,轻松提升技术,斩获各种offer:

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

  1. 90% 的 Java 程序员都说不上来的为何 Java 代码越执行越快(2)- TLAB预热

    经常听到 Java 性能不如 C/C++ 的言论,也经常听说 Java 程序需要预热,那么其中主要原因是啥呢? 面试的时候谈到 JVM,也有很多面试官喜欢问,为啥 Java 程序越执行越快呢? 一般人 ...

  2. Java程序员都应该去使用一下这款强大的国产工具类库

    这不是标题党,今天给大家推荐一个很棒的国产工具类库:Hutool.可能有很多朋友已经知道这个类库了,甚至在已经在使用了,如果你还没有使用过,那不妨去尝试一下,我们项目组目前也在用这个.这篇文章来简单介 ...

  3. 2020年薪30W的Java程序员都要求熟悉JVM与性能调优!

    前言 作为Java程序员,你有没有被JVM伤害过?面试的时候是否碰到过对JVM的灵魂拷问?   一.JVM 内存区域划分 1.程序计数器(线程私有) 程序计数器(Program Counter Reg ...

  4. Java程序员都需要懂的「反射」

    前言 只有光头才能变强. 文本已收录至我的GitHub精选文章,欢迎Star:https://github.com/ZhongFuCheng3y/3y 今天来简单写一下Java的反射.本来没打算写反射 ...

  5. 99.9%的Java程序员都说不清的问题:JVM中的对象内存布局?

    本文转载自公众号:石彬的架构笔记,阅读大约需要8分钟. 作者:李瑞杰 目前就职于阿里巴巴,资深 JVM 研究人员 在 Java 程序中,我们拥有多种新建对象的方式.除了最为常见的 new 语句之外,我 ...

  6. Java程序员都要懂得知识点:反射

    摘要:Java反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法:对于任意一个对象,都能够调用它的任意一个方法和属性:这种动态获取的信息以及动态调用对象的方法的功能称为java语 ...

  7. 看完这篇微服务架构设计思想,90%的Java程序员都收藏了

    本博客强烈推荐: Java电子书高清PDF集合免费下载 https://www.cnblogs.com/yuxiang1/p/12099324.html 微服务 软件架构是一个包含各种组织的系统组织, ...

  8. 阿里面试Java程序员都问些什么?

    刚开始也是小白,也是一步步成成起来的.需要提的一点是,你将来是需要靠这个吃饭的,所以请对找工作保持十二分的热情,而且越早准备越好. 阿里一面 一面是在上午9点多接到支付宝的面试电话的,因为很期望能够尽 ...

  9. 90 % Java 程序员被误导的一个性能优化策略

    我们经常看到一些 Java 性能优化的书或者理念,说不要在循环内定义变量,这样会占用过多的内存影响性能,而要在循环外面定义.接触 Java 这么久以来,相信很多 Java 程序员都被这种代码性能优化策 ...

随机推荐

  1. java的注释方法

    1.单行注释 //注释的内容 2.多行注释 /....../ 3./**......*/,这种方式和第二种方式相似.这种格式是为了便于javadoc程序自动生成文档.

  2. 技术选型关于redis客户端选择

    redis作为分布式缓存框架的首选  相信已经毋庸置疑.能高效.合理的使用好它  必定能提升系统的可用性,高性能.高吞吐量的保障.但选择一个客户端,充分发挥它的能力,就是一个选型问题.现在市场上能选择 ...

  3. druid监控

    1 @ConfigurationProperties(prefix = "spring.datasource") 2 @Bean 3 public DataSource druid ...

  4. java中装箱,拆箱

    (1)包装类 java中不能定义基本数据类型的对象,但是提供了基本数据类型的包装类来解决这一问题,例如int类型的包装类Integer, 包装类的常用方法有Integer.ParseInt()方法类将 ...

  5. linux系统重启后提示An error occurred during the file system check.

    一.问题描述 生产环境中一台浪潮NF8480M3外观红灯报警,鉴于无法登陆带外管理口,只能对服务器进行断电重启操作 二.问题现象 重启后进入开机过程并报错,正常来说进入此界面后直接输入root密码即可 ...

  6. System类常用方法

    System类常用方法 public static long currentTimeMills() 获取当前系统时间,以毫秒值为单位的当前时间. public static void arraycop ...

  7. NIO基础操作

    原文链接http://zhhll.icu/2020/05/18/java%E5%9F%BA%E7%A1%80/IO/NIO%E5%9F%BA%E6%9C%AC%E6%93%8D%E4%BD%9C/ N ...

  8. MongoDB按照嵌套数组中的map的某个key无法正常排序的问题

    前阵子同事有一个需求: 在一个数组嵌套map的结构中,首先按照map中的某个key进行筛选,再按照map中的某个key进行排序,但是奇怪的是数据总是乱序的. 再检查了代码和数据之后并没有发现什么错误, ...

  9. #2020征文-开发板# 用鸿蒙开发AI应用(一)硬件篇

    目录: 前言 开发板简介 产品特色及功能 产品参数 各个主板功能简介 Hi3516DV300 芯片手册 前言鸿蒙2.0的系统刚开源出来,华为志在打造1+8+N万物互联的全场景智慧生活,不仅是国产操作系 ...

  10. postgresql-从表中随机获取一条记录

    目录 postgresql如何从表中高效的随机获取一条记录 随机获取一条记录random() 改写1 改写2 改写3 对比 注意 结语 postgresql如何从表中高效的随机获取一条记录 selec ...