代码优化

字段访问相关优化

即时编译器关于字段访问的优化方式,以及死代码消除。

即时编译器将沿着控制流缓存字段存储、读取的值,并在接下来的字段读取操作时直接使用该缓存值。这要求生成缓存值的访问以及使用缓存值的读取之间没有方法调用、内存屏障,或者其他可能存储该字段的节点。

即时编译器还会优化冗余的字段存储操作。如果一个字段的两次存储之间没有对该字段的读取操作、方法调用以及内存屏障,那么即时编译器可以将第一个冗余的存储操作给消除掉。

死代码消除的两种形式。

第一种是局部变量的死存储消除以及部分死存储消除。它们可以通过转换为 Sea-of-Nodes IR 来完成。

第二种则是不可达分支。通过消除不可达分支,即时编译器可以精简数据流,并且减少编译时间以及最终生成机器码的大小。

循环优化

循环无关代码外提

所谓的循环无关代码(Loop-invariant Code),指的是循环中值不变的表达式。如果能够在不改变程序语义的情况下,将这些循环无关代码提出循环之外,那么程序便可以避免重复执行这些表达式,从而达到性能提升的效果。

循环展开

它指的是在循环体中重复多次循环迭代,并减少循环次数的编译优化。

在 C2 中,只有计数循环(Counted Loop)才能被展开。所谓的计数循环需要满足如下四个条件。

1.维护一个循环计数器,并且基于计数器的循环出口只有一个(但可以有基于其他判断条件的出口)。

2.循环计数器的类型为 int、short 或者 char(即不能是 byte、long,更不能是 float 或者 double)。

3.每个迭代循环计数器的增量为常数。

4.循环计数器的上限(增量为正数)或下限(增量为负数)是循环无关的数值。

循环展开的缺点显而易见:

它可能会增加代码的冗余度,导致所生成机器码的长度大幅上涨。

不过,随着循环体的增大,优化机会也会不断增加。一旦循环展开能够触发进一步的优化,总体的代码复杂度也将降低。

循环展开有一种特殊情况,那便是完全展开(Full Unroll)。当循环的数目是固定值而且非常小时,即时编译器会将循环全部展开。此时,原本循环中的循环判断语句将不复存在,取而代之的是若干个顺序执行的循环体

即时编译器会在循环体的大小与循环展开次数之间做出权衡。例如,对于仅迭代三次(或以下)的循环,即时编译器将进行完全展开;对于循环体 IR 节点数目超过阈值的循环,即时编译器则不会进行任何循环展开即时编译器会在循环体的大小与循环展开次数之间做出权衡。例如,对于仅迭代三次(或以下)的循环,即时编译器将进行完全展开;对于循环体 IR 节点数目超过阈值的循环,即时编译器则不会进行任何循环展开

循环判断外提(loop unswitching)

将循环中的 if 语句外提至循环之前,并且在该 if 语句的两个分支中分别放置一份循环代码

以及循环剥离(loop peeling)

将循环的前几个迭代或者后几个迭代剥离出循环的优化方式。一般来说,循环的前几个迭代或者后几个迭代都包含特殊处理。通过将这几个特殊的迭代剥离出去,可以使原本的循环体的规律性更加明显,从而触发进一步的优化

向量化

自动向量化的条件。

1.循环变量的增量应为 1,即能够遍历整个数组。

2.循环变量不能为 long 类型,否则 C2 无法将循环识别为计数循环。

3.循环迭代之间最好不要有数据依赖,例如出现类似于a[i] = a[i-1]的语句。

4.当循环展开之后,循环体内存在数据依赖,那么 C2 无法进行自动向量化。

5.循环体内不要有分支跳转。不要手工进行循环展开。

6.如果 C2 无法自动展开,那么它也将无法进行自动向量化

向量化优化借助的是 CPU 的 SIMD 指令,即通过单条指令控制多组数据的运算。它被称为 CPU 指令级别的并行。

HotSpot 虚拟机运用向量化优化的方式有两种。

第一种是使用 HotSpot intrinsic,在调用特定方法的时候替换为使用了 SIMD 指令的高效实现。Intrinsic 属于点覆盖,只有当应用程序明确需要这些 intrinsic 的语义,才能够获得由它带来的性能提升。

第二种是依赖即时编译器进行自动向量化,在循环展开优化之后将不同迭代的运算合并为向量运算。自动向量化的触发条件较为苛刻,因此也无法覆盖大多数用例。

注解处理器

Java 源代码的编译过程可分为三个步骤:

1.将源文件解析为抽象语法树;

2.调用已注册的注解处理器;

3.生成字节码。

如果在第 2 步调用注解处理器过程中生成了新的源文件,那么编译器将重复第 1、2 步,解析并且处理新生成的源文件。每次重复我们称之为一轮(Round)。

也就是说,第一轮解析、处理的是输入至编译器中的已有源文件。如果注解处理器生成了新的源文件,则开始第二轮、第三轮,解析并且处理这些新生成的源文件。当注解处理器不再生成新的源文件,编译进入最后一轮,并最终进入生成字节码的第 3 步。

注解处理器主要有三个用途。

一是定义编译规则,并检查被编译的源文件。

二是修改已有源代码。

三是生成新的源代码。

其中,第二种涉及了 Java 编译器的内部 API,因此并不推荐。第三种较为常见,是 OpenJDK 工具 jcstress,以及 JMH 生成测试代码的方式。

Java 源代码的编译过程可分为三个步骤,分别为解析源文件生成抽象语法树,调用已注册的注解处理器,和生成字节码。如果在第 2 步中,注解处理器生成了新的源代码,那么 Java 编译器将重复第 1、2 步,直至不再生成新的源代码。

