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 层:使用服务端编译器将字节码编译为本地代码,其耗时更长,并且会根据性能监控信息进行一些不可靠的激进优化。

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

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

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) 之间,那么此时就可以消除整个循环的数据边界检查,从而避免多次无用的判断



目录
相关文章
|
21天前
|
缓存 负载均衡 算法
后端架构设计中的优化技巧
【2月更文挑战第9天】 后端架构设计是一个复杂而关键的工作,不仅需要考虑系统的可靠性和扩展性,还需要保证系统的高性能。本文将介绍一些后端架构设计中的优化技巧,包括数据库设计、缓存优化、负载均衡等方面的内容,帮助开发者在设计后端架构时更好地提升系统性能。
50 1
|
21天前
|
前端开发 Java 编译器
深入理解jvm - 编译优化(上)
深入理解jvm - 编译优化(上)
89 0
|
21天前
|
自然语言处理 前端开发 Java
深入浅出JVM(六)之前端编译过程与语法糖原理
深入浅出JVM(六)之前端编译过程与语法糖原理
|
21天前
|
存储 缓存 NoSQL
Redis多级缓存指南:从前端到后端全方位优化!
本文探讨了现代互联网应用中,多级缓存的重要性,特别是Redis在缓存中间件的角色。多级缓存能提升数据访问速度、系统稳定性和可扩展性,减少数据库压力,并允许灵活的缓存策略。浏览器本地内存缓存和磁盘缓存分别优化了短期数据和静态资源的存储,而服务端本地内存缓存和网络内存缓存(如Redis)则提供了高速访问和分布式系统的解决方案。服务器本地磁盘缓存因I/O性能瓶颈和复杂管理而不推荐用于缓存,强调了内存和网络缓存的优越性。
208 47
|
5天前
|
消息中间件 缓存 负载均衡
构建高性能的后端服务:优化策略与实践
在当今互联网时代,构建高性能的后端服务至关重要。本文将深入探讨如何通过优化策略与实践来提升后端服务的性能。我们将从数据库优化、缓存策略、异步处理和负载均衡等方面展开讨论,帮助开发者构建出稳定、高效的后端架构。
11 2
|
5天前
|
消息中间件 数据库 网络架构
构建高效后端:微服务架构的优化策略
【5月更文挑战第31天】在这篇文章中,我们将深入探讨如何通过采用微服务架构来提升后端开发的效率和性能。我们将分析微服务架构的关键优势,并讨论如何克服实施过程中的挑战。通过具体的案例研究,我们将展示如何优化微服务架构以实现最佳的性能和可维护性。无论你是后端开发的新手还是经验丰富的专业人士,这篇文章都将为你提供有价值的见解和实用的技巧。
|
21天前
|
前端开发 测试技术 持续交付
《跨界合作:前端与后端如何优化协作效率》
在当今软件开发领域,前端和后端开发团队通常是分开工作的,但他们的协作质量直接影响着项目的成功与否。本文将探讨如何通过优化前端与后端的协作方式,提高开发效率和项目质量,从而实现更好的跨界合作。
|
21天前
|
存储 自然语言处理 前端开发
深入浅出JVM(七)之执行引擎的解释执行与编译执行
深入浅出JVM(七)之执行引擎的解释执行与编译执行
|
21天前
|
API 开发者 UED
构建高效微服务架构:后端开发的新趋势移动应用与系统:开发与优化的艺术
【4月更文挑战第30天】 随着现代软件系统对可伸缩性、灵活性和敏捷性的日益需求,传统的单体应用架构正逐渐向微服务架构转变。本文将探讨微服务架构的核心概念,分析其优势,并着重讨论如何利用最新的后端技术栈实现一个高效的微服务系统。我们将涵盖设计模式、服务划分、数据一致性、服务发现与注册、API网关以及容器化等关键技术点,为后端开发者提供一份实操指南。 【4月更文挑战第30天】 在数字化时代的浪潮中,移动应用和操作系统的紧密交织已成为日常生活和商业活动的基石。本文将深入探讨移动应用开发的关键技术、跨平台开发工具的选择以及移动操作系统的架构和性能优化策略。通过分析当前移动应用开发的挑战与机遇,我们将
|
21天前
|
存储 缓存 算法
后端技术优化与应用研究
后端技术优化与应用研究
17 1