南皮县网站建设价格/中国新闻网发稿
JVM性能调优
- 1. C1、C2与Graal编译器
- 1.1 C1编译器
- 1.2 C2编译器
- 1.3 分层编译
- 2. 热点代码
- 3. 热点探测
- 4. 方法调用计数器
- 5. 回边计数器
- 6. 编译优化技术
- 6.1 方法内联
- 7. 锁消除
本文是按照自己的理解进行笔记总结,如有不正确的地方,还望大佬多多指点纠正,勿喷。
课程内容:
1、Java的解释执行与JIT(即时编译器)
2、JIT历史发展与分层编译
3、JIT优化技术之方法内联
4、JIT优化技术之锁消除
5、JIT优化技术之逃逸分析
6、JIT优化技术之标量替换与栈上分配
跨语言(语言无关性):JVM只识别字节码(所以跨语言强大)
,所以JVM其实跟语言是解耦的,也就是没有直接关联,JVM运行不是翻译Java文件,而是识别class文件,这个一般称之为字节码。还有像Groovy 、Kotlin、Scala等等语言,它们其实也是编译成字节码,所以它们也可以在JVM上面跑,这个就是JVM的跨语言特征。Java的跨语言性一定程度上奠定了非常强大的java语言生态圈。
Java程序在运行的时候,主要就是执行字节码指令,一般这些指令会按照顺序解释执行,这种就是解释执行。
但是那些被频繁调用的代码,比如调用次数很高或者在 for 循环里的那些代码,如果按照解释执行,效率是非常低的。(这个就是Java以前被C、C++开发者吐槽慢的原因)
以上的这些代码称为热点代码。所以,为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化。
完成这个任务的编译器,就称为即时编译器(Just In Time Compiler),简称 JIT 编译器。
1. C1、C2与Graal编译器
这个编译不是把java变成class,而是将class变成0101这种二进制代码。
在JDK1.8中 HotSpot 虚拟机中,内置了两个 JIT,分别为 C1 编译器和 C2 编译器。
1.1 C1编译器
C1 编译器是一个简单快速的编译器,主要的关注点在于局部性的优化,适用于执行时间较短或对启动性能有要求的程序,例如,GUI 应用对界面启动速度就有一定要求,C1也被称为 Client Compiler。
C1编译器几乎不会对代码进行优化
1.2 C2编译器
C2 编译器是为长期运行的服务器端应用程序做性能调优的编译器,适用于执行时间较长或对峰值性能有要求的程序。根据各自的适配性,这种即时编译也被称为Server Compiler。
但是C2代码已超级复杂,无人能维护!
所以才会开发Java编写的Graal编译器取代C2(JDK10开始)
1.3 分层编译
在 Java7之前,需要根据程序的特性来选择对应的 JIT,虚拟机默认采用解释器和其中一个编译器配合工作。
Java7及以后引入了分层编译,这种方式综合了 C1 的启动性能优势和 C2 的峰值性能优势,当然我们也可以通过参数强制指定虚拟机的即时编译模式。比如我启动的时候就可以用C1,C1比较快嘛。
在 Java8 中,默认开启分层编译。
通过 java -version 命令行可以直接查看到当前系统使用的编译模式(默认分层编译)
使用“-Xint”参数强制虚拟机运行于只有解释器的编译模式
使用“-Xcomp”强制虚拟机运行于只有 JIT 的编译模式下
2. 热点代码
热点代码,就是那些被频繁调用的代码,比如调用次数很高或者在 for 循环里的那些代码。这些再次编译后的机器码会被缓存起来,以备下次使用,但对于那些执行次数很少的代码来说,这种编译动作就纯属浪费。
JVM提供了一个参数“-XX:ReservedCodeCacheSize”,用来限制 CodeCache 的大小。也就是说,JIT 编译后的代码都会放在 CodeCache 里。
如果这个空间不足,JIT 就无法继续编译,编译执行会变成解释执行,性能会降低一个数量级。同时,JIT 编译器会一直尝试去优化代码,从而造成了 CPU 占用上升。
通过 java -XX:+PrintFlagsFinal –version查询:
3. 热点探测
在 HotSpot 虚拟机中的热点探测是 JIT 优化的条件,热点探测是基于计数器的热点探测,采用这种方法的虚拟机会为每个方法建立计数器统计方法的执行次数,如果执行次数超过一定的阈值就认为它是“热点方法”
虚拟机为每个方法准备了两类计数器:方法调用计数器(Invocation Counter)和回边计数器(Back Edge Counter)。在确定虚拟机运行参数的前提下,这两个计数器都有一个确定的阈值,当计数器超过阈值溢出了,就会触发 JIT 编译。
热点探测技术(以下条件满足一个即可)
- 方法调用计数器
服务端模式默认10000 - 回边计数器(就是的goto line 22,意思就是执行到这个地方回到上面的第22行去。)
服务端模式默认10700
4. 方法调用计数器
用于统计方法被调用的次数,方法调用计数器的默认阈值在客户端模式下是 1500 次,在服务端模式下是 10000 次(我们用的都是服务端,java –version查询),可通过 -XX: CompileThreshold 来设定
通过 java -XX:+PrintFlagsFinal –version查询
5. 回边计数器
用于统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令称为“回边”(Back Edge),该值用于计算是否触发 C1 编译的阈值,在不开启分层编译的情况下,在服务端模式下是10700。
怎么算的呢!参考以下公式(有兴趣可了解):
回边计数器阈值 =方法调用计数器阈值(CompileThreshold)×(OSR比率(OnStackReplacePercentage)-解释器监控比率(InterpreterProfilePercentage)/100
通过 java -XX:+PrintFlagsFinal –version查询先关参数:
其中OnStackReplacePercentage默认值为140,InterpreterProfilePercentage默认值为33,如果都取默认值,那Server模式虚拟机回边计数器的阈值为10700.
回边计数器阈值 =10000×(140-33)=10700
6. 编译优化技术
JIT 编译运用了一些经典的编译优化技术来实现代码的优化,即通过一些例行检查优化,可以智能地编译出运行时的最优性能代码.
6.1 方法内联
方法内联的优化行为就是把目标方法的代码复制到发起调用的方法之中,避免发生真实的方法调用。
- 热点探测技术触发
- 方法体大小受限
- 使用方法内联提升性能
例如以下方法:
private int add1(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 add(int x1, int x2, int x3, int x4){return x1 + x2 + x3 + x4;
}
JVM 会自动识别热点方法,并对它们使用方法内联进行优化。
我们可以通过 -XX:CompileThreshold 来设置热点方法的阈值。
但要强调一点,热点方法不一定会被 JVM 做内联优化,如果这个方法体太大了,JVM 将不执行内联操作。
而方法体的大小阈值,我们也可以通过参数设置来优化:
经常执行的方法,默认情况下,方法体大小小于 325 字节的都会进行内联,我们可以通过 -XX:FreqInlineSize=N 来设置大小值;
不是经常执行的方法,默认情况下,方法大小小于 35 字节才会进行内联,我们也可以通过 -XX:MaxInlineSize=N 来重置大小值。
所以我们熟悉的阿里归约之类的对方法体 参数名不要超过几个之类的 因为这里涉及底层的编译有
代码演示
package ding;/*** 方法内联* -XX:+PrintCompilation 再控制台打印编译过程信息* -XX:+UnlockDiagnosticVMOptions 解锁对JVM进行诊断的选项参数。默认是关闭的,开启后支持一些特定参数对JVM进行诊断* -XX:+PrintInlining 将内联方法打印出来*/
public class CompDemo {private int add1(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 add(int x1, int x2, int x3, int x4){return x1 + x2 + x3 + x4;}public static void main(String[] args) {CompDemo compDemo = new CompDemo();//方法调用计数器的默认阈值10000次,我们循环遍历超过需要阈值for (int i = 0; i < 10000; i++){compDemo.add1(1,2,3,4);}}
}
设置 VM 参数:-XX:+PrintCompilation -XX:+UnlockDiagnosticVMOptions
-XX:+PrintInlining
-XX:+PrintCompilation //在控制台打印编译过程信息 -XX:+UnlockDiagnosticVMOptions //解锁对JVM进行诊断的选项参数。默认是关闭的,开启后支持一些特定参数对JVM进行诊断 -XX:+PrintInlining //将内联方法打印出来
-XX:+PrintCompilation -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining
从打印结果看发生了内联
什么情况下不会发生呢,我们把for循环里面的数值改的小一点。改成100吧。
可以看到没有发生内联。
热点方法的优化可以有效提高系统性能,一般我们可以通过以下几种方式来提高方法内联:
- 通过设置 JVM 参数来减小热点阈值或增加方法体阈值,以便更多的方法可以进行内联,但这种方法意味着需要占用更多地内存;
- 在编程中,避免在一个方法中写大量代码,习惯使用小方法体;
- 尽量使用 final、private、static 关键字修饰方法,编码方法因为继承,会需要额外的类型检查。
7. 锁消除
在非线程安全的情况下,尽量不要使用线程安全容器,比如 StringBuffer。由于 StringBuffer 中的 append 方法被 Synchronized 关键字修饰,会使用到锁,从而导致性能下降。
但实际上,在以下代码测试中,StringBuffer 和 StringBuilder 的性能基本没什么区别。这是因为在局部方法中创建的对象只能被当前线程访问,无法被其它线程访问,这个变量的读写肯定不会有竞争,这个时候 JIT 编译会对这个对象的方法锁进行锁消除。
下代码测试中,StringBuffer 和 StringBuilder 的性能基本没什么区别。这是因为在局部方法中创建的对象只能被当前线程访问,无法被其它线程访问,这个变量的读写肯定不会有竞争,这个时候 JIT 编译会对这个对象的方法锁进行锁消除。
package ding;public class UnLock {//两种国之间性能有没有很大的差别public static void main(String[] args) {long timeStart1 = System.currentTimeMillis();for (int i = 0; i < 10000000; i++){BufferString("king","666");//这里是调用方法}long timeEnd1 = System.currentTimeMillis();System.out.println("StringBuffer花费的时间" + (timeStart1 - timeEnd1));long timeStart2 = System.currentTimeMillis();for (int i = 0; i < 10000000; i++){BufferString("king","999");//这里是调用方法}long timeEnd2 = System.currentTimeMillis();System.out.println("StringBuffer花费的时间" + (timeStart2 - timeEnd2));}private static String BufferString(String s1, String s2) {StringBuffer sb = new StringBuffer();sb.append(s1);sb.append(s2);return sb.toString();}
}
我们把锁消除关闭—测试发现性能差别有点大
-XX:+EliminateLocks开启锁消除(jdk1.8默认开启,其它版本未测试)
-XX:-EliminateLocks 关闭锁消除