热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

深入探讨JVM高级特性:即时编译技术解析

在启用分层编译的情况下,即时编译器(JIT)的触发条件涉及多个因素,包括方法调用频率、代码复杂度和运行时性能数据。本文将详细解析这些条件,并探讨分层编译如何优化JVM的执行效率。
  1. 即时编译是用来 提升应用运行效率 的技术
  2. 代码会先在JVM上 解释执行 ,之后反复执行的 热点代码 会被 即时翻译成为机器码 ,直接运行在 底层硬件

分层编译

  1. HotSpot包含多个即时编译器:C1、C2和Graal(Java 10,实验性)
  2. 在 Java 7之前,需要根据程序的特性选择对应的 即时编译器
    • 对于 执行时间较短对启动性能有要求 的程序,采用 编译效率较快的C1 ,对应参数: -client
    • 对于 执行时间较长对峰值性能有要求 的程序,采用 生成代码执行效率较快的C2 ,对应参数: -server
  3. Java 7引入了 分层编译 (-XX:+TieredCompilation),综合了 C1的启动性能优势C2的峰值性能优势
  4. 分层编译将 JVM的执行状态 分了5个层次
    • 0:解释执行(也会profiling)
    • 1:执行 不带profiling 的C1代码
    • 2:执行仅带 方法调用次数循环回边执行次数 profiling的C1代码
    • 3:执行带 所有profiling 的C1代码
    • 4:执行C2代码
  5. 通常情况下, C2代码的执行效率比C1代码高出30%以上
  6. 对于C1代码的三种状态,按执行效率从高至低:1层 > 2层 > 3层
    • 1层的性能略高于2层,2层的性能比3层高出30%
    • profiling越多,额外的性能开销越大
  7. profiling:在程序执行过程中,收集能够反映程序执行状态的数据
    • profile:收集的数据
    • JDK附带的 hprof (CPU+Heap)
    • JVM 内置profiling
  8. Java 8默认开启了分层编译,无论开启还是关闭分层编译,原本的 -client-client 都是无效的
    • 如果 关闭分层编译 ,JVM将直接采用 C2
    • 如果只想用C1,在打开分层编译的同时,使用参数:-XX:TieredStopAtLevel=1

编译路径

JVM进阶 -- 浅谈即时编译

  1. 1层和4层是 终止状态
    • 当一个 方法终止状态 编译后,如果 编译后的代码没有失效 ,那么JVM 不会再次发出该方法的编译请求
  2. 通常情况下,热点方法会被3层的C1编译,然后再被4层的C2编译
  3. 如果方法的 字节码数目较少 (如getter/setter),并且 3层的profiling没有可收集的数据
    • JVM会断定 该方法对于C1和C2的执行效率相同
    • JVM会在3层的C1编译后, 直接选用1层的C1编译
    • 由于1层是 终止状态 ,JVM不会继续用4层的C2编译
  4. 在C1忙碌的情况下,JVM在 解释执行过程 中对程序进行 profiling ,而后直接由4层的C2编译
  5. 在C2忙碌的情况下,方法会被2层的C1编译,然后再被3层的C1编译,以减少方法在3层的执行时间

触发JIT的条件

  1. JVM是依据 方法的调用次数 以及 循环回边的执行次数 来触发JIT的
  2. JVM将在0层、2层和3层执行状态时进行profiling,其中包括方法的调用次数和循环回边的执行次数
    • 循环回边是一个控制流程图中的概念,在字节码中,可以简单理解为 往回跳 的指令
    • 在即时编译过程中,JVM会识别循环的头部和尾部, 循环尾部到循环头部的控制流就是真正意义上的循环回边
    • C1将在 循环回边 插入 循环回边计数器 的代码
    • 解释执行和C1代码中增加循环回边计数的 位置 并不相同,但这不会对程序造成影响
    • JVM不会对这些 计数器 进行 同步 操作,因此收集到的执行次数也 不是精确值
    • 只要该数值 足够大 ,就能表示对应的方法包含热点代码
  3. 不启动 分层编译时,当 方法的调用次数和循环回边的次数的和 超过-XX:CompileThreshold,便会触发JIT
    • 使用 C1 时,该值为 1500
    • 使用 C2 时,该值为 10000
  4. 启用 分层编译时,阈值大小是 动态调整
    • 阈值 * 系数

