Java程序最初是通过解释器进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁时,就会把这些代码认定为“热点代码”(Hot Spot Code)。
1.即时编译器(JIT编译器)
为了提高热点代码的执行效率,在运行时,虚拟机会把这些热点代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器就是即时编译器(JIT编译器)。
在介绍即时编译器之前先来提出几个问题,然后带着这些问题,寻找文中的答案。
为何HotSpot要使用解释器与编译器并存的架构?为何HotSpot要实现两个不同的即时编译器?程序什么时候使用解释器执行,什么时候使用编译器执行?哪些程序代码会被编译为本地代码?如何编译为本地代码?如何从外部观察即时编译器的编译过程和编译结果?
1.1解释器与编译器
1.1.1编译器并存架构
并不是所有的Java虚拟机都采用解释器和编译器并存的架构,但是很多的主流的商用虚拟机(HotSpot,J9等),都同时包含了解释器和编译器。
为什么要采用这种并存的架构呢?其实啊,解释器和编译器他们是各有优势:当程序需要快速启动和执行的时候,解释器可以首先发挥作用,省去编译的时间,立即执行。然而随着程序的运行,编译器就会逐渐发挥作用,把越来越多的代码编译成本地机器码,用来提高程序的执行效率。
不过,在整个虚拟机执行过程中,解释器与编译器经常配合工作。
1.1.2两个即时编译器
HotSpot虚拟机内置了两个即时编译器,分别是Client Compiler(C1编译器)和Server Compiler(C2编译器),目前主流的HotSpot虚拟机中,默认采用解释器与其中的一个编译器直接配合的方式工作,具体使用哪一个编译器,取决于虚拟机的运行模式。不过,用户可以使用“-client”,“-server”参数去强制指定虚拟机运行在Client模式或者Server模式。
不管采用哪一个编译器,这种搭配使用的工作模式在虚拟机中成为“混合模式”(Mixed Mode)。
$ java -versionjava version "1.8.0_201"Java(TM) SE Runtime Environment (build 1.8.0_201-b09)Java HotSpot(TM) 64-Bit Server VM (build 25.201-b09, mixed mode)
即时编译器编译本地代码需要占用程序执行时间,为了编译更高效优化程度更高的代码,那么花费时间可能更长,解释器还要给人家收集性能监控信息,这对解释器的执行速度也有一定的影响。
1.1.3分层编译
那么有了影响怎么办?不用担心,总是有办法的。
为了在程序启动速度与运行效率之间达到最佳平衡,HotSpot虚拟机采用了“分层编译”的策略,不过是在JDK1.7后的Server模式虚拟机中作为默认编译策略被开启。那么分层编译时怎么个分层法呢,有什么依据条件呢?
分层编译根据编译器编译,优化的规模与耗时进行划分的。其中包含了以下几个编译层次:
第0层,程序解释执行,解释器不开启性能监控功能,可触发第1层编译。第1层,也称C1编译,将字节码编译为本地机器码,进行简单,可靠的优化,如有必要还会加入性能监控逻辑。第2层(或2层以上),也是将字节码编译为本地机器码,但是会启用一些编译耗时较长的优化,甚至会根据性能监控信息进行一些不可靠的激进优化。
实施分层编译后,C1和C2编译器会同时工作,许多代码可能被编译多次,用C1编译器能获取更高的编译速度,用C2编译器能获得更好的编译质量。
1.2编译对象与触发条件
1.2.1热点代码
对于“热点代码”,上面已经有所提及,也就是被JIT编译的代码就是“热点代码”,那么就有这么两个问题值得思考。
什么样的代码被认为是热点代码?如何检测这些热点代码?
先回答一下第一个问题:被即时编译器编译的“热点代码”有两类,也就是:
被多次调用的方法被多次执行的循环体
对于被多次调用的方法,这个非常好理解,也就是方法调用次数多了,那么方法体内的代码执行次数也就多了。对于被多次执行的循环体,这个怎么理解呢,因为方法可能调用一次或者少量的几次,但是方法体内的循环体执行次数非常多,这也被认为是“热点代码”。
1.2.2热点探测
我们说被执行多次调用的方法或者被多次执行的循环体,被称为“热点代码”,那么这个“多次”到底是多少次,并不是一个精确的数字,那么如何统计这个次数?回答了这个问题,也就回答了即时编译的触发条件。
判断一段代码是不水热点代码,是不是需要触发即时编译,这样的行为称作:热点探测(Hot Spot Detection)。目前主要的热点探测判定方式有两种:
基于采样的热点探测基于计数器的热点探测
在HotSpot虚拟机中,采用的是第二中判定方式:基于计数器的热点探测。它为每个方法设置了两类计数器:方法调用计数器和回边计数器。在确定虚拟机运行参数的情况下,这两个计数器都有确定的阈值,当计数器超过阈值溢出了,就会触发JIT编译。
方法调用计数器
这个很好理解,就是统计方法被调用的次数,在Client模式下,默认值是1500次,在Server模式下是10000次,这个阈值可以通过虚拟机参数-XX: CompileThreshold
来认为设置。接下来看看整个JIT编译的交互流程图。
回边计数器
这个计数器的作用是统计一个方法中循环体代码的执行次数,在字节码中遇到控制流向后跳转的指令成为“回边”。回边计数器统计的目的就是为了触发OSR编译。
OSR(栈上替换): 一旦判定代码段是热点代码,则解释器将发送一次请求编译器,进行编译,在编译成功之前 解释器仍旧运行着。 等编译完成后,直接将pc寄存器中方法的调用地址进行替换,替换为编译后的方法地址。
目前回边计数器的虚拟机参数-XX:BackEdgeThreshold并未使用起来,需要通过-XX:OnStackReplacePercentage来间接调整回边计数器的阈值。关于回边计数器的阈值计算,暂不做了解。看看回边计时器触发即时编译的流程图。
1.2.3编译过程
默认情况下,无论是方法调用产生的即时编译请求,还是OSR编译请求,虚拟机在代码编译未完成之前,都是按照解释方式继续执行,而编译动作是交给后台的编译线程进行。用户可以通过设置虚拟机参数-XX:-BackgroundCompilation
来进行后台编译。禁止后台编译以后,一旦触发了JIT编译条件,执行线程向虚拟机提交编译请求后将会一直等待,直到编译完成后再开始执行编译器输出的本地代码。
主要来看看C1编译的大致过程。
对于Server Compiler编译的速度无疑是比较缓慢的,但是依然要超过传统的静态优化编译的速度,但是相比C1编译器来说,编译代码的质量更高。
1.3查看及分析即时编译结果
先来看段测试代码,代码内容比较简单,具体如下:
public class CheckCompileResult {public static void main(String[] args) {for (int i = 0; i < 15000; i++){calcSum();}}public static int doubleValue(int i){for (int j = 0; j < 100000; j++);return i*2;}public static long calcSum(){long sum = 0;for (int i = 0; i <= 100; i++){sum += doubleValue(i);}return sum;}}
虚拟机运行参数:-XX:+PrintCompilation
,这个参数要求虚拟机在即时编译的时候将被编译成的本地代码的方法名称打印出来,通过运行上面程序,可以确认这段代码是否触发了即时编译。打印结果如下:
/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/bin/java -XX:+PrintCompilation "-javaagent:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar=55847:/Applications/IntelliJ IDEA.app/Contents/bin" -Dfile.encoding=UTF-8 -classpath /Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/charsets.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/deploy.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/ext/cldrdata.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/ext/dnsns.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/ext/jaccess.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/ext/jfxrt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/ext/localedata.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/ext/nashorn.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/ext/sunec.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/ext/sunjce_provider.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/ext/sunpkcs11.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/ext/zipfs.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/javaws.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/jce.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/jfr.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/jfxswt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/jsse.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/management-agent.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/plugin.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/resources.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/rt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/lib/ant-javafx.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/lib/dt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/lib/javafx-mx.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/lib/jconsole.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/lib/packager.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/lib/sa-jdi.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/lib/tools.jar:/Users/fengguoqiang/myproject/xihu/target/classes:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/org/springframework/boot/spring-boot-starter-web/2.2.4.RELEASE/spring-boot-starter-web-2.2.4.RELEASE.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/org/springframework/boot/spring-boot-starter/2.2.4.RELEASE/spring-boot-starter-2.2.4.RELEASE.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/org/springframework/boot/spring-boot/2.2.4.RELEASE/spring-boot-2.2.4.RELEASE.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/org/springframework/boot/spring-boot-autoconfigure/2.2.4.RELEASE/spring-boot-autoconfigure-2.2.4.RELEASE.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/org/springframework/boot/spring-boot-starter-logging/2.2.4.RELEASE/spring-boot-starter-logging-2.2.4.RELEASE.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/ch/qos/logback/logback-classic/1.2.3/logback-classic-1.2.3.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/ch/qos/logback/logback-core/1.2.3/logback-core-1.2.3.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/org/apache/logging/log4j/log4j-to-slf4j/2.12.1/log4j-to-slf4j-2.12.1.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/org/apache/logging/log4j/log4j-api/2.12.1/log4j-api-2.12.1.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/org/slf4j/jul-to-slf4j/1.7.30/jul-to-slf4j-1.7.30.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/jakarta/annotation/jakarta.annotation-api/1.3.5/jakarta.annotation-api-1.3.5.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/org/yaml/snakeyaml/1.25/snakeyaml-1.25.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/org/springframework/boot/spring-boot-starter-json/2.2.4.RELEASE/spring-boot-starter-json-2.2.4.RELEASE.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/com/fasterxml/jackson/datatype/jackson-datatype-jdk8/2.10.2/jackson-datatype-jdk8-2.10.2.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/com/fasterxml/jackson/datatype/jackson-datatype-jsr310/2.10.2/jackson-datatype-jsr310-2.10.2.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/com/fasterxml/jackson/module/jackson-module-parameter-names/2.10.2/jackson-module-parameter-names-2.10.2.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/org/springframework/boot/spring-boot-starter-tomcat/2.2.4.RELEASE/spring-boot-starter-tomcat-2.2.4.RELEASE.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/org/apache/tomcat/embed/tomcat-embed-core/9.0.30/tomcat-embed-core-9.0.30.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/org/apache/tomcat/embed/tomcat-embed-el/9.0.30/tomcat-embed-el-9.0.30.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/org/apache/tomcat/embed/tomcat-embed-websocket/9.0.30/tomcat-embed-websocket-9.0.30.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/org/springframework/boot/spring-boot-starter-validation/2.2.4.RELEASE/spring-boot-starter-validation-2.2.4.RELEASE.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/jakarta/validation/jakarta.validation-api/2.0.2/jakarta.validation-api-2.0.2.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/org/hibernate/validator/hibernate-validator/6.0.18.Final/hibernate-validator-6.0.18.Final.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/org/jboss/logging/jboss-logging/3.4.1.Final/jboss-logging-3.4.1.Final.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/com/fasterxml/classmate/1.5.1/classmate-1.5.1.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/org/springframework/spring-web/5.2.3.RELEASE/spring-web-5.2.3.RELEASE.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/org/springframework/spring-beans/5.2.3.RELEASE/spring-beans-5.2.3.RELEASE.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/org/springframework/spring-webmvc/5.2.3.RELEASE/spring-webmvc-5.2.3.RELEASE.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/org/springframework/spring-aop/5.2.3.RELEASE/spring-aop-5.2.3.RELEASE.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/org/springframework/spring-context/5.2.3.RELEASE/spring-context-5.2.3.RELEASE.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/org/springframework/spring-expression/5.2.3.RELEASE/spring-expression-5.2.3.RELEASE.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/org/redisson/redisson/3.6.5/redisson-3.6.5.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/io/netty/netty-common/4.1.45.Final/netty-common-4.1.45.Final.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/io/netty/netty-codec/4.1.45.Final/netty-codec-4.1.45.Final.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/io/netty/netty-buffer/4.1.45.Final/netty-buffer-4.1.45.Final.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/io/netty/netty-transport/4.1.45.Final/netty-transport-4.1.45.Final.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/io/netty/netty-resolver/4.1.45.Final/netty-resolver-4.1.45.Final.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/io/netty/netty-resolver-dns/4.1.45.Final/netty-resolver-dns-4.1.45.Final.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/io/netty/netty-codec-dns/4.1.45.Final/netty-codec-dns-4.1.45.Final.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/io/netty/netty-handler/4.1.45.Final/netty-handler-4.1.45.Final.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/javax/cache/cache-api/1.1.1/cache-api-1.1.1.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/io/projectreactor/reactor-core/3.3.2.RELEASE/reactor-core-3.3.2.RELEASE.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/org/reactivestreams/reactive-streams/1.0.3/reactive-streams-1.0.3.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/org/slf4j/slf4j-api/1.7.30/slf4j-api-1.7.30.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/com/fasterxml/jackson/dataformat/jackson-dataformat-yaml/2.10.2/jackson-dataformat-yaml-2.10.2.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/com/fasterxml/jackson/core/jackson-core/2.10.2/jackson-core-2.10.2.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/com/fasterxml/jackson/core/jackson-databind/2.10.2/jackson-databind-2.10.2.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/com/fasterxml/jackson/core/jackson-annotations/2.10.2/jackson-annotations-2.10.2.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/net/bytebuddy/byte-buddy/1.10.6/byte-buddy-1.10.6.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/org/jodd/jodd-bean/3.7.1/jodd-bean-3.7.1.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/org/jodd/jodd-core/3.7.1/jodd-core-3.7.1.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/org/springframework/boot/spring-boot-starter-redis/1.2.5.RELEASE/spring-boot-starter-redis-1.2.5.RELEASE.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/org/springframework/spring-context-support/5.2.3.RELEASE/spring-context-support-5.2.3.RELEASE.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/org/springframework/spring-core/5.2.3.RELEASE/spring-core-5.2.3.RELEASE.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/org/springframework/spring-jcl/5.2.3.RELEASE/spring-jcl-5.2.3.RELEASE.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/org/springframework/spring-tx/5.2.3.RELEASE/spring-tx-5.2.3.RELEASE.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/org/springframework/data/spring-data-redis/2.2.4.RELEASE/spring-data-redis-2.2.4.RELEASE.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/org/springframework/data/spring-data-keyvalue/2.2.4.RELEASE/spring-data-keyvalue-2.2.4.RELEASE.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/org/springframework/data/spring-data-commons/2.2.4.RELEASE/spring-data-commons-2.2.4.RELEASE.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/org/springframework/spring-oxm/5.2.3.RELEASE/spring-oxm-5.2.3.RELEASE.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/redis/clients/jedis/3.1.0/jedis-3.1.0.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/org/apache/commons/commons-pool2/2.7.0/commons-pool2-2.7.0.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/io/netty/netty-all/4.1.6.Final/netty-all-4.1.6.Final.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/org/apache/commons/commons-lang3/3.9/commons-lang3-3.9.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/cglib/cglib/3.2.4/cglib-3.2.4.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/org/ow2/asm/asm/5.1/asm-5.1.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/org/apache/ant/ant/1.9.6/ant-1.9.6.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/org/apache/ant/ant-launcher/1.9.6/ant-launcher-1.9.6.jar:/Users/fengguoqiang/Software/apache-maven-3.6.0/myrepo/org/projectlombok/lombok/1.18.10/lombok-1.18.10.jar com.max.xihu.jvm.CheckCompileResult218 1 3 java.lang.AbstractStringBuilder::<init> (12 bytes)219 2 3 java.lang.String::<init> (82 bytes)219 3n 0 java.lang.System::arraycopy (native) (static)219 6 2 java.lang.String::equals (81 bytes)219 4 3 java.nio.charset.CharsetEncoder::maxBytesPerChar (5 bytes)220 8 3 java.lang.StringBuilder::append (8 bytes)220 11 3 java.lang.String::indexOf (25 bytes)221 12 s 1 java.util.Vector::size (5 bytes)221 13 3 java.io.UnixFileSystem::normalize (75 bytes)221 7 %4 java.lang.String::hashCode @ 24 (55 bytes)221 5 %4 java.lang.String::indexOf @ 37 (70 bytes)222 10 %4 sun.nio.cs.UTF_8$Encoder::encode @ 20 (359 bytes)225 20 3 java.lang.String::lastIndexOf (52 bytes)225 14 3 java.lang.String::length (6 bytes)225 25 3 java.lang.Math::min (11 bytes)225 23 3 java.util.Arrays::copyOfRange (63 bytes)226 9 4 java.lang.String::charAt (29 bytes)226 22 1 java.lang.StringCoding$StringEncoder::requestedCharsetName (5 bytes)226 15 3 java.lang.String::getBytes (27 bytes)227 30 4 java.lang.String::hashCode (55 bytes)227 29 3 java.lang.String::startsWith (72 bytes)227 27 4 java.lang.Object::<init> (1 bytes)227 26 3 java.lang.String::startsWith (7 bytes)227 28 3 java.lang.String::toCharArray (25 bytes)228 32 3 java.lang.AbstractStringBuilder::append (50 bytes)228 34 4 java.lang.String::indexOf (70 bytes)228 33 3 java.lang.String::getChars (62 bytes)229 36 3 java.lang.AbstractStringBuilder::ensureCapacityInternal (27 bytes)229 38 3 java.lang.String::indexOf (7 bytes)230 37 3 java.util.HashMap::hash (20 bytes)230 31 3 java.util.BitSet::get (69 bytes)230 24 3 java.util.Arrays::copyOf (19 bytes)231 35 1 java.lang.ref.Reference::get (5 bytes)231 21 1 sun.instrument.TransformerManager::getSnapshotTransformerList (5 bytes)231 16 ! 3 java.lang.StringCoding::encode (120 bytes)233 39 %3 com.max.xihu.jvm.CheckCompileResult::doubleValue @ 2 (18 bytes)233 40 3 java.lang.AbstractStringBuilder::newCapacity (39 bytes)233 41 %4 com.max.xihu.jvm.CheckCompileResult::doubleValue @ 2 (18 bytes)234 17 ! 3 java.lang.StringCoding$StringEncoder::encode (179 bytes)234 39 %3 com.max.xihu.jvm.CheckCompileResult::doubleValue @ -2 (18 bytes) made not entrant234 41 %4 com.max.xihu.jvm.CheckCompileResult::doubleValue @ -2 (18 bytes) made not entrant234 42 4 com.max.xihu.jvm.CheckCompileResult::doubleValue (18 bytes)235 43 %4 com.max.xihu.jvm.CheckCompileResult::doubleValue @ 2 (18 bytes)236 18 3 java.lang.StringCoding::access$300 (8 bytes)236 44 3 com.max.xihu.jvm.CheckCompileResult::calcSum (26 bytes)236 19 3 java.lang.StringCoding::safeTrim (24 bytes)236 45 %4 com.max.xihu.jvm.CheckCompileResult::calcSum @ 4 (26 bytes)238 46 4 com.max.xihu.jvm.CheckCompileResult::calcSum (26 bytes)239 44 3 com.max.xihu.jvm.CheckCompileResult::calcSum (26 bytes) made not entrant
其中带有%的输出说明是由回边计数器触发的OSR编译。
2.编译优化技术
我们都知道,编译执行肯定比解释执行更快。解释执行字节码时不仅需要额外消耗时间,更重要的原因是虚拟机设计团队几乎把对代码的所有优化措施都集中在了即时编译器中。所以说即时编译器编译的本地代码会比Javac产生的字节码更优秀。即时编译器有非常多的优化技术,不进行一一列举,仅介绍几种比较常见的优化技术。
语言无关的经典优化之一:公共子表达式消除。语言相关的经典优化之一:数组范围检查消除。最重要的优化技术之一:方法内联。最前沿的优化技术之一:逃逸分析。
2.1公共子表达式消除
什么是公共子表达式消除嘞?就是如果一个表达式E已经计算过了,并且从先前的计算到现在E中的所有变量的值都没有发生过变化,那么E的这次出现就成为公共子表达式。
因为出现了这种每次变量都不变,并且表达式的值都是一样的,那么干嘛要重新计算呢,干脆把前面计算的结果拿来用就行了。
局部公共子表达式消除全局公共子表达式消除
这两个区别就是看优化的范围是仅限于程序的基本块内,还是说涵盖了多个基本块。
举个例子说明下:
int r = (a * b) * 7 + a + (a + b * a)
如果a * b的值在程序的执行过程中都不变,那么就可以优化为下面表达式
int r = E * 7 + a + (a + E)
不仅如此,还可以进行代数化简为:
int r = E * 8 + 2 * a
2.2数组范围检查消除
数组越界可能作为程序员都会遇到过,但是不同的语言可能有不同的处理方式。Java语言不像C,C++那样本质上是裸指针操作。Java是一门动态安全的语言,如果有个数组foo[],在Java中访问的话,例如foo[i]。系统会自动进行上下界的范围检查,如果i<0||i>foo.length
的话,就会抛出运行时异常:java.lang.ArrayIndexOutOfBoundsException
。抛出异常对于开发者来说是一件好事儿,即使没有编写防御型的代码,也至少不会出现指针溢出之类的攻击。
但是呢,对于虚拟机执行子系统,数组的读写都有有隐含的条件判断的,那么大量的数组访问,无疑会造成一种性能损失。无论如何,为了安全,数组边界的检查是必须要做的,但是数组边界的检查是不是必须在运行期间一次不落的都要检查,答案是否定的。比如说foo[3],只要编译期间根据数据流能确定foo.length
的值,并判断下标3没有越界,那么执行期间就无须判断了。而且循环之内访问数组,如果编译期通过数据流确定循环的范围永远在[0,foo.length]
之内,那整个循环中就可以把数组的上下界检查消除,节省了很多的条件判断操作。
除了数组的边界消除优化之外,还有一种避免思路—隐式异常处理。使用伪代码举个例子说明下:
if (foo != null){return foo.value;}else{throw new NullPointerException();}
使用隐式异常优化之后:
try{return foo.value;}catch(segment_fault){uncommon_trap();//虚拟机注册一个Segment Fault的信号异常处理器}
这样的话,只要foo不为空,那么就会减少一次非空判断的开销。当时当foo真的为空时,必须转到异常处理器中恢复并抛出NPE,这个过程需要从用户态转到内核态中处理,结束后,再返回到用户态。可想而知一次非空判断的开销和用户态与内核态之间的切换,哪个更耗时,毋庸置疑是后者了。所以foo经常为空的话,反而会让程序更慢。不过,HotSpot虚拟机可不笨,它会根据运行期收集的Profile信息自动选择最优方案。
2.3方法内联
方法内联是编译器最重要的优化手段之一,其实就是把目标方法的代码“复制”到发起调用方之后,避免发生真实的方法调用而已。这种优化不仅减少了方法的调用,而且还为其他优化手段建立了良好的基础。为什么这么说呢,先来看段代码,:
public static void foo(OBject obj){if (obj != null) {System.out.println("do it")}}public static void testInline(String[] args){Object obj = null;foo(obj);//调用foo方法}
可以看得出,testInline方法内部都是些无用代码,如果不做内联,后续即使进行无用代码消除优化,也无法发现“Dead Code”,因为分开来看的话,两个方法里面的操作可能都有意义。
2.4逃逸分析
最后介绍的逃逸分析可谓是比较前沿的优化技术了。它并不是直接优化代码,而是为其他优化手段提供依据的分析技术。
逃逸分析的基本行为就是分析对象动态作用域:当一个对象在方法中被定义后,可能会被其他方法引用,称为方法逃逸。例如:作为调用参数传递到其他方法中。还有被其他线程访问到,称为线程逃逸。例如:赋值给类变量或者其他线程中访问的实例变量。
如果能证明一个对象不会逃逸到方法或者线程之外,也就是不能被别的方法或者线程通过任何途径访问到这个对象,那么就可以为这个变量进行一些高效的优化。那么有哪些高效的优化呢?
栈上分配(Stack Allocation)
在Java虚拟中,对象在堆上分配已经成为Java程序员的最清楚的常识了。而且虚拟机的垃圾回收也是针对堆来进行的。回收垃圾无论是筛选可收回对象,还是回收整理内存都需要消耗时间。如果确定一个对象不会逃逸出方法之外,那么就可以将该对象在栈上分配,对象所占用的内存空间随着栈帧的出栈而销毁。一般来说,程序中是有大量的不会逃逸的对象,如果能使用栈上分配,对于垃圾收集系统的压力将会小很多。
同步消除(Synchronization Elimination)开启逃逸分析 (
-XX:+DoEscapeAnalysis
),JDK1.6以后默认开启。可通过参数-XX:+PrintEscapeAnalysis
来查看分析结果。
线程同步本身就是非常耗时的过程,如果逃逸分析能够确定一个变量不会逃逸出线程,无法被其他线程访问,那这个变量的读写肯定不会有竞争,那么就可以将同步措施进行消除。
标量替换(Scalar Replacement)开启同步消除(
-XX:+EliminateLocks
)
标量是指一个数据已经无法分解成更小的数据来表示了,Java虚拟机中的原始数据类型(int,long等数值类型以及reference类型等)都不能在进行一一分解。相对来说,如果一个数据可以继续分解,就称为聚合量,Java中的对象就是典型的聚合量。如果逃逸分析证明一个对象不会被外部访问,并且这个对象可以被拆的话,那程序真正执行的时候,就不需要创建对象,而直接创建被这个方法用到的成员变量来替换。对象拆分后,不仅可进行栈上分配和读写,而且可以为后续进一步的优化创建条件。
开启标量替换(
-XX:+EliminateAllocations
),可通过参数-XX:PrintEliminateAllocations
来查看标量替换情况。