字节码执行方式--解释执行和JIT

此文已由做者赵计刚薪受权网易云社区发布。html

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


一、两种执行方式:安全

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

    • 强制使用该模式:-Xintscrapy

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

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

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

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

注意:线程

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

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

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



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

二、解释执行

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

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

 

三、编译执行

  • 编译的对象

    • 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,而后判断回边计数器是否超过阈值,若超过,进行编译,后台线程进行编译,前台线程继续解释执行(即不会阻塞),直到下一次执行到循环体时,若是编译好了,就直接执行机器码,若是没编译好,就解释执行。

     

    四、C1优化

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

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

    • 方法内联

    • 冗余消除

    • 复写传播

    • 消除无用代码

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

    • 去虚拟化

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

    4.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.二、冗余消除

    冗余消除:如上边的两个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.三、复写传播

    固然,在冗余消除后,JIT对上述的代码进行分析,发现变量z没用(能够彻底用y来代替),进行"复写传播"以后,

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

    4.1.四、无用代码消除

    在"复写传播"后,发现"y=y"是无用代码,因此能够进行"无用代码的消除"操做,消除以后,

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

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

     

    4.二、类型继承关系分析、去虚拟化

    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.好比"去虚拟化",若是编译以后,发现类的实现方法多于一种了,此时就要执行"逆优化"

     

    五、C2优化

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

    逃逸分析(辅助):

    开启:-XX:+DoEscapeAnalysis

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

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

    • 栈上分配

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

    5.一、标量替换

    含义:将一个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.二、栈上分配

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

    好处:

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

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

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

    5.三、同步削除

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

        synchronized(cat){
            //do xxx
        }

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

    //do xxx

     

    总结:

    解释器:

    • 程序启动速度比编译快

    • 节省内存(不须要编译,因此不须要放置编译后的机器码)

    JIT编译器:

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

    注意:

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


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

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


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

    相关文章
    相关标签/搜索