系数

系数的计算方法:
s = queue_size_X / (TierXLoadFeedback * compiler_count_X) + 1

其中X是执行层次,可取3或者4
queue_size_X:执行层次为X的待编译方法的数目
TierXLoadFeedback:预设好的参数,其中Tier3LoadFeedback为5,Tier4LoadFeedback为3
compiler_count_X:层次X的编译线程数目。

编译线程数

  1. 在64位JVM中,默认情况下,编译线程的总数目是根据 处理器数量 来调整的
    • -XX:+CICompilerCountPerCPU=true, 编译线程数依赖于处理器数量
    • -XX:+CICompilerCountPerCPU=false -XX:+CICompilerCount=N, 强制设定总编译线程数
  2. JVM会将这些编译线程按照1:2的比例分配给C1和C2(至少1个),对于4核CPU,总编译线程数为3
// -XX:+CICompilerCountPerCPU=true
n = log2(N) * log2(log2(N)) * 3 / 2
其中 N 为 CPU 核心数目,N >= 4

触发条件

当启用分层编译时,触发JIT的条件

i > TierXInvocationThreshold * s || (i > TierXMinInvocationThreshold * s  && i + b > TierXCompileThreshold * s)
其中i为方法调用次数,b为循环回边执行次数

Profiling

  1. 在分层编译中的0层、2层和3层,都会进行profiling,最为基础的是 方法的调用次数 以及 循环回边的执行次数
    • 主要拥有触发JIT
  2. 此外,0层和3层还会收集用于4层C2编译的数据,例如
    • branch profiling
      • 分支跳转字节码,包括跳转次数和不跳转次数
    • type profiling
      • 非私有实例方法调用指令: invokevirtual
      • 强制类型转换指令: checkcast
      • 类型测试指令: instanceof
      • 引用类型数组存储指令: aastore
  3. branch profiling和type profiling将给应用带来不少的 性能开销
    • 3层C1的性能比2层C1的性能低30%
    • 通常情况下,我们不会在 解析执行 过程中进行branch profiling和type profiling
      • 只有在方法 触发C1编译后 ,JVM认为该方法 有可能被C2编译 ,才会在该方法的C1代码中收集这些profile
    • 只有在 极端 情况下(如等待C1编译的方法数目太多),才会开始在 解释 执行过程中收集这些profile
    • C2可以根据收集得到的数据进行 猜测和假设 ,从而作出比较 激进的优化

branch profiling

Java代码

public static int foo(boolean f, int in) {
    int v;
    if (f) {
        v = in;
    } else {
        v = (int) Math.sin(in);
    }
    if (v == in) {
        return 0;
    } else {
        return (int) Math.cos(v);
    }
}

字节码

public static int foo(boolean, int);
  descriptor: (ZI)I
  flags: ACC_PUBLIC, ACC_STATIC
  Code:
    stack=2, locals=3, args_size=2
       0: iload_0
       1: ifeq          9       // false,跳转到偏移量为9的字节码
       4: iload_1
       5: istore_2
       6: goto          16
       9: iload_1
      10: i2d
      11: invokestatic          // Method java/lang/Math.sin:(D)D
      14: d2i
      15: istore_2
      16: iload_2
      17: iload_1
      18: if_icmpne     23      // 如果v!=in,跳转到偏移量为23的字节码
      21: iconst_0
      22: ireturn
      23: iload_2
      24: i2d
      25: invokestatic          // Method java/lang/Math.cos:(D)D
      28: d2i
      29: ireturn

优化过程

正常分支

JVM进阶 -- 浅谈即时编译

profiling

假设应用程序调用该方法,所传入的都是true,那么偏移量为1和偏移量为18的条件跳转指令所对应的分支profile中,其跳转的次数都是0。实际执行的分支如下:

