写点什么

你的 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:058048

评论

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

2022Q4消费级AR眼镜市场季度分析:雷鸟创新领跑,华为强势入局

易观分析

市场 消费 AR眼镜

什么是AutodeskMaya?为什么要学习它?

Finovy Cloud

3DMAX maya

网易伏羲预训练模型”玉言“登顶CLUE分类榜单,成绩首次超过人类水平

网易伏羲

人工智能

使用 YonBuilder 进行报表分析 - 扩展篇

YonBuilder低代码开发平台

还在用Excel和SQL?火山引擎VeDI这款产品帮你更快处理数据

字节跳动数据平台

大数据 数据分析 企业 数据看板

英特尔CEO帕特·基辛格:五大“超级技术力量”,推动人类社会发展

科技之家

叮咚~,这份春节前突击检查应对指南请收好!

嘉为蓝鲸

自动化运维 weops 嘉为蓝鲸

MASA Stack 1.0 发布会圆满收官

MASA技术团队

.net 云原生 PaaS dapr MASA

CuPL 利用大规模的语言模型,更高效地生成提示

Zilliz

【案例分享】如何利用京东云建设高可用业务架构

京东科技开发者

云计算 架构 高可用架构 后端、 企业号 1 月 PK 榜

标准升级 |《企业数字化成熟度模型IOMM标准》(企业整体视角)发布

信通院IOMM数字化转型团队

数字化转型 IOMM ICT深度观察

Svelte框架实现表格协同文档

葡萄城技术团队

聚焦技术与体验极致提升,阿里云视频云连续5年领跑!

阿里云CloudImagine

阿里云 IDC 视频云

昆仑万维深耕AIGC领域 昆仑天工助力内容创作者创造无限可能

Geek_2d6073

​洞悉获客之道,林肯汽车开展高端社区精准营销俘获消费者芳心

联营汇聚

如何通过C#和VB.NET合并Excel文档

Geek_249eec

C# Excel VB.NET

WeOps上新啦 | WeOpsV3.14拓展云平台能力,支持自动发现和监控告警

嘉为蓝鲸

自动化运维 weops 嘉为蓝鲸

位运算在数据库中的实际应用

领创集团Advance Intelligence Group

数据库 位计算

深度 | 新兴软件研发范式崛起,云计算全面走向 Serverless 化

阿里巴巴云原生

阿里云 Serverless 云原生

免费下载 | 2023 中国技术成熟度评估曲线发布,共看六大发展趋势

博睿数据

可观测性 智能运维 博睿数据 权威报告

21世纪啤酒与尿布的故事

Marvin Ma

广告 流媒体 啤酒与尿布

KubeVela 再升级:交付管理一体化的云原生应用平台

阿里巴巴云原生

阿里云 开源 云原生 KubeVela

阿里云云边一体容器架构创新论文被云计算顶会 ACM SoCC 录用

阿里巴巴云原生

阿里云 容器 云原生

实力领跑 | 旺链科技入选《2022中国区块链技术创新典型企业名录》

旺链科技

区块链 区块链技术 产业区块链

安卓影像飞升时刻:vivo X90 Pro+打通HDR任督二脉

脑极体

Vivo 蔡司影像

如何利用极狐GitLab 轻松管理NPM依赖发布与更新?

极狐GitLab

node.js DevOps npm 依赖 极狐GitLab

嘉为蓝鲸研运一体化解决方案荣获信通院XOps领域年度明星解决方案

嘉为蓝鲸

自动化运维 嘉为蓝鲸

揭开华为云CodeArts TestPlan启发式测试设计神秘面纱!

华为云开发者联盟

云计算 后端 华为云 企业号 1 月 PK 榜

SQL 嵌套 N 层太长太难写怎么办?

王磊

如何对小程序进行更高效的管理

Onegun

小程序 微信小程序 小程序管理平台

干货 | 企业监控系统体系化建设思路

嘉为蓝鲸

自动化运维 嘉为蓝鲸 企业监控系统

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