写点什么

你的 Java 代码对 JIT 编译友好么?

  • 2015-09-05
  • 本文字数:5369 字

    阅读完需:约 18 分钟

JIT 编译器是 Java 虚拟机(以下简称 JVM)中效率最高并且最重要的组成部分之一。但是很多的程序并没有充分利用 JIT 的高性能优化能力,很多开发者甚至也并不清楚他们的程序有效利用 JIT 的程度。

在本文中,我们将介绍一些简单的方法来验证你的程序是否对 JIT 友好。这里我们并不打算覆盖诸如 JIT 编译器工作原理这些细节。只是提供一些简单基础的检测和方法来帮助你的代码对 JIT 友好,进而得到优化。

JIT 编译的关键一点就是 JVM 会自动地监控正在被解释器执行的方法。一旦某个方法被视为频繁调用,这个方法就会被标记,进而编译成本地机器指令。这些频繁执行的方法的编译由后台的一个 JVM 线程来完成。在编译完成之前,JVM 会执行这个方法的解释执行版本。一旦该方法编译完成,JVM 会使用将方法调度表中该方法的解释的版本替换成编译后的版本。

Hotspot 虚拟机有很多 JIT 编译优化的技术,但是其中最重要的一个优化技术就是内联。在内联的过程中,JIT 编译器有效地将一个方法的方法体提取到其调用者中,从而减少虚方法调用。举个例子,看如下的代码:

复制代码
<span>public</span> <span>int</span> <span>add</span>(<span>int</span> x, <span>int</span> y) {
<span>return</span> x + y;
}
<span>int</span> result = add(a, b);

当内联发生之后,上述代码会变成

<span>int result </span>=<span> a + b;</span>上面的变量 a 和 b 替换了方法的参数,并且 add 方法的方法体已经复制到了调用者的区域。使用内联可以为程序带来很多好处,比如

  • 不会引起额外的性能损失
  • 减少指针的间接引用
  • 不需要对内联方法进行虚方法查找

另外,通过将方法的实现复制到调用者中,JIT 编译器处理的代码增多,使得后续的优化和更多的内联成为可能。

内联取决于方法的大小。缺省情况下,含有 35 个字节码或更少的方法可以进行内联操作。对于被频繁调用的方法,临界值可以达到 325 个字节。我们可以通过设置 -XX:MaxInlineSize=# 选项来修改最大的临界值,通过设置‑XX:FreqInlineSize=#选项来修改频繁调用的方法的临界值。但是在没有正确的分析的情况下,我们不应该修改这些配置。因为盲目地修改可能会对程序的性能带来不可预料的影响。

由于内联会对代码的性能有大幅提升,因此让尽可能多的方法达到内联条件尤为重要。这里我们介绍一款叫做 Jarscan 的工具来帮助我们检测程序中有多少方法是对内联友好的。

Jarscan 工具是分析 JIT 编译的 JITWatch 开源工具套件中的一部分。和在运行时分析 JIT 日志的主工具不同,Jarscan 是一款静态分析 jar 文件的工具。该工具的输出结果格式为 CSV,结果中包含了超过频繁调用方法临界值的方法等信息。JITWatch 和 Jarscan 是 AdoptOpenJDK 工程的一部分,该工程由 Chris Newland 领导。

在使用 Jarscan 并得到分析结果之前,需要从 AdoptOpenJDK Jenkins 网站下载二进制工具( Java 7 工具 Java 8 工具)。

运行很简单,如下所示

./jarScan.sh <span><<span>jars</span> <span>to</span> <span>analyse</span>></span>更多关于 Jarscan 的细节可以访问 AdoptOpenJDK wiki 进行了解。

上面产生的报告对于开发团队的开发工作很有帮助,根据报告结果,他们可以查找程序中是否包含了过大而不能 JIT 编译的关键路径方法。上面的操作依赖于手动执行。但是为了以后的自动化,可以开启 Java 的 -XX:+PrintCompilation 选项。开启这个选项会生成如下的日志信息:

复制代码
<span>37</span> <span>1</span> java<span>.</span>lang<span>.</span><span>String</span><span>::hashCode</span> (<span>67</span> <span>bytes</span>)
<span>124</span> <span>2</span> s<span>!</span> java<span>.</span>lang<span>.</span>ClassLoader<span>::loadClass</span> (<span>58</span> <span>bytes</span>)