JVM进阶 -- 浅谈即时编译

剪枝

C2根据这两个分支profile作出假设,在后续的执行过程中,这两个条件跳转指令仍旧不会执行,基于这个假设,C2不会在编译这两个条件跳转语句所对应的false分支(剪枝)。最终的结果是在第一个条件跳转之后,C2代码直接返回0

JVM进阶 -- 浅谈即时编译

小结

  1. 根据条件跳转指令的分支profile,即时编译器可以将 从未执行过 的分支减掉
    • 避免编译这些不会用到的代码
    • 节省 编译时间 以及部署代码所要消耗的 内存空间
  2. 剪枝同时也能精简数据流,从而触发更多的优化
  3. 现实中,分支profile出现仅跳转或者不跳转的情况并不常见
  4. 即时编译器对分支profile的利用也不仅仅限于剪枝
    • 还可以依据分支profile, 计算每一条执行路径的概率
    • 以便于某些编译器优化优先处理概率较高的路径

type profiling

Java代码

public static int hash(Object in) {
    if (in instanceof Exception) {
        return System.identityHashCode(in);
    } else {
        return in.hashCode();
    }
}

字节码

public static int hash(java.lang.Object);
  descriptor: (Ljava/lang/Object;)I
  flags: ACC_PUBLIC, ACC_STATIC
  Code:
    stack=1, locals=1, args_size=1
       0: aload_0
       1: instanceof        // class java/lang/Exception
       4: ifeq          12  // 不是Exception,跳转到偏移量为12的字节码
       7: aload_0
       8: invokestatic      // Method java/lang/System.identityHashCode:(Ljava/lang/Object;)I
      11: ireturn
      12: aload_0
      13: invokevirtual     // Method java/lang/Object.hashCode:()I
      16: ireturn

优化过程

正常分支

JVM进阶 -- 浅谈即时编译

profiling+优化

  1. 假设应用调用该方法时,所传入的Object皆为Integer实例
    • 偏移量为1的 instanceof 指令的 类型profile 仅包含Integer
    • 偏移量为4的分支跳转语句的 分支profile 不跳转次数为0
    • 偏移量为13的方法调用指令的 类型profile 仅包含Integer
  2. 测试instanceof
    • 如果instanceof的 目标类型是final类型 ,那么JVM仅需比较测试 对象的动态类型 是否为该final类型
    • 如果 目标类型不是final类型 ,JVM需要依次按下列顺序测试是否与目标类型一致
      • 该类本身
      • 该类的父类、祖先类
      • 该类所直接实现或间接实现的接口
  3. instanceof指令的类型profile仅包含Integer
    • JVM会假设在接下来的执行过程中,所输入的Object对象仍为Integer对象
    • 生成的代码将 直接测试所输入的动态类型是否为Integer ,如果是继续执行接下来的代码
  4. 然后,即时编译器会采用 针对分支profile的优化 以及 对方法调用的条件去虚化内联
    • 内联结果:生成的代码将测试所输入对象的动态类型是否为Integer,如果是,执行 Integer.hashCode() 方法的代码
public final class Integer ... {
    @Override
    public int hashCode() {
        return Integer.hashCode(value);
    }

    public static int hashCode(int value) {
        return value;
    }
}

针对上面三个profile的分支图

JVM进阶 -- 浅谈即时编译

进一步优化(剪枝)

JVM进阶 -- 浅谈即时编译

小结

  1. 和基于分支profile的优化一样,基于类型profile的优化同样也是作出假设,从而精简控制流以及数据流,两者的 核心是假设
  2. 对于 分支profile ,即时编译器假设 仅执行某一分支
  3. 对于 类型profile ,即时编译器假设的是 对象的动态类型仅为类型profile中的那几个
  4. 如果 假设失败 ,将进入 去优化

以上所述就是小编给大家介绍的《JVM进阶 -- 浅谈即时编译》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 我们 的支持!


