zoukankan      html  css  js  c++  java
  • 字节码执行方式--解释执行和JIT

    此文已由作者赵计刚薪授权网易云社区发布。

    欢迎访问网易云社区,了解更多网易技术产品运营经验。


    1、两种执行方式:

    • 解释执行(运行期解释字节码并执行)

      • 强制使用该模式:-Xint

    • 编译为机器码执行(将字节码编译为机器码并执行,这个编译过程发生在运行期,称为JIT编译)

      • 强制使用该模式:-Xcomp,下面是两种编译模式

      • client(即C1):只做少量性能开销比高的优化,占用内存少,适用于桌面程序。

      • server(即C2):进行了大量优化,占用内存多,适用于服务端程序。会收集大量的运行时信息。

    注意:

    • 32为机器默认选择C1,可在启动时添加-client或-server来指定,64位机器若CPU>2且物理内存>2G则默认为C2,否则为C1

    • Hotspot JVM执行代码的机制:对在执行过程中执行频率高的代码进行编译,对执行频率不高的代码继续解释执行

    查看当前机器默认是client模式还是server模式,使用:"java -version"命令,如下



    其中,mixed mode表示"解释执行+编译执行"的混合模式

    2、解释执行

    查看 第三章 类文件结构与javap的使用 中的inc()方法的执行

    或者查看《深入了解java虚拟机(第二版)》P272-P275

     

    3、编译执行

    • 编译的对象

      • OSR编译:编译整段代码,但是只有循环体部分会执行机器码,其他部分还是解释执行

      • 方法

      • 方法中的循环体

    • 触发条件(执行频率大于多少)

      • client:13995  server:10700

      • 该阈值可通过-XX:OnStackReplacePercent(注意该OSRP只是一个计算回边计数阈值的中间值),回边计数阈值

      • client:CompileThreshold*OSRP/100

      • server:CompileThreshold*(OSRP-InterPreterProfilePercentage)/100

      • -XX:OnStackReplacePercent:140  InterPreterProfilePercentage:33

      • client:1500  server:10000 

      • 该阈值可通过-XX:CompileThreshold来指定

      • 这里"方法调用的次数"是指一段时间(半衰周期)内的调用次数,如果半衰周期内,该次数没有达到阈值,则该次数减半。


      • -XX:-UseCounterDecay 关闭上述机制,即半衰周期的无穷大

      • -XX:CounterHalfLifeTime 半衰周期

      • 方法调用计数器:方法被调用的次数

      • 回边计数器:循环体内循环代码的执行次数(即for中代码的循环的次数)

      • 方法编译执行

        • 解释器调用方法时,检查是否有已经存在的编译版本,如果有,执行机器码,如果没有,方法调用计数器+1,然后判断方法调用计数器是否超过阈值,若超过,进行编译,后台线程进行编译,前台线程继续解释执行(即不会阻塞),直到下一次调用方法时,如果编译好了,就直接执行机器码,如果没编译好,就解释执行。

      • 循环体编译执行

        • 解释器执行到循环体时,检查是否有已经存在的编译版本,如果有,执行机器码,如果没有,回边计数器+1,然后判断回边计数器是否超过阈值,若超过,进行编译,后台线程进行编译,前台线程继续解释执行(即不会阻塞),直到下一次执行到循环体时,如果编译好了,就直接执行机器码,如果没编译好,就解释执行。

       

      4、C1优化

      说明:关于全部的优化技术列表,查看《深入理解java虚拟机(第二版)》P346-P347

      只做少量性能开销比高的优化,占用内存少,主要的优化包括:

      • 方法内联

      • 冗余消除

      • 复写传播

      • 消除无用代码

      • 类型继承关系分析(CHA,辅助)

      • 去虚拟化

      4.1、方法内联、冗余消除、复写传播、消除无用代码

      4.1.1、方法内联

      方法内联含义:假设方法A调用了方法B,把B的指令直接植入到A中。

          static class B{
              int value;
              final int get() {
                  return value;
              }
          }
          
          public void foo() {
              y = b.get();
              //do something
              z = b.get();
              sum = y + z;
          }

      说明:在上述代码中,b是B的一个实例。

      方法内联之后,

          public void foo() {
              y = b.value;
              //do something
              z = b.value;
              sum = y + z;
          }

      方法内联的条件:

      • get()编译后的字节数<=35byte(默认) -XX:MaxInlineSize=35指定

      方法内联的地位:

      • 优化系列中最一开始使用的方式(因为是很多其他优化手段的基础)

      • 消除方法调用的成本(建立栈帧、避免参数传递、避免返回值传递、避免跳转)

      4.1.2、冗余消除

      冗余消除:如上边的两个b.value冗余(前提,在do something部分没有对b.value进行操作,这也是我们在做优化之前需要先收集数据的原因)

      假设在do something部分没有对b.value进行操作,进行冗余消除后,

          public void foo() {
              y = b.value;
              //do something
              z = y;
              sum = y + z;
          }

      4.1.3、复写传播

      当然,在冗余消除后,JIT对上述的代码进行分析,发现变量z没用(可以完全用y来代替),进行"复写传播"之后,

          public void foo() {
              y = b.value;
              //do something
              y = y;
              sum = y + y;
          }

      4.1.4、无用代码消除

      在"复写传播"后,发现"y=y"是无用代码,所以可以进行"无用代码的消除"操作,消除之后,

          public void foo() {
              y = b.value;
              //do something
              sum = y + y;
          }

      需要说明的是,这里的"无用代码的消除"是在前三部优化的基础上来做的,而javac编译中"语义分析"部分的"无用代码的消除"是直接消除一些直接写好的代码(例如:if(false){})

       

      4.2、类型继承关系分析、去虚拟化

      public interface Animal {
          public void eat();
      }
      
      public class Cat implements Animal{
          public void eat() {
              System.out.println("cat eat fish");
          }
      }
      
      public class Test{
          public void methodA(Animal animal){
              animal.eat();
          }
      }

      首先分析Animal的整个"类型继承关系",发现只有一个实现类Cat,那么在methodA(Animal animal)的代码就可以优化为如下,

          public void methodA(Animal animal){
              System.out.println("cat eat fish");
          }

      但是,如果之后在运行过程中,"类型继承关系"发现Animal又多了一个实现类Dog,那么此时就不在执行之前优化编译好的机器码了,而是进行解释执行,即如下的"逆优化"。

      逆优化:

      当编译后的机器码的执行不再符合优化条件,则该机器码对应的部分回到解释执行。

      eg.比如"去虚拟化",如果编译之后,发现类的实现方法多于一种了,此时就要执行"逆优化"

       

      5、C2优化

      进行了大量优化,占用内存多,适用于服务端程序,对于C2优化,除了具有C1的优化措施后,还有很多优化。

      逃逸分析(辅助):

      开启:-XX:+DoEscapeAnalysis

      根据运行状况来判断方法中的变量是否会被方法或外部线程所读取,若不会,此变量是不逃逸的。基于此,C2在编译时会做:

      • 标量替换:开启 -XX:+EliminateAllocations

      • 栈上分配

      • 同步削除:开启 -XX:+EliminateLocks

      5.1、标量替换

      含义:将一个java对象打散,根据程序,将该对象中的属性作为一个个标量来使用。

          Point point = new Point(1,2);
          System.out.println("point.x:" + point.x + ",point.y:" + point.y);
          //do after

      若在//do after中(即前边两句代码之后的所有代码中)再没有其他代码访问"point对象"了,则将"point对象"打散并进行标量替换,

          int x = 1;
          int y = 2;
          System.out.println("point.x:" + x + ",point.y:" + y);

      好处:

      • 如果对象中定义的所有变量有的并没有被用到,"标量替换"可以节省内存

      • 执行时,不需要寻找对象引用,速度会快

      5.2、栈上分配

      含义:确定一个方法的变量不会逃逸出当前方法之外(即该变量不会被其他方法引用),则该变量可以直接分配在栈上,随方法执行结束,栈帧消失,该变量也消失,减轻GC压力。

      好处:

      • 执行时,不需要根据对象引用去堆中找对象,速度会快

      • 分配在栈上,随方法执行结束,栈帧消失,该变量也消失,减轻GC压力。

      • 使用栈上分配,必须开启标量替换

      5.3、同步削除

      含义:确定一个方法的变量不会逃逸出当前线程之外(即该变量不会被其他线程使用),则对于该变量的同步策略就消除掉,如下,

          synchronized(cat){
              //do xxx
          }

      若cat不会逃逸出当前线程,则同步块可以去掉,如下,

      //do xxx

       

      总结:

      解释器:

      • 程序启动速度比编译快

      • 节省内存(不需要编译,所以不需要放置编译后的机器码)

      JIT编译器:

      • 时间长了,对于"热点代码"的执行会快

      注意:

      • 使用JIT而不是使用在编译期直接编译成机器码,除了解释器部分的两条有点外,还为了在运行期收集数据,有目的的进行编译


      免费领取验证码、内容安全、短信发送、直播点播体验包及云服务器等套餐

      更多网易技术、产品、运营经验分享请点击


      相关文章:
      【推荐】 用scrapy数据抓取实践

    • 相关阅读:
      【Azure Redis 缓存】Azure Redis 功能性讨论二
      【Azure Developer】如何用Microsoft Graph API管理AAD Application里面的Permissions
      【Azure 环境】通过Python SDK收集所有订阅简略信息,例如订阅id 名称, 资源组及组内资源信息等,如何给Python应用赋予相应的权限才能获取到信息呢?
      【Azure 应用服务】App Service与APIM同时集成到同一个虚拟网络后,如何通过内网访问内部VNET的APIM呢?
      【Azure 云服务】如何从Azure Cloud Service中获取项目的部署文件
      【Azure Redis 缓存】Azure Redis 异常
      【Azure 微服务】基于已经存在的虚拟网络(VNET)及子网创建新的Service Fabric并且为所有节点配置自定义DNS服务
      【Azure Redis 缓存】遇见Azure Redis不能创建成功的问题:至少一个资源部署操作失败,因为 Microsoft.Cache 资源提供程序未注册。
      【Azure Redis 缓存】如何得知Azure Redis服务有更新行为?
      【Azure API 管理】在 Azure API 管理中使用 OAuth 2.0 授权和 Azure AD 保护 Web API 后端,在请求中携带Token访问后报401的错误
    • 原文地址:https://www.cnblogs.com/zyfd/p/10082264.html
    Copyright © 2011-2022 走看看