其中,第一列表示从进程启动到 JIT 编译发生经过的时间,单位为毫秒。第二列表示的是编译 id,表明该方法正在被编译(在 Hotspot 中一个方法可以多次去优化和再优化)。第三列表示的是附加的一些标志信息,比如 s 代表 synchronized,!代表有异常处理。最后两列分别代表正在编译的方法名称和该方法的字节大小。

关于 PrintCompilation 输出的更多细节,Stephen Colebourne 写过一篇博客文章详细介绍日志结果中各列的具体含义,感兴趣的可以访问这里阅读。

PrintCompilation 的输出结果会提供运行时正在编译的方法的信息,Jarscan 工具的输出结果可以告诉我们哪些方法不能进行 JIT 编译。结合两者,我们就可以清楚地知道哪些方法进行了编译,哪些没有进行。另外,PrintCompilation 选项可以在线上环境使用,因为开启这个选项几乎不会影响 JIT 编译器的性能。

但是,PrintCompilation 也存在着两个小问题,有时候会显得不是那么方便:

  1. 输出的结果中未包含方法的签名,如果存在重载方法,区分起来则比较困难。
  2. Hotspot 虚拟机目前不能将结果输出到单独的文件中,目前只能是以标准输出的形式展示。

上述的第二个问题的影响在于 PrintCompilation 的日志会和其他常用的日志混在一起。对于大多数服务器端程序来说,我们需要一个过滤进程来将 PrintCompilation 的日志过滤到一个独立的日志中。最简单的判断一个方法否是 JIT 友好的途径就是遵循下面这个简单的步骤:

  1. 确定程序中位于要处理的关键路径上的方法。
  2. 检查这些方法没有出现在 Jarscan 的输出结果中。
  3. 检查这些方法确实出现在了 PrintCompilation 的输出结果中。

如果一个方法超过了内联的临界值,大多数情况下最常用的方法就是讲这个重要的方法拆分成多个可以进行内联的小方法,这样修改之后通常会获取更好的执行效率。但是对于所有的性能优化而言,优化之前的执行效率需要测量记录,并且需要需要同优化后的数据进行对比之后,才能决定是否进行优化。为了性能优化而做出的改变不应该是盲目的。

几乎所有的 Java 程序都依赖大量的提供关键功能的库。Jarscan 可以帮助我们检测哪些库或者框架的方法超过了内联的临界值。举一个具体的例子,我们这里检查 JVM 主要的运行时库 rt.jar 文件。

为了让结果有点意思,我们分别比较 Java 7 和 Java 8,并查看这个库的变化。在开始之前我们需要安装 Java 7 和 Java8 JDK。首先,我们分别运行 Jarscan 扫描各自的 rt.jar 文件,并得到用来后续分析的报告结果:

复制代码
$ ./jarScan<span>.sh</span> /Library/Java/JavaVirtualMachines/jdk1<span>.7</span><span>.0</span>_71<span>.jdk</span>/Contents/Home/jre/lib/rt<span>.jar</span>
> large_jre_methods_7u71<span>.txt</span>
$ ./jarScan<span>.sh</span> /Library/Java/JavaVirtualMachines/jdk1<span>.8</span><span>.0</span>_25<span>.jdk</span>/Contents/Home/jre/lib/rt<span>.jar</span>
> large_jre_methods_8u25<span>.txt</span>

上述操作结束之后,我们得到两个 CSV 文件,一个是 JDK 7u71 的结果,另一个是 JDK 8u25。然后我们看一看不同的版本内联情况有哪些变化。首先,一个最简单的判断验证方式,看一看不同版本的 JRE 中有多少对 JIT 不友好的方法。

复制代码
$ wc -l large_jre_methods_*
<span>3684</span> large_jre_methods_7u71<span>.txt</span>
<span>3576</span> large_jre_methods_8u25<span>.txt</span>

我们可以看到,相比 Java 7,Java 8 少了 100 多个内联不友好的方法。下面继续深入研究,看看一些关键的包的变化。为了便于理解如何操作,我们再次介绍一下 Jarscan 的输出结果。Jarscan 的输出结果有如下 3 个属性组成:

<span>"<package>"</span>,<span>"<method name and signature>"</span>,<<span>num</span> <span>of</span> <span>bytes</span>>了解了上述的格式,我们可以利用一些 Unix 文本处理的工具来研究报告结果。比如,我们想看一下 Java 7 和 Java 8 这两个版本中 java.lang 包下哪些方法变得内联友好了:

复制代码
$ cat large_jre_methods_7u71.txt large_jre_methods_8u25.txt <span>| grep -i</span>
^\<span>"java.lang | sort | uniq -c</span>

