java实现jit即时编译_您的Java应用程序是否对JIT编译有害?

java实现jit即时编译

即时(JIT)编译器是Java虚拟机最有效和重要的部分之一。 但是,许多应用程序并未充分利用JIT的高性能。 许多开发人员甚至都不知道他们的应用程序使用JIT的效率如何。

在本文中,我们将提供一些简单技术的新手入门指南,您可以使用这些简单技术来确定应用程序可能存在的问题,这些问题会使它对JIT不友好。 我们将不介绍有关JIT编译工作原理的全部详细信息,仅介绍一些基本检查和改进代码的方法,这些方法可以提供一种轻松的方法来使您的应用程序对JIT友好。

关于JIT编译的关键点是Hotspot自动监视解释器正在执行的方法。 一旦足够频繁地调用一种方法,就将其标记为可编译为机器代码。 这些“热方法”由JVM线程在后台编译。 在此编译完成之前,JVM会一直运行-使用该方法的原始解释版本。 只有在方法完全编译后,热点才会修补方法分派表以指向新表单。

热点针对JIT编译有大量不同的优化技术-但对于我们而言,最重要的一项是内联。 通过有效地将方法主体提升到调用者的作用域中,这是删除虚拟方法调用的过程。 例如,考虑这段代码:

public int add(int x, int y) {
    return x + y;
  }
  
  int result = add(a, b);

发生内联时,此代码将有效地转换为:

int result = a + b;

值a和b已代替方法参数,并且构成方法主体的代码已复制到调用方的作用域中。 内联对于程序员有很多好处。 例如:

  • 良好的编码习惯不会降低性能
  • 减少指针间接
  • 消除虚拟方法查找

此外,由于JIT编译器现在可以处理更多的代码,因此通过查看更多代码,内联打开了进一步优化和进行更多内联的可能性。

内联取决于方法的大小。 默认情况下,包含35个或更少字节字节码的方法是适用的。 对于经常被调用的“热”方法,此阈值增加到325个字节。 可以使用-XX:MaxInlineSize =#来控制主阈值(使用‑XX:FreqInlineSize =#可以控制热阈值),但是如果没有进行适当的分析,则不应更改这些阈值,因为盲目更改它可能会对性能产生意外影响的应用程序。

由于内联会对代码性能产生巨大影响,因此重要的是,应有尽可能多的方法可以进行内联。 让我们使用一个名为Jarscan的工具来检查有多少种方法是行内友好的。

Jarscan工具是用于分析JIT编译的JITWatch开源工具套件的一部分。 与主要工具分析从运行中的应用程序生成的JIT日志不同,Jarscan是可以使用jar文件的静态分析工具。 它会生成CSV报告,说明哪些方法超过了“热”阈值。 JITWatch和Jarscan是AdoptOpenJDK项目的一部分,由项目负责人Chris Newland编写。

要使用该工具并生成大型方法报告,请从AdoptOpenJDK Jenkins站点下载一个二进制文件( Java 7二进制文件Java 8二进制文件 )。

然后可以通过以下方式简单运行:

./jarScan.sh<jars to analyse> 

有关Jarscan的更多详细信息,可以在AdoptOpenJDK Wiki上找到。

大型方法报告可能非常有用,开发团队可以使用它来检查其应用程序中是否没有对于JIT而言太大的关键路径方法。 但是,这仍然是手动过程。 为了进一步使其自动化,我们可以使用-XX:+ PrintCompilation开关。 这将生成如下日志行:

37    1      java.lang.String::hashCode (67 bytes)
124   2  s!  java.lang.ClassLoader::loadClass  (58 bytes)

第一列显示自JIT编译发生以来,该过程开始以来的时间(以毫秒为单位)。 下一列是编译ID,它指示正在编译的方法(Hotspot可以对方法进行多次优化和重新编译)。 接下来,输出以标志的形式显示其他信息(例如s表示同步,而!表示“具有异常处理程序”)。 最后两列显示了正在编译的方法的名称和大小(以字节码的字节数为单位)。

有关PrintCompilation输出的更多详细信息,Stephen Colebourne撰写了一篇博客文章 ,其中详细介绍了输出中出现的各个字段的确切含义。

PrintCompilation输出提供有关在给定运行中实际编译的方法的有用的动态信息,可以将其与Jarscan提供的静态分析结合使用,以清晰地了解正在编译和未编译的内容。 PrintCompilation可以留在生产中,因为它不会对JIT编译器的性能产生有意义的影响。

但是,PrintCompilation有两个小麻烦之处,使它的实用性降低了:

  1. 方法的签名未在输出中打印出来,因此很难区分重载的方法
  2. Hotspot当前不提供将PrintCompilation的输出重定向到单独文件的方法。 就目前而言,PrintCompilation的输出只能发送到stdout。

第二个问题是编译输出最终与常规应用程序输出混合在一起。 对于大多数服务器应用程序,这需要进行过滤过程,以将编译输出分离到单独的日志中。 确定方法是否对JIT友好的最简单方法是遵循一个简单的过程:

  1. 确定交易关键路径上的应用程序方法
  2. 检查这些方法是否未出现在Jarscan输出中
  3. 检查方法是否确实出现在PrintCompilation输出中

如果方法高于内联阈值,则在大多数情况下,标准方法是将重要方法拆分为较小的部分以进行内联。 通常,这会产生更好的性能,但是与所有性能优化一样,应该对原始系统进行测量,并与修改后的版本进行比较。 性能驱动的更改绝不能盲目地应用。