推荐阅读
  • 本文探讨了如何通过一系列技术手段提升Spring Boot项目的并发处理能力,解决生产环境中因慢请求导致的系统性能下降问题。 ... [详细]
  • 深入解析Spring启动过程
    本文详细介绍了Spring框架的启动流程,帮助开发者理解其内部机制。通过具体示例和代码片段,解释了Bean定义、工厂类、读取器以及条件评估等关键概念,使读者能够更全面地掌握Spring的初始化过程。 ... [详细]
  • 深入解析SpringMVC核心组件:DispatcherServlet的工作原理
    本文详细探讨了SpringMVC的核心组件——DispatcherServlet的运作机制,旨在帮助有一定Java和Spring基础的开发人员理解HTTP请求是如何被映射到Controller并执行的。文章将解答以下问题:1. HTTP请求如何映射到Controller;2. Controller是如何被执行的。 ... [详细]
  • 在尝试使用C# Windows Forms客户端通过SignalR连接到ASP.NET服务器时,遇到了内部服务器错误(500)。本文将详细探讨问题的原因及解决方案。 ... [详细]
  • 在编译BSP包过程中,遇到了一个与 'gets' 函数相关的编译错误。该问题通常发生在较新的编译环境中,由于 'gets' 函数已被弃用并视为安全漏洞。本文将详细介绍如何通过修改源代码和配置文件来解决这一问题。 ... [详细]
  • 本文探讨了如何利用HTML5和JavaScript在浏览器中进行本地文件的读取和写入操作,并介绍了获取本地文件路径的方法。HTML5提供了一系列API,使得这些操作变得更加简便和安全。 ... [详细]
  • 本文详细介绍了Java中实现异步调用的多种方式,包括线程创建、Future接口、CompletableFuture类以及Spring框架的@Async注解。通过代码示例和深入解析,帮助读者理解并掌握这些技术。 ... [详细]
  • 本文深入探讨了UNIX/Linux系统中的进程间通信(IPC)机制,包括消息传递、同步和共享内存等。详细介绍了管道(Pipe)、有名管道(FIFO)、Posix和System V消息队列、互斥锁与条件变量、读写锁、信号量以及共享内存的使用方法和应用场景。 ... [详细]
  • 深入解析动态代理模式:23种设计模式之三
    在设计模式中,动态代理模式是应用最为广泛的一种代理模式。它允许我们在运行时动态创建代理对象,并在调用方法时进行增强处理。本文将详细介绍动态代理的实现机制及其应用场景。 ... [详细]
  • 本题要求在一组数中反复取出两个数相加,并将结果放回数组中,最终求出最小的总加法代价。这是一个经典的哈夫曼编码问题,利用贪心算法可以有效地解决。 ... [详细]
  • 深入剖析JVM垃圾回收机制
    本文详细探讨了Java虚拟机(JVM)中的垃圾回收机制,包括其意义、对象判定方法、引用类型、常见垃圾收集算法以及各种垃圾收集器的特点和工作原理。通过理解这些内容,开发人员可以更好地优化内存管理和程序性能。 ... [详细]
  • 使用WinForms 实现 RabbitMQ RPC 示例
    本文通过两个WinForms应用程序演示了如何使用RabbitMQ实现远程过程调用(RPC)。一个应用作为客户端发送请求,另一个应用作为服务端处理请求并返回响应。 ... [详细]
  • 本文探讨了Web开发与游戏开发之间的主要区别,旨在帮助开发者更好地理解两种开发领域的特性和需求。文章基于作者的实际经验和网络资料整理而成。 ... [详细]
  • LeetCode 347. 高频元素前 K 名
    本题旨在从给定数组中找出出现频率最高的前 K 个元素。难度等级为中等,使用优先队列(大顶堆)作为主要数据结构进行求解。 ... [详细]
  • BFS深搜hashtable来判断是横线还是竖线但是为啥还是90分啊呜呜!找不到原因#define_CRT_SECURE_NO_WARNINGS1#include ... [详细]
author-avatar
美煤MM就
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有