一、即时编译(JIT)
JIT:Just In Time Compiler,即时编译器
这是针对解释型语言而言的,而且并非虚拟机必须,是一种优化手段。Hotspot就有这种技术,Java虚拟机标准对JIT的存在没有作出任何规范,这是虚拟机实现的自定义优化技术。
HotSpot虚拟机的执行引擎在执行Java代码是可以采用 解释执行和 编译执行两种方式的
如果采用的是编译执行方式,那么就会使用到JIT,而解释执行就不会使用到JIT。
HotSpot中的编译器是javac,它的工作就是将java代码编译为可执行的class文件,这部分工作是完全独立的,完全不需要运行时参与,所以java程序的编译是半独立实现的。有了字节码,就由解释器来进行解释执行,这是早期java虚拟机的工作流程;后来,java虚拟机会将执行频率高的方法或者语句块通过JIT编译成本地机器码,提高了代码执行的效率
(一)JIT工作原理
当JIT编译启用时(默认是启用的),JVM读入.class文件解释后,将其发给JIT编译器。JIT编译器将字节码编译成本机机器代码。
通常javac将程序源代码编译,转换成java字节码,JVM通过解释字节码将其翻译成对应的机器指令,逐条读入,逐条解释翻译。很显然,经过解释执行,其执行速度必然会比可执行的二进制字节码程序慢。为了提高执行速度,引入了JIT技术。
在运行时JIT会把翻译过的机器码保存起来,以备下次使用,因此从理论上来说,采用该JIT技术可以,可以接近以前纯编译技术
(二)分层编译(Tiered Compilation)
Tiered Compilation是Java7中出现的,目的是整合C1的快速编译和C2的快速执行。因为C2使用了“激进”的优化手段,编译较慢。Java7以前,一般要求快速启动的GUI程序会选择C1,偏好性能的服务器程序使用C2
热点探测
HotSpot虚拟机中有两个编译器,一个是给客户端用的叫client Compiler,另一个是服务器用的叫Server Compiler。
一般的,把Client Compiler也叫C1编译器,Server Compiler叫C2编译器或Opto编译器
虚拟机会根据自身版本与宿主机的硬件性能自动选择运行模式,也可以使用 “-client”或“-server”参数去强制指定虚拟机运行在Client模式或Server模式
热点代码有两类
- 被多次执行的方法
- 多次执行的循环体
虚拟机为每个代码块和方法设置了计数器,执行一次就加1。超过限定次数就认为是热点代码,开始JIT处理。给JIT去处理只是一个请求,并不会立即同步等待结果。因为JIT编译比较耗时,在编译完成前会继续解释执行。编译器处理都是以方法为单位,所以第一类热点代码是标准的JIT编译方式;对于第二种热点代码,JIT编译器会处理包含该循环的方法
HotSpot虚拟机有两种计数器(方法会同时记录这两个计数),它们的阈值并不同
1、调用次数计数器,可以通过-XX:CompileThreadhold参数指定阈值,不指定默认C1是1500次,C2是1万次
2、字节码中向之前跳转的指令叫“回边”,回边次数是回边计数器。明显这个针对的是第二类热点代码。它的阈值是算出来的,公式如下
OSR 阈值 = CompileThreshold *
((OnStackReplacePercentage - InterpreterProfilePercentage)/100)
第一个参数CompileThreshold就是调用计数器,后面两个也都可以通过-XX指定。默认InterpreterProfilePercentage是33,而OnStackReplacePercentage的默认值在客户端和服务器模式不一样,分别是933和140,所以阈值分别是13500和10700
// -XX:+PrintCompilation -XX:-DoEscapeAnalysis
public static void main(String[] args) {
for (int i = 0; i < 200; i++) {
long start = System.nanoTime();
for (int j = 0; j < 1000; j++) {
new Object();
}
long end = System.nanoTime();
System.out.printf("%d\t%d\n",i,(end - start));
}
}
上述代码在后面循环次数中时间花费比较少
图片来自百度
因为JVM 将执行状态分成了 5 个层次
0 层,解释执行(Interpreter)
1 层,使用 C1 即时编译器编译执行(不带 profiling)
2 层,使用 C1 即时编译器编译执行(带基本的 profiling)
3 层,使用 C1 即时编译器编译执行(带完全的 profiling)
4 层,使用 C2 即时编译器编译执行
注:
profiling 是指在运行过程中收集一些程序执行状态的数据,例如【方法的调用次数】,【循环的 回边次数】等
(三)即时编译器(JIT)与解释器的区别
1、解释器是将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释
2、JIT 是将一些字节码编译为机器码,并存入 Code Cache,下次遇到相同的代码,直接执行,无需 再编译
3、解释器是将字节码解释为针对所有平台都通用的机器码
4、JIT 会根据平台类型,生成平台特定的机器码
对于占据大部分的不常用的代码,我们无需耗费时间将其编译成机器码,而是采取解释执行的方式运 行;另一方面,对于仅占据小部分的热点代码,我们则可以将其编译成机器码,以达到理想的运行速 度。 执行效率上简单比较一下 Interpreter < C1 < C2
(四)内联
方法内联就是把被调用方函数代码"复制"到调用方函数中,来减少因函数调用开销的技术
一个简单的两数相加程序,被内联前的代码
private int add4(int x1, int x2, int x3, int x4) {
return add2(x1, x2) + add2(x3, x4);
}
private int add2(int x1, int x2) {
return x1 + x2;
}
运行一段时间后,代码被内联翻译成
private int add4(int x1, int x2, int x3, int x4) {
return x1 + x2 + x3 + x4;
}
JVM会自动的识别热点方法,并对它们使用方法内联优化 ;一段代码需要执行多少次才会触发JIT优化呢?通常这个值由-XX:CompileThreshold参数进行设置
- 使用client编译器时,默认为1500;
- 使用server编译器时,默认为10000
一个方法就算被JVM标注成为热点方法,JVM仍然不一定会对它做方法内联优化。其中有个比较常见的原因就是这个方法体太大了,分为两种情况。
- 如果方法是经常执行的,默认情况下,方法大小小于325字节的都会进行内联(可以通过** -XX:MaxFreqInlineSize=N**来设置这个大小)
- 如果方法不是经常执行的,默认情况下,方法大小小于35字节才会进行内联(可以通过** -XX:MaxInlineSize=N **来设置这个大小)
可以通过增加这个大小,以便更多的方法可以进行内联;但是除非能够显著提升性能,否则不推荐修改这个参数。因为更大的方法体会导致代码内存占用更多,更少的热点方法会被缓存,最终的效果不一定好。
如果想要知道方法被内联的情况,可以使用下面的JVM参数来配置
-XX:+PrintCompilation //在控制台打印编译过程信息
-XX:+UnlockDiagnosticVMOptions //解锁对JVM进行诊断的选项参数。默认是关闭的,开启后支持一些特定参数对JVM进行诊断
-XX:+PrintInlining //将内联方法打印出来
想要对热点的方法使用内联的优化方法,最好尽量使用final、private、static这些修饰符修饰方法,避免方法因为继承,导致需要额外的类型检查,而出现效果不好情况
(五)字段优化
JMH 基准 OpenJDK: jmh
创建 maven 工程,添加依赖如下
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<jmh.version>1.0</jmh.version>
<uberjar.name>benchmarks</uberjar.name>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>${jmh.version}</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>${jmh.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
@Warmup(iterations = 2, time = 1)
@Measurement(iterations = 5, time = 1)
@State(Scope.Benchmark)
public class Benchmark1 {
int[] elements = randomInts(1_000);
private static int[] randomInts(int size) {
Random random = ThreadLocalRandom.current();
int[] values = new int[size];
for (int i = 0; i < size; i++) {
values[i] = random.nextInt();
}
return values;
}
@Benchmark
public void test1() {
for (int i = 0; i < elements.length; i++) {
doSum(elements[i]);
}
}
@Benchmark
public void test2() {
int[] local = this.elements;
for (int i = 0; i < local.length; i++) {
doSum(local[i]);
}
}
@Benchmark
public void test3() {
for (int element : elements) {
doSum(element);
}
}
static int sum = 0;
@CompilerControl(CompilerControl.Mode.INLINE)
static void doSum(int x) {
sum += x;
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(Benchmark1.class.getSimpleName())
.forks(1)
.build();
new Runner(opt).run();
}
}
启用 doSum 的方法内联,测试结果如下(每秒吞吐量,分数越高的更好):
# Run complete. Total time: 00:00:28
Benchmark Mode Samples Score Score error Units
c.m.Benchmark1.test1 thrpt 5 3543660.510 220838.607 ops/s
c.m.Benchmark1.test2 thrpt 5 3349451.189 913399.544 ops/s
c.m.Benchmark1.test3 thrpt 5 3472374.929 602064.401 ops/s
禁用 doSum 方法内联
@CompilerControl(CompilerControl.Mode.DONT_INLINE)
static void doSum(int x) {
sum += x;
}
# Run complete. Total time: 00:00:28
Benchmark Mode Samples Score Score error Units
c.m.Benchmark1.test1 thrpt 5 421998.239 185020.935 ops/s
c.m.Benchmark1.test2 thrpt 5 505350.741 14294.063 ops/s
c.m.Benchmark1.test3 thrpt 5 463303.567 180720.536 ops/s
如果 doSum 方法内联了,刚才的 test1 方法会被优化成下面的样子
@Benchmark
public void test1() {
// elements.length 首次读取会缓存起来 -> int[] local
for (int i = 0; i < elements.length; i++) { // 后续 999 次 求长度 <- local
sum += elements[i]; // 1000 次取下标 i 的元素 <- local
}
}
二、反射优化
Java的反射技术,使静态语言的java具备了动态语言的某些特质。反射,让java动态,编程的时候更加灵活,能够动态获取信息以及动态调用对象方法。其实,Java基础技术中的代理,注解也都是依托反射才 能得以实现并应用广泛,另外我们常用的Spring、myBatis等技术框架也都是依托反射才能得以实现
public class Reflect1 {
public static void test() {
System.out.println("test...");
}
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException {
Method test = Reflect1.class.getMethod("test");
for (int i = 0; i <= 16; i++) {
System.out.printf("%d\t", i);
test.invoke(null);
}
System.in.read();
}
}
test.invoke 前面 0 ~ 15 次调用使用的是 MethodAccessor 的 NativeMethodAccessorImpl 实现
package sun.reflect;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import sun.reflect.misc.ReflectUtil;
class NativeMethodAccessorImpl extends MethodAccessorImpl {
private final Method method;
private DelegatingMethodAccessorImpl parent;
private int numInvocations;
NativeMethodAccessorImpl(Method method) {
this.method = method;
}
public Object invoke(Object target, Object[] args)
throws IllegalArgumentException, InvocationTargetException {
// inflationThreshold 膨胀阈值,默认 15
if (++this.numInvocations > ReflectionFactory.inflationThreshold()
&& !ReflectUtil.isVMAnonymousClass(this.method.getDeclaringClass()))
{
// 使用 ASM 动态生成的新实现代替本地实现,速度较本地实现快 20 倍左右
MethodAccessorImpl generatedMethodAccessor =
(MethodAccessorImpl)
(new MethodAccessorGenerator())
.generateMethod(
this.method.getDeclaringClass(),
this.method.getName(),
this.method.getParameterTypes(),
this.method.getReturnType(),
this.method.getExceptionTypes(),
this.method.getModifiers()
);
this.parent.setDelegate(generatedMethodAccessor);
}
// 调用本地实现
return invoke0(this.method, target, args);
}
void setParent(DelegatingMethodAccessorImpl parent) {
this.parent = parent;
}
private static native Object invoke0(Method method, Object target, Object[]args);
}
当调用到第 16 次(从0开始算)时,会采用运行时生成的类代替掉最初的实现,可以通过 debug 得到 类名为 sun.reflect.GeneratedMethodAccessor1
使用阿里的 arthas 工具
java -jar arthas-boot.jar
再输入【jad + 类名】来进行反编译
$ jad sun.reflect.GeneratedMethodAccessor1