你的Java代码对JIT编译友好么?
作者:网络转载 发布时间:[ 2015/9/7 11:10:14 ] 推荐标签:编程语言 测试开发技术
关于PrintCompilation输出的更多细节,Stephen Colebourne写过一篇博客文章详细介绍日志结果中各列的具体含义,感兴趣的可以访问这里阅读。
PrintCompilation的输出结果会提供运行时正在编译的方法的信息,Jarscan工具的输出结果可以告诉我们哪些方法不能进行JIT编译。结合两者,我们可以清楚地知道哪些方法进行了编译,哪些没有进行。另外,PrintCompilation选项可以在线上环境使用,因为开启这个选项几乎不会影响JIT编译器的性能。
但是,PrintCompilation也存在着两个小问题,有时候会显得不是那么方便:
输出的结果中未包含方法的签名,如果存在重载方法,区分起来则比较困难。
Hotspot虚拟机目前不能将结果输出到单独的文件中,目前只能是以标准输出的形式展示。
上述的第二个问题的影响在于PrintCompilation的日志会和其他常用的日志混在一起。对于大多数服务器端程序来说,我们需要一个过滤进程来将PrintCompilation的日志过滤到一个独立的日志中。简单的判断一个方法否是JIT友好的途径是遵循下面这个简单的步骤:
确定程序中位于要处理的关键路径上的方法。
检查这些方法没有出现在Jarscan的输出结果中。
检查这些方法确实出现在了PrintCompilation的输出结果中。
如果一个方法超过了内联的临界值,大多数情况下常用的方法是讲这个重要的方法拆分成多个可以进行内联的小方法,这样修改之后通常会获取更好的执行效率。但是对于所有的性能优化而言,优化之前的执行效率需要测量记录,并且需要需要同优化后的数据进行对比之后,才能决定是否进行优化。为了性能优化而做出的改变不应该是盲目的。
几乎所有的Java程序都依赖大量的提供关键功能的库。Jarscan可以帮助我们检测哪些库或者框架的方法超过了内联的临界值。举一个具体的例子,我们这里检查JVM主要的运行时库 rt.jar文件。
为了让结果有点意思,我们分别比较Java 7 和Java 8,并查看这个库的变化。在开始之前我们需要安装Java 7 和 Java8 JDK。首先,我们分别运行Jarscan扫描各自的rt.jar文件,并得到用来后续分析的报告结果:
$ ./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
上述操作结束之后,我们得到两个CSV文件,一个是JDK 7u71的结果,另一个是JDK 8u25。然后我们看一看不同的版本内联情况有哪些变化。首先,一个简单的判断验证方式,看一看不同版本的JRE中有多少对JIT不友好的方法。
$ wc -l large_jre_methods_*
3684 large_jre_methods_7u71.txt
3576 large_jre_methods_8u25.txt
我们可以看到,相比Java 7,Java 8 少了100多个内联不友好的方法。下面继续深入研究,看看一些关键的包的变化。为了便于理解如何操作,我们再次介绍一下Jarscan的输出结果。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中的类的内联不友好的方法。sort | uniq -c 是一个比较老的Unix小技巧,首先将讲行信息进行排序(相同的信息将聚集到一起),然后对上面的排序数据进行去重操作。另外本命令还会统计一个当前行信息重复的次数,这个数据位于每一行信息的开始部分。让我们看一下上述命令的执行结果:
$ 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的方法有如下的情况:
a)方法的字节码已经改变。
b)这些方法为新的方法。
我们看一下以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
上面三个对内联不友好的方法全部来自Java 8,因此这属于新方法的情况。前两个方法与lamda表达式实现相关,第三个方法和反射子系统中继承层级调整有关。在这里,这个改变是在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工具可以帮助我们更好地优化。JITWatch工具需要设置-XX:+LogCompilation选项开启日志打印。其打印出来的日志为XML格式,而非PrintCompilation简单的文本输出,并且这些日志比较大,通常会到达几百MB。它会影响正在运行的程序(默认情况下主要来自日志输出的影响),因此这个选项不适合在线上的生产环境使用。
PrintCompilation和Jarscan结合使用并不困难,但却提供了简单且很有实际作用的一步,尤其是对于开发团队打算研究其程序中即时编译执行情况时。大多数情况下,在性能优化中,一个快速的分析可以帮助我们完成一些容易实现的目标。
本文内容不用于商业目的,如涉及知识产权问题,请权利人联系SPASVO小编(021-61079698-8054),我们将立即处理,马上删除。
相关推荐
Java性能测试有哪些不为众人所知的原则?Java设计模式??装饰者模式谈谈Java中遍历Map的几种方法Java Web入门必知你需要理解的Java反射机制知识总结编写更好的Java单元测试的7个技巧编程常用的几种时间戳转换(java .net 数据库)适合Java开发者学习的Python入门教程Java webdriver如何获取浏览器新窗口中的元素?Java重写与重载(区别与用途)Java变量的分类与初始化JavaScript有这几种测试分类Java有哪四个核心技术?给 Java开发者的10个大数据工具和框架Java中几个常用设计模式汇总java生态圈常用技术框架、开源中间件,系统架构及经典案例等
更新发布
功能测试和接口测试的区别
2023/3/23 14:23:39如何写好测试用例文档
2023/3/22 16:17:39常用的选择回归测试的方式有哪些?
2022/6/14 16:14:27测试流程中需要重点把关几个过程?
2021/10/18 15:37:44性能测试的七种方法
2021/9/17 15:19:29全链路压测优化思路
2021/9/14 15:42:25性能测试流程浅谈
2021/5/28 17:25:47常见的APP性能测试指标
2021/5/8 17:01:11热门文章
常见的移动App Bug??崩溃的测试用例设计如何用Jmeter做压力测试QC使用说明APP压力测试入门教程移动app测试中的主要问题jenkins+testng+ant+webdriver持续集成测试使用JMeter进行HTTP负载测试Selenium 2.0 WebDriver 使用指南