热门标签 | 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进阶 -- 浅谈即时编译》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 我们 的支持!


推荐阅读
  • 2019年后蚂蚁集团与拼多多面试经验详述与深度剖析
    2019年后蚂蚁集团与拼多多面试经验详述与深度剖析 ... [详细]
  • 深入解析零拷贝技术(Zerocopy)及其应用优势
    零拷贝技术(Zero-copy)是Netty框架中的一个关键特性,其核心在于减少数据在操作系统内核与用户空间之间的传输次数。通过避免不必要的内存复制操作,零拷贝显著提高了数据传输的效率和性能。本文将深入探讨零拷贝的工作原理及其在实际应用中的优势,包括降低CPU负载、减少内存带宽消耗以及提高系统吞吐量等方面。 ... [详细]
  • 深入理解Spark框架:RDD核心概念与操作详解
    RDD是Spark框架的核心计算模型,全称为弹性分布式数据集(Resilient Distributed Dataset)。本文详细解析了RDD的基本概念、特性及其在Spark中的关键操作,包括创建、转换和行动操作等,帮助读者深入理解Spark的工作原理和优化策略。通过具体示例和代码片段,进一步阐述了如何高效利用RDD进行大数据处理。 ... [详细]
  • 在操作系统中,阻塞状态与挂起状态有着显著的区别。阻塞状态通常是指进程因等待某一事件(如I/O操作完成)而暂时停止执行,而挂起状态则是指进程被系统暂时移出内存,以释放资源或降低系统负载。此外,本文还深入分析了`sleep()`函数的实现机制,探讨了其在不同操作系统中的具体实现方式及其对进程调度的影响。通过这些分析,读者可以更好地理解操作系统如何管理进程的不同状态以及`sleep()`函数在其中的作用。 ... [详细]
  • 在Java中,一个类可以实现多个接口,但是否能够继承多个类则存在限制。本文探讨了Java中实现多继承的方法及其局限性,详细分析了通过接口、抽象类和组合等技术手段来模拟多继承的策略,并讨论了这些方法的优势和潜在问题。 ... [详细]
  • 在 Java 中,`synchronized` 关键字用于实现线程同步,确保多个线程在同一时间只能有一个线程访问特定的代码块或方法。可以将其比喻为一个大型房屋,其中大门始终开放,但每个房间的门只能由持有特定钥匙的线程打开。通过这种方式,`synchronized` 有效地防止了多线程环境下的数据竞争和不一致问题。该关键字常用于需要保证线程安全的场景,如共享资源的访问控制、并发编程中的临界区管理等。 ... [详细]
  • Java 零基础入门:SQL Server 学习笔记(第21篇)
    Java 零基础入门:SQL Server 学习笔记(第21篇) ... [详细]
  • 如何在Java中高效构建WebService
    本文介绍了如何利用XFire框架在Java中高效构建WebService。XFire是一个轻量级、高性能的Java SOAP框架,能够简化WebService的开发流程。通过结合MyEclipse集成开发环境,开发者可以更便捷地进行项目配置和代码编写,从而提高开发效率。此外,文章还详细探讨了XFire的关键特性和最佳实践,为读者提供了实用的参考。 ... [详细]
  • 深入解析Java中HashCode的功能与应用
    本文深入探讨了Java中HashCode的功能与应用。在Java中,HashCode主要用于提高哈希表(如HashMap、HashSet)的性能,通过快速定位对象存储位置,减少碰撞概率。文章详细解析了HashCode的生成机制及其在集合框架中的作用,帮助开发者更好地理解和优化代码。此外,还介绍了如何自定义HashCode方法以满足特定需求,并讨论了常见的实现误区和最佳实践。 ... [详细]
  • MongoDB Aggregates.group() 方法详解与编程实例 ... [详细]
  • 本课程详细解析了Spring AOP的核心概念及其增强机制,涵盖前置增强、后置增强和环绕增强等类型。通过具体示例,深入探讨了如何在实际开发中有效运用这些增强技术,以提升代码的模块化和可维护性。此外,还介绍了Spring AOP在异常处理和性能监控等场景中的应用,帮助开发者更好地理解和掌握这一强大工具。 ... [详细]
  • 掌握DSP必备的56个核心问题,我已经将其收藏以备不时之需! ... [详细]
  • 在iOS平台上,应用的流畅操作体验一直备受赞誉。然而,过去开发者往往将更多精力集中在功能实现上,而对性能优化的关注相对较少。本文深入探讨了iOS应用性能优化的关键要点与实践方法,旨在帮助开发者提升应用的响应速度、降低功耗,并改善整体用户体验。通过具体案例分析和技术解析,文章提供了实用的优化策略,包括代码层面的改进、资源管理优化以及界面渲染效率的提升等。 ... [详细]
  • 深入解析JDK中的四种随机数生成器
    我们从jdk8说起。主要是四个随机数生成器。神马?有四个? 接下来我们简单说下这几个类的使用场景,来了解其中的细微差别,和a ... [详细]
  • c#学Java–Java基本语法1.类比JAVA .NETJVM CLRJDK  FCL2.java命名约定类名称应以大写字母开头,并成为容易理解的名词或组合。如 ... [详细]
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社区 版权所有