深入理解Jvm即时编译器 (Just in time compiler)
深入理解Jvm即时编译器 (Just in time compiler)
首先在了解即时编译器之前要了解1个问题:
为什么要有即时编译器?
首先作为Java的执行,首先要将代码编译成字节码(对应的class文件),然后将字节码解释为机器码在操作系统上边运行,这个过程我们通常是通过解释执行的方式,解释执行:比如有十行代码,一行一行将字节码解释为机器码运行的方式。解释执行好处在于不需要考虑启动速度,比如说有100个或者1000个类,甚至更多的类,不管有多少启动的速度都是一样的。但是这样子它的效率并不高,比如说有一个for循环,或者while(true)的循环,循环次数很高,1w,10w,甚至更高,用解释执行的方式去运行的话,一直循环解释执行同样的代码块肯定不是最优的选择。所以就有了即时编译器的存在,大量去调用的方法、循环,通过热点探测技术可以标记为"热点代码"(hotspot),“热点代码”使用即时编译器编译编译成机器码,即时编译器会对代码进行优化,从而使代码的执行效率变高。这段“热点代码”为了提高效率,在jvm里会通过JIT即时编译器编译成机器码存储在缓存里边,我们称之为CodeCache,Jvm默认CodeCache大小为240M,执行命令:java -XX:PrintFlagsFinal -version查看所有jvm -XX的默认参数值 搜索ReservedCodeCacheSize 查询默认参数大小中显示大小:251658240 转换为M单位 为240M ,使用-XX ReservedCodeCacheSize可设置默认参数的大小,在实际项目过程中,如果即时编译器缓存大小不够用,会造成性能下降,下降的原因是当缓存大小不够用的时候,JIT还是会不停的编译,优化代码,进行热点探测等等,从而使CPU急剧升高,导致代码执行速度变慢
在Jvm中即时编译器分为两种,C1、C2:
即时编译器:
C1: 适合简单场景,对于启动时间有要求的程序,具体功能有方法内联,去虚拟化,冗余消除等
C2: 适合复杂场景 优化代码 比如说标量替换, 同步锁消除 ,栈上分配
热点探测技术(怎么判断为热点代码?通过计数器的方式):
方法调用计数器:
调用同一个方法超过1w次,jvm中也可以查到默认大小: CompileThreshold 默认1w 可以通过-XX: CompileThreshold 修改默认值。
回边计数器:
统计一个方法中同一个循环调用超过1w零七百次,为什么叫回边是因为在Java代码里展示的是for循环的格式,编译成字节码之后循环的方式类似于一个箭头指向,跳行执行,所以称之为回边。回边计数器阈值=方法计数器阈值*(OSR比例-监视控制器比例) OSR比例Jvm中查默认大小:OnStackReplacePercentage 默认140,监视控制器比例Jvm中查默认大小:InterpreterProfilePercentage 默认33
在jvm中,解释执行、JIT或者混合执行(解释执行+JIT)是可以选择的,单独使用解释执行命令:java -Xint -version,单独使用JIT编译执行命令:java -Xcomp -verion 使用混合模式的执行命令:java -Xmixed -version
分层编译(了解即可):
混合执行方式的情况下,会进行分层编译,第0层是解释执行(性能监控功能:热点探测技术,监控热点代码),第1层是C1编译 (简单、可靠的优化,字节码->机器码),第2层还是C1编译(仅开启性能监控,热点探测),第3层还是C1编译(开启所有的性能监控的功能),第4层C2(字节码->机器码 优化代码,标量替换, 同步锁消除 ,栈上分配)
如图:
编译优化技术:
方法内联:
把调用目标的代码复制到调用的方法中,避免真实的调用方法,简单来讲就是A方法调用B方法,B方法不执行,把B方法的代码复制到A方法当中。目的:在代码执行过程中,方法执行需要涉及虚拟机栈,要有入栈、出栈的操作,可以少一次入栈、出栈的操作,在一次入栈出栈中执行。
方法内联代码示例:
/**
* 方法内联
* -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<1000000; i++) {
compDemo.add1(1,2,3,4);
}
}
}
结论: 热点代码多写方法内联,利用JIT特性,可以提高性能 提高方法内联的方式:1.调整热点探测技术的计数次数,参数如上章节。 2.尽量写小方法。3. 使用final/static 关键字
锁消除:
在非线程安全的情况下,尽量不要使用线程安全容器,比如 StringBuffer。由于 StringBuffer 中的 append 方法被 Synchronized 关键字修饰,会使用到锁,从而导致性能下降。
但实际上,在以下代码测试中,StringBuffer 和 StringBuilder 的性能基本没什么区别。这是因为在局部方法中创建的对象只能被当前线程访问,无法被其它线程访问,这个变量的读写肯定不会有竞争,这个时候 JIT 编译会对这个对象的方法锁进行锁消除。
/**
* 锁消除
*
* -XX:+EliminateLocks开启锁消除(jdk1.8默认开启,其它版本未测试)
* -XX:-EliminateLocks 关闭锁消除
*/
public class UnLock {
public static void main(String[] args) {
long timeStart1 = System.currentTimeMillis();
for(int i=0; i<10000000; i++) {
BufferString("king","zilu");
}
long timeEnd1 = System.currentTimeMillis();
System.out.println("StringBuffer花费的时间" + (timeEnd1 - timeStart1));
long timeStart2 = System.currentTimeMillis();
for(int i=0; i<10000000; i++) {
BuilderString("james","lison");
}
long timeEnd2 = System.currentTimeMillis();
System.out.println("StringBuilder花费的时间" + (timeEnd2 - timeStart2));
}
public static String BufferString(String s1, String s2) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
return sb.toString();
}
public static String BuilderString(String s1, String s2) {
StringBuilder sd = new StringBuilder();
sd.append(s1);
sd.append(s2);
return sd.toString();
}
}
-XX:+EliminateLocks 开启锁消除(jdk1.8 默认开启,其它版本未测试)
-XX:-EliminateLocks 关闭锁消除
我们把锁消除关闭---测试发现性能差别有点大
标量替换:
逃逸分析证明一个对象不会被外部访问,如果这个对象可以被拆分的话,当程序真正执行的时候可能不创建这个对象,而直接创建它的成员变量来代替。将对象拆分后,可以分配对象的成员变量在栈或寄存器上,原本的对象就无需分配内存空间了。这种编译优化就叫做标量替换(前提是需要开启逃逸分析)。
标量替换 代码示例:
/**
* 标量替换
*
* -XX:+DoEscapeAnalysis开启逃逸分析(jdk1.8默认开启)
*-XX:-DoEscapeAnalysis 关闭逃逸分析
*
* -XX:+EliminateAllocations开启标量替换(jdk1.8默认开启)
* -XX:-EliminateAllocations 关闭标量替换
*/
public class VariableDemo {
public void foo() {
Student student = new Student();
student.name = "zhangsan";
student.age = 17;
//to do something
}
public void foo1() {
String name = "zhangsan";
int age = 17;
//to do something
}
}
class Student{
String name;
String sexType;
int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSexType() {
return sexType;
}
public void setSexType(String sexType) {
this.sexType = sexType;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
posted on 2022-02-22 16:45 这就是程序猿的快乐吧 阅读(506) 评论(0) 编辑 收藏 举报