这就是程序猿的快乐吧

导航

深入理解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编辑  收藏  举报