上面的语句使用 grep 命令过滤出每份报告中以 java.lang 开头的行,即只显示位于包 java.lang 中的类的内联不友好的方法。sort | uniq -c 是一个比较老的 Unix 小技巧,首先将讲行信息进行排序(相同的信息将聚集到一起),然后对上面的排序数据进行去重操作。另外本命令还会统计一个当前行信息重复的次数,这个数据位于每一行信息的最开始部分。让我们看一下上述命令的执行结果:

复制代码
$ cat large_jre_methods_7u71.txt large_jre_methods_8u25.txt | grep -i ^\<span>"java.lang | sort | uniq -c
2 "</span>java.lang.CharacterData00<span>","</span><span>int</span> getNumericValue(<span>int</span>)<span>",835
2 "</span>java.lang.CharacterData00<span>","</span><span>int</span> toLowerCase(<span>int</span>)<span>",1339
2 "</span>java.lang.CharacterData00<span>","</span><span>int</span> toUpperCase(<span>int</span>)<span>",1307
// ... skipped output
2 "</span>java.lang.invoke.DirectMethodHandle<span>","</span><span>private</span> <span>static</span> java.lang.invoke.LambdaForm makePreparedLambdaForm(java.lang.invoke.MethodType,<span>int</span>)<span>",613
1 "</span>java.lang.invoke.InnerClassLambdaMetafactory<span>","</span><span>private</span> java.lang.Class spinInnerClass()<span>",497
// ... more output ----</span>

报告中,以 2(这是使用了 uniq -c 对相同的信息计算数量的结果)最为起始的条目说明这些方法在 Java 7 和 Java 8 中起字节码大小没有改变。虽然这并不能完全肯定地说明这些方法的字节码没有改变,但通常我们也可以视为没有改变。重复次数为 1 的方法有如下的情况:

a) 方法的字节码已经改变。

b) 这些方法为新的方法。

我们看一下以 1 开始的行数据

复制代码
<span>1</span> <span>"java.lang.invoke.AbstractValidatingLambdaMetafactory"</span>,<span>"void</span>
validateMetafactoryArgs()<span>",864</span>
<span>1</span> <span>"java.lang.invoke.InnerClassLambdaMetafactory"</span>,<span>"private</span>
java.lang.Class spinInnerClass()<span>",497</span>
<span>1</span> <span>"java.lang.reflect.Executable"</span>,<span>"java.lang.String</span>
sharedToGenericString(int,boolean)<span>",329</span>

上面三个对内联不友好的方法全部来自 Java 8,因此这属于新方法的情况。前两个方法与 lamda 表达式实现相关,第三个方法和反射子系统中继承层级调整有关。在这里,这个改变就是在 Java 8 中引入了方法和构造器可以继承的通用基类。

最后,我们看一看 JDK 核心库一些令人惊讶的特性:

复制代码
$ grep -i ^\<span>"java.lang.String large_jre_methods_8u25.txt
"</span>java.lang.String<span>","</span><span>public</span> java.lang.String[] split(java.lang.String,<span>int</span>)<span>",326
"</span>java.lang.String<span>","</span><span>public</span> java.lang.String toLowerCase(java.util.Locale)<span>",431
"</span>java.lang.String<span>","</span><span>public</span> java.lang.String toUpperCase(java.util.Locale)<span>",439</span>

从上面的日志我们可以了解到,即使是 Java 8 中一些 java.lang.String 中一些关键的方法还是处于内联不友好的状态。尤其是 toLowerCase 和 toUpperCase 这两个方法居然过大而无法内联,着实让人感到奇怪。但是,这两个方法由于要处理 UTF-8 数据而不是简单的 ASCII 数据,进而增加了方法的复杂性和大小,因而超过了内联友好的临界值。

对于性能要求较高并且确定只处理 ASCII 数据的程序,通常我们需要实现一个自己的 StringUtils 类。该类中包含一些静态的方法来实现上述内联不友好的方法的功能,但这些静态方法既保持紧凑型又能到达内联的要求。

上述我们讨论的改进都是大部分基于静态分析。除此之外,使用强大的 JITWatch 工具可以帮助我们更好地优化。JITWatch 工具需要设置 -XX:+LogCompilation 选项开启日志打印。其打印出来的日志为 XML 格式,而非 PrintCompilation 简单的文本输出,并且这些日志比较大,通常会到达几百 MB。它会影响正在运行的程序(默认情况下主要来自日志输出的影响),因此这个选项不适合在线上的生产环境使用。