几乎所有Java应用程序都依赖一堆库来提供它们所依赖的关键功能。 Jarscan还可以通过报告哪些库或框架方法超出热内联阈值来帮助开发人员。 作为一个极端的例子,让我们检查rt.jar-JVM本身的主要运行时库。

为了使它更加有趣,让我们并排比较Java 7和Java 8,看看情况如何变化。 假设我们同时安装了Java 7和Java 8 JDK。 首先,让我们在各自的rt.jar文件上运行Jarscan并生成报告,将其保存以进行进一步分析:

$ ./jarScan.sh /Library/Java/JavaVirtualMachines/jdk1.7.0_71.jdk/Contents/Home/jre/lib/rt.jar
  > large_jre_methods_7u71.txt
    $ ./jarScan.sh /Library/Java/JavaVirtualMachines/jdk1.8.0_25.jdk/Contents/Home/jre/lib/rt.jar
  > large_jre_methods_8u25.txt

现在我们有2个CSV文件,一个用于7u71,一个用于8u25。 让我们做一些比较,看看不同版本之间的内联行为如何变化。 首先,一个非常简单的指标-每个JRE中有多少种方法对内联不友好?

$ wc -l large_jre_methods_*
    3684 large_jre_methods_7u71.txt
    3576 large_jre_methods_8u25.txt

我们可以看到,与Java 7相比,Java 8内联不友好的方法减少了100多个。让我们更深入地研究一下某些关键软件包的稳定性(或其他方面)。 要了解如何执行此操作,让我们回顾一下Jarscan输出的报告格式。 该报告包含3个字段:

"<package>","<method name and signature>",<num of bytes>

由此,我们可以使用简单的Unix文本处理工具来调查报告。 例如,让我们看看Java 7和Java 8之间在java.lang中对内联友好方法进行了哪些更改:

$ cat large_jre_methods_7u71.txt large_jre_methods_8u25.txt | grep -i
  ^\"java.lang | sort | uniq -c

这将使用grep命令从任一报告中仅返回以“ java.lang开头”的行,基本上将结果限制为java.lang包中任何类中存在的任何行内不友好方法。 uniq -c“子句是一种古老的Unix技巧-它的基本含义是:对各行进行排序(以便所有相同的行彼此相邻),然后对它们进行重复数据删除,但要保持计数(在行首插入)每行有很多副本,让我们看一下应用于Jarscan结果的实际输出:

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

以2开头的条目(请记住,这是“ uniq -c”报告的相同行数)表示在Java 7和Java 8之间,这些方法的字节码大小完全没有变化。 当然,这不像知道字节码完全相同那样有力的保证,但是仍然是不错的稳定性指标。 行计数为1的方法表明:

让我们看看我们的报告有什么:

1 "java.lang.invoke.AbstractValidatingLambdaMetafactory","void
validateMetafactoryArgs()",864
   1 "java.lang.invoke.InnerClassLambdaMetafactory","private
java.lang.Class
   
    spinInnerClass()",497
    1 "java.lang.reflect.Executable","java.lang.String
    sharedToGenericString(int,boolean)",329

这3种内联不友好的方法都来自Java 8,因此它们是新方法。 前两个与lambda表达式的实现有关,第三个与反射子系统中继承层次的调整有关。 在这种情况下,更改是引入了一个通用基类,该方法和构造函数都在Java 8中继承。

最后,让我们看一下核心JDK库的令人惊讶的功能:

$ grep -i ^\"java.lang.String large_jre_methods_8u25.txt
  "java.lang.String","public java.lang.String[] split(java.lang.String,int)",326
  "java.lang.String","public java.lang.String toLowerCase(java.util.Locale)",431
  "java.lang.String","public java.lang.String toUpperCase(java.util.Locale)",439

这告诉我们,即使在Java 8中,java.lang.String的几个关键方法仍然是内联不友好的。 特别是,toLowerCase()和toUpperCase()都太大而无法内联,这似乎很奇怪。 但是,由于这些方法必须处理通用的UTF-8数据而不是ASCII,因此确实增加了方法的大小和复杂性,并且不幸的是,它提示其超出了对行内友好的阈值。

对于知道仅限于ASCII数据的性能非常高的应用程序,很常见的是实现专有的StringUtils类,该类包含静态方法,这些方法的工作与所提到的内联非友好方法相似,但保留了紧凑性,并且内联能力。

我们讨论的改进主要集中在静态分析上。 通过使用主要JITWatch工具的全部功能,我们可以做得更好。 这需要使用-XX:+ LogCompilation标志生成的编译日志。 这些日志是基于XML的(而不是PrintCompilation的简单文本输出),并且它们很大,通常达到数百MB。 它们还会影响正在运行的应用程序(如果没有其他影响,则将影响写出日志),因此该开关通常不适合在生产环境中使用。

PrintCompilation和Jarscan的结合并不复杂,但是它可以为刚刚开始了解其应用程序的JIT行为的应用程序团队提供有用的第一步。 在许多情况下,就改进的应用程序性能而言,快速分析也将获得一些令人垂涎的成果。

翻译自: https://www.infoq.com/articles/Java-Application-Hostile-to-JIT-Compilation/?topicPageSponsorship=c1246725-b0a7-43a6-9ef9-68102c8d48e1

java实现jit即时编译

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值