性能基准测试项目 JMH。

Java 程序的性能测试存在着许多深坑,有来自 Java 虚拟机的,有来自操作系统的,甚至有来自硬件系统的。如果没有足够的知识,那么性能测试的结果很有可能是有偏差的。

性能基准测试框架 JMH 是 OpenJDK 中的其中一个开源项目。它内置了许多功能,来规避由 Java 虚拟机中的即时编译器或者其他优化对性能测试造成的影响。此外,它还提供了不少策略来降低来自操作系统以及硬件系统的影响。

开发人员仅需将所要测试的业务逻辑通过@Benchmark注解,便可以让 JMH 的注解处理器自动生成真正的性能测试代码,以及相应的性能测试配置文件。


$ mvn archetype:generate \
          -DinteractiveMode=false \
          -DarchetypeGroupId=org.openjdk.jmh \
          -DarchetypeArtifactId=jmh-java-benchmark-archetype \
          -DgroupId=org.sample \
          -DartifactId=test \
          -Dversion=1.21
$ cd test

@Benchmark注解

测试方法加注解即可

基准测试框架 JMH 的进阶功能。

1.@Fork允许开发人员指定所要 Fork 出的 Java 虚拟机的数目。

2.@BenchmarkMode允许指定性能数据的格式。

3.@Warmup和@Measurement允许配置预热迭代或者测试迭代的数目,每个迭代的时间以及每个操作包含多少次对测试方法的调用。

4.@State允许配置测试程序的状态。

5.测试前对程序状态的初始化以及测试后对程序状态的恢复或者校验可分别通过@Setup和@TearDown来实现。

Java虚拟机的监控及诊断工具(命令行)

jps:它将打印所有正在运行的 Java 进程的相关信息。

在默认情况下,jps的输出信息包括 Java 进程的进程 ID 以及主类名。我们还可以通过追加参数,来打印额外的信息。例如,-l将打印模块名以及包名;-v将打印传递给 Java 虚拟机的参数(如-XX:+UnlockExperimentalVMOptions -XX:+UseZGC);-m将打印传递给主类的参数。

需要注意的是,如果某 Java 进程关闭了默认开启的UsePerfData参数(即使用参数-XX:-UsePerfData),那么jps命令(以及下面介绍的jstat)将无法探

jstat:可用来打印目标 Java 进程的性能数据。

-class将打印类加载相关的数据,

-compiler和-printcompilation将打印即时编译相关的数据。

剩下的都是以-gc为前缀的子命令,它们将打印垃圾回收相关的数据。

默认情况下,jstat只会打印一次性能数据。我们可以将它配置为每隔一段时间打印一次,直至目标 Java 进程终止,或者达到我们所配置的最大打印次数

JDK 中用于监控及诊断的命令行工具。

1:jps将打印所有正在运行的 Java 进程。

2:jstat允许用户查看目标 Java 进程的类加载、即时编译以及垃圾回收相关的信息。它常用于检测垃圾回收问题以及内存泄漏问题。

3:jmap允许用户统计目标 Java 进程的堆中存放的 Java 对象,并将它们导出成二进制文件。

4:jinfo将打印目标 Java 进程的配置参数,并能够改动其中 manageabe 的参数。

5:jstack将打印目标 Java 进程中各个线程的栈轨迹、线程状态、锁状况等信息。它还将自动检测死锁。

6:jcmd则是一把瑞士军刀,可以用来实现前面除了jstat之外所有命令的功能

监控及诊断工具GUI

两个 GUI 工具:eclipse MAT 以及 JMC。

eclipse MAT 可用于分析由jmap命令导出的 Java 堆快照。

它包括两个相对比较重要的视图,分别为直方图和支配树。

直方图展示了各个类的实例数目以及这些实例的 Shallow heap 或 Retained heap 的总和。

支配树则展示了快照中每个对象所直接支配的对象。

Java Mission Control 是 Java 虚拟机平台上的性能监控工具。

Java Flight Recorder 是 JMC 的其中一个组件,能够以极低的性能开销收集 Java 虚拟机的性能数据。

JFR 的启用方式有三种,

分别为在命令行中使用-XX:StartFlightRecording=参数,

使用jcmd的JFR.*子命令,

以及 JMC 的 JFR 插件。JMC 能够加载 JFR 的输出结果,并且生成各种信息丰富的图表。

JNI 的运行机制

Java 中的 native 方法的链接方式主要有两种。

一是按照 JNI 的默认规范命名所要链接的 C 函数,并依赖于 Java 虚拟机自动链接。

另一种则是在 C 代码中主动链接。

JNI 提供了一系列 API 来允许 C 代码使用 Java 语言特性。

这些 API 不仅使用了特殊的数据结构来表示 Java 类,还拥有特殊的异常处理模式。

JNI 中的引用可分为局部引用和全局引用。这两者都可以阻止垃圾回收器回收被引用的 Java 对象。不同的是,局部引用在 native 方法调用返回之后便会失效。传入参数以及大部分 JNI API 函数的返回值都属于局部引用

 Java agent 以及字节码注入

我们可以通过 Java agent 的类加载拦截功能,修改某个类所对应的 byte 数组,并利用这个修改过后的 byte 数组完成接下来的类加载。

基于字节码注入的 profiler,可以统计程序运行过程中某些行为的出现次数。如果需要收集 Java 核心类库的数据,那么我们需要小心避免无限递归调用。

另外,我们还需通过自定义类加载器来解决命名空间的问题。

由于字节码注入会产生观察者效应,因此基于该技术的 profiler 所收集到的数据并不能反映程序的真实运行状态。它所反映的是程序在被注入的情况下的执行状态。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值