PrintCompilation 和 Jarscan 结合使用并不困难,但却提供了简单且很有实际作用的一步,尤其是对于开发团队打算研究其程序中即时编译执行情况时。大多数情况下,在性能优化中,一个快速的分析可以帮助我们完成一些容易实现的目标。

关于作者

Ben Evans 是 jClarity 公司的 CEO,jClarity 是一家致力于 Java 和 JVM 性能分析研究的创业公司。除此之外他还是 London Java Community 的负责人之一并在 Java Community Process Executive Committee 有一席之地。他之前的项目有 Google IPO 性能测试,金融交易系统,90 年代知名电影网站等。

查看英文原文: Is Your Java Application Hostile to JIT Compilation?


感谢张龙对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ @丁晓昀),微信(微信号: InfoQChina )关注我们,并与我们的编辑和其他读者朋友交流(欢迎加入 InfoQ 读者交流群)。

2015-09-05 06:057961

评论

发布
暂无评论
发现更多内容

pringBoot + Mybatis Plus + ClickHouse增删改查入门教程

程序员万金游

MySQL 数据库 mybatis Clickhouse

如何修复org/jacoco/agent/rt/internal_773e439/Offline异常?

Changing Lin

11月日更

工具 | 一条 SQL 实现 PostgreSQL 数据找回

RadonDB

数据库 postgresql RadonDB

Micro Focus Fortify 最新能力发布,应用安全进入“一切皆代码”新时代

干货必收藏!墨天轮最受DBA欢迎的250份学习文档合集

墨天轮

MySQL 数据库 oracle postgresql

多商户入驻系统开发多商户入驻app源码开发

风行无疆

论文解读丨无参数的注意力模块SimAm

华为云开发者联盟

卷积神经网络 视觉 注意力模块 SimAm 神经元

CSS布局(四)之文本布局

Augus

CSS 11月日更

24 K8S之Service资源及其模型

穿过生命散发芬芳

k8s 11月日更

秋招拿到offer之后,分享面试阿里、头条、腾讯的经过以及一些总结

Geek_1df311

Java 编程 程序员 架构 面试

TypeScript 之 Typeof Type Operator

冴羽

JavaScript typescript html5 翻译 大前端

掌握Java的内存模型,你就是解决并发问题最靓的仔

华为云开发者联盟

Java 内存模型 线程 并发 工作内存

Alibaba最新神作!耗时182天肝出来1015页分布式全栈手册太香了

热爱java的分享家

Java 面试 分布式 编程语言 经验分享

外卖跑腿系统专业开发外卖跑腿app源码开发

风行无疆

彻底搞懂访问者模式的静态、动态和伪动态分派

Tom弹架构

Java 架构 设计模式

分区丢失导致文件丢失?巧用EasyreCovery找回!

淋雨

数据恢复

帧彩视界之Pseudo HDR:为普通SDR设备用户提供HDR体验

微帧Visionular

视频编码 HDR

优秀!阿里甩出GC面试小册,仅7天Github获赞96.9K

热爱java的分享家

Java 架构 程序人生 编程语言 经验分享

Pulsar 分级存储 -- S3 演示

Apache Pulsar

大数据 架构 云原生 分布式存储 Apache Pulsar

会声会影编辑面板常用功能介绍

懒得勤快

趣谈哈希表优化:从规避 Hash 冲突到利⽤ Hash 冲突

热爱java的分享家

Java 架构 编程语言 经验分享 哈希表

行业分析| 为什么uni-app在音视频领域这么火?

anyRTC开发者

uni-app 音视频 移动开发 语音通话 视频通话

在牛客逮到一个阿里10年老Java开发,聊过之后收益良多...

Geek_1df311

Java 编程 架构 面试 计算机

Function Mesh:Serverless 在消息与流数据场景下的火花

Apache Pulsar

大数据 架构 云原生 Apache Pulsar pulsar社区

又一次漏洞复现与分析

网络安全学海

网络安全 信息安全 渗透测试 WEB安全 漏洞挖掘

艾媒报告:百度输入法AI功能领跑行业 用户满意度第一

百度开发者中心

百度 AI

你的web知识有所欠缺。

喀拉峻

Web 安全

元气部落盲盒软件原生开发芒趣盲盒app源码

风行无疆

为何每次用完ThreadLocal都要调用remove()?

热爱java的分享家

Java 程序人生 编程语言 经验分享

Go语言学习查缺补漏ing Day6

Regan Yue

Go 语言 11月日更

DAS易用性设计

赫杰辉

你的Java代码对JIT编译友好么?_Java_Ben Evans_InfoQ精选文章