JVM系列9-后端编译与优化

简介: JVM系列9-后端编译与优化

一、即时编译器

1 编译器分类

  • 前端编译器:把 *.java 文件转变成 .class 文件的过程;如 JDK 的 Javac,Eclipse JDT 中的增量式编译器。
  • 即使编译器:常称为 JIT 编译器(Just In Time Complier),在运行期把字节码转变成本地机器码的过程;如 HotSpot 虚拟机中的 C1、C2 编译器,Graal 编译器。
  • 提前编译器:直接把程序编译成目标机器指令集相关的二进制代码的过程。如 JDK 的 jaotc,GUN Compiler for the Java(GCJ),Excelsior JET 。

2 解释器与编译器

在 HotSpot 虚拟机中,Java 程序最初都是通过解释器(Interpreter)进行解释执行的,其优点在于可以省去编译时间,让程序快速启动。当程序启动后,如果虚拟机发现某个方法或代码块的运行特别频繁,就会使用编译器将其编译为本地机器码,并使用各种手段进行优化,从而提高执行效率,这就是即时编译器。HotSpot 内置了两个(或三个)即时编译器:

  • 客户端编译器 (Client Complier):简称 C1;
  • 服务端编译器 (Servier Complier):简称 C2,在有的资料和 JDK 源码中也称为 Opto 编译器;
  • Graal 编译器:在 JDK 10 时才出现,长期目标是替代 C2。

在分层编译的工作模式出现前,采用客户端编译器还是服务端编译器完全取决于虚拟机是运行在客户端模式还是服务端模式下,可以在启动时通过 -client-server 参数进行指定,也可以让虚拟机根据自身版本和宿主机性能来自主选择。

3 分层编译

由于即时编译器编译本地代码需要占用程序运行时间, 通常要编译出优化程度越高的代码, 所花 费的时间便会越长; 而且想要编译出优化程度更高的代码, 解释器可能还要替编译器收集性能监控信 息, 这对解释执行阶段的速度也有所影响。 为了在程序启动响应速度与运行效率之间达到最佳平衡, HotSpot虚拟机在编译子系统中加入了分层编译的功能(Tiered Compilation):

  • 第 0 层:程序纯解释执行,并且解释器不开启性能监控功能;
  • 第 1 层:使用客户端编译器将字节码编译为本地代码来运行,进行简单可靠的稳定优化,不开启性能监控功能;
  • 第 2 层:仍然使用客户端编译执行,仅开启方法及回边次数统计等有限的性能监控;
  • 第 3 层:仍然使用客户端编译执行,开启全部性能监控;
  • 第 4 层:使用服务端编译器将字节码编译为本地代码,其耗时更长,并且会根据性能监控信息进行一些不可靠的激进优化。

以上层次并不是固定不变的,根据不同的运行参数和版本,虚拟机可以调整分层的数量。各层次编译之间的交互转换关系如下图所示:

image.png

实施分层编译后,解释器、客户端编译器和服务端编译器就会同时工作,可以用客户端编译器获取更高的编译速度、用服务端编译器来获取更好的编译质量。

4 热点探测( Hot Spot Code Detection)

即时编译器编译的目标是 “热点代码”,它主要分为以下两类:

  • 被多次调用的方法。
  • 被多次执行循环体。这里指的是一个方法只被少量调用过,但方法体内部存在循环次数较多的循环体,此时也认为是热点代码。但编译器编译的仍然是循环体所在的方法,而不会单独编译循环体。

判断某段代码是否是热点代码的行为称为 “热点探测” (Hot Spot Code Detection),主流的热点探测方法有以下两种:

  • 基于采样的热点探测 (Sample Based Hot Spot Code Detection):采用这种方法的虚拟机会周期性地检查各个线程的调用栈顶,如果发现某个(或某些)方法经常出现在栈顶,那么就认为它是 “热点方法”。基于采样的热点探测的好处是实现简单高效, 还可以很容易地获取方法调用关系( 将调用堆栈展 开即可) , 缺点是很难精确地确认一个方法的热度, 容易因为受到线程阻塞或别的外界因素的影响而 扰乱热点探测。
  • 基于计数的热点探测 (Counter Based Hot Spot Code Detection):采用这种方法的虚拟机会为每个方法(甚至是代码块)建立计数器,统计方法的执行次数,如果执行次数超过一定的阈值就认为它是 “热点方法”。这种统计方法实现起来要麻烦一些, 需要为每个方法建立并维护计数器, 而且不能 直接获取到方法的调用关系。 但是它的统计结果相对来说更加精确严谨

这两种探测手段在商用Java虚拟机中都有使用到, 譬如J9用过第一种采样热点探测, 而在HotSpot 虚拟机中使用的是第二种基于计数器的热点探测方法, 为了实现热点计数, HotSpot为每个方法准备了 两类计数器: 方法调用计数器( Invocation Counter) 和回边计数器( Back Edge Counter, “回边”的意思 就是指在循环边界往回跳转) 。 当虚拟机运行参数确定的前提下, 这两个计数器都有一个明确的阈 值, 计数器阈值一旦溢出, 就会触发即时编译。

二、编译器优化技术

即时编译器除了将字节码编译为本地机器码外,还会对代码进行一定程度的优化,它包含多达几十种优化技术,这里选取其中代表性的四种进行介绍:

1 方法内联

最重要的优化手段,它会将目标方法中的代码原封不动地 “复制” 到发起调用的方法之中,避免发生真实的方法调用,并采用名为类型继承关系分析(Class Hierarchy Analysis,CHA)的技术来解决虚方法(Java 语言中默认的实例方法都是虚方法)的内联问题。

2 逃逸分析

逃逸行为主要分为以下两类:

  • 方法逃逸:当一个对象在方法里面被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中,此时称为方法逃逸;
  • 线程逃逸:当一个对象在方法里面被定义后,它可能被外部线程所访问,例如赋值给可以在其他线程中访问的实例变量,此时称为线程,其逃逸程度高于方法逃逸。
public static StringBuilder concat(String... strings) {
    StringBuilder sb = new StringBuilder();
    for (String string : strings) {
        sb.append(string);
    }
    return sb; // 发生了方法逃逸
}

public static String concat(String... strings) {
    StringBuilder sb = new StringBuilder();
    for (String string : strings) {
        sb.append(string);
    }
    return sb.toString(); // 没有发生方法逃逸
}
复制代码

如果能证明一个对象不会逃逸到方法或线程之外,或者逃逸程度比较低(只逃逸出方法而不会逃逸出线程),则可以为这个对象实例采取不同程序的优化:

  • 栈上分配 (Stack Allocations):如果一个对象不会逃逸到线程外,那么将会在栈上分配内存来创建这个对象,而不是 Java 堆上,此时对象所占用的内存空间就会随着栈帧的出栈而销毁,从而可以减轻垃圾回收的压力。
  • 标量替换 (Scalar Replacement):如果一个数据已经无法再分解成为更小的数据类型,那么这些数据就称为标量(如 int、long 等数值类型及 reference 类型等);反之,如果一个数据可以继续分解,那它就被称为聚合量(如对象)。如果一个对象不会逃逸外方法外,那么就可以将其改为直接创建若干个被这个方法使用的成员变量来替代,从而减少内存占用。
  • 同步消除 (Synchronization Elimination):如果一个变量不会逃逸出线程,那么对这个变量实施的同步措施就可以消除掉。

3 公共子表达式消除

如果一个表达式 E 之前已经被计算过了,并且从先前的计算到现在 E 中所有变量的值都没有发生过变化,那么 E 这次的出现就称为公共子表达式。对于这种表达式,无需再重新进行计算,只需要直接使用前面的计算结果即可。

4 数组边界检查消除

对于虚拟机执行子系统来说,每次数组元素的读写都带有一次隐含的上下文检查以避免访问越界。如果数组的访问发生在循环之中,并且使用循环变量来访问数据,即循环变量的取值永远在 [0,list.length) 之间,那么此时就可以消除整个循环的数据边界检查,从而避免多次无用的判断


目录
相关文章
|
20天前
|
存储 缓存 负载均衡
高效后端开发中的架构设计与优化策略
在当今快速发展的技术环境中,高效的后端开发不仅仅依赖于编程技能,更需要精心设计的架构和优化策略。本文探讨了如何通过合理的架构设计和优化策略,提升后端系统的性能和可维护性,以应对复杂的业务需求和大规模的用户访问。【7月更文挑战第5天】
27 1
|
21天前
|
存储 NoSQL 数据处理
高效数据处理与后端优化:现代技术实践与挑战
在当今数字化快速发展的环境下,高效的数据处理成为了后端开发的核心挑战之一。本文探讨了现代后端技术中的数据处理方法与优化策略,深入分析了面临的挑战及其解决方案,旨在为开发者提供实用的指导与技术思路。【7月更文挑战第4天】
19 1
|
15天前
|
缓存 安全 算法
Java面试题:如何通过JVM参数调整GC行为以优化应用性能?如何使用synchronized和volatile关键字解决并发问题?如何使用ConcurrentHashMap实现线程安全的缓存?
Java面试题:如何通过JVM参数调整GC行为以优化应用性能?如何使用synchronized和volatile关键字解决并发问题?如何使用ConcurrentHashMap实现线程安全的缓存?
12 0
|
20天前
|
缓存 开发框架 监控
优化后端服务响应时间的关键策略与实践
在当今数字化时代,优化后端服务响应时间至关重要。本文探讨了几种关键策略和实践方法,帮助开发团队提高系统性能和用户体验。通过合理的资源分配、技术选型和代码优化,可以有效缩短响应时间,提升系统的整体效率和稳定性。【7月更文挑战第5天】
18 0
|
25天前
|
存储 NoSQL Java
大事件后端项目34_登录优化----redis_SpringBoot集成redis
大事件后端项目34_登录优化----redis_SpringBoot集成redis
大事件后端项目34_登录优化----redis_SpringBoot集成redis
|
15天前
|
缓存 Prometheus 监控
Java面试题:如何监控和优化JVM的内存使用?详细讲解内存调优的几种方法
Java面试题:如何监控和优化JVM的内存使用?详细讲解内存调优的几种方法
33 3
|
3天前
|
Java 编译器 C++
HotSpot JVM中的分层编译策略是怎样的
HotSpot JVM中的分层编译策略是怎样的?
|
27天前
|
Java 编译器
Java健壮性 Java可移植性 JDK, JRE, JVM三者关系 Java的加载与执行原理 javac编译与JAVA_HOME环境变量介绍 Java中的注释与缩进 main方法的args参数
Java健壮性 Java可移植性 JDK, JRE, JVM三者关系 Java的加载与执行原理 javac编译与JAVA_HOME环境变量介绍 Java中的注释与缩进 main方法的args参数
22 1
|
15天前
|
存储 设计模式 监控
Java面试题:简述JVM的内存结构,包括堆、栈、方法区等。栈内存优化的方法有 哪些?
Java面试题:简述JVM的内存结构,包括堆、栈、方法区等。栈内存优化的方法有 哪些?
21 0
|
25天前
|
NoSQL Redis 数据安全/隐私保护
大事件后端项目35——登录优化_redis_主动失效机制实现
大事件后端项目35——登录优化_redis_主动失效机制实现