JVM运行期编译与优化

概述

        在java中目前使用的虚拟机还是Hotspot虚拟机,该名称的由来就是它内部采用的热点代码检测技术。我们首先需要知道class文件时如何被执行的,大部分的语言都是单纯的分为编译和解释两种方式。

  • 编译器:首先将源代码编译为所有电脑都能执行的目标代码(机器码),然后再执行该编译后的代码,这样通过提前先编译好,等到执行的时候就会比较快,好处就是快,缺点是编译需要占用用户的一些时间。
  • 解释器:一行一行的进行解释和运行,这种方式好处就是节约了编译时的事件,且节省了存放目标代码的空间,缺点就是运行效率没有编译好的快。

随着时间的推移,编译器是比较划算的,解释器适合那些代码量少,空间要求严格的场景

        但是java采用的是混合模式,也就是通常情况下通过解释执行,在运行时会记录执行过程中的一些代码,如果发现某些方法或代码块执行的比较频繁,那么编译器就开始工作了,它把这些热点代码进行编译,下次再运行这些编译后的代码就不需要再解释了,直接运行,加快了速度,这也是对解释执行进行了优化,所以这种检测热点代码的特点就是Hotspot的名称由来。

解释器和编译器

        Hotspot中的解释器和编译器联合工作,汲取各自的优点,可以获得更高的执行效率。我们的代码通过编译器可以变为机器码,同样的当类结构出现变化时,编译后的代码需要回退为原始代码时,也时可以的,这是通过逆优化让编译后的代码退回到解释状态继续执行。        

        Hotspot虚拟机中内置了3个编译器,1个解释器。这三个即时编译器分别为:

  • 客户端编译器(简称C1)
  • 服务端编译器(简称C2)
  • Graal编译器,这是jdk10加上去的,用于以后代替C2编译器。

        在分层编译的工作模式之前,通常采用的是解释器和其中的一个编译器进行配合工作,虚拟机会根据我们的硬件进行选择具体是使用哪个编译器,我们也可以强制指定,使用-client-server

        除了强制指定使用哪个编译器还可以强制指定hotspot的运行方式,默认的方式就是混合模式(一个解释器加一个编译器),我们通过-Xint可以强制虚拟机以解释模式进行运行,不采用编译器,通过-Xcomp强制为编译模式(不可避免的解释器还是会执行编译器不能干的事)。

使用java [-Xint|-Xcomp]  -version 可以查看工作方式和解释器,编译器版本号

分层编译

        为了能够让解释器和编译器配合工作,达到一个最佳平衡,jvm加入了分层编译的功能,其中的层次包括:

  • 第0层:纯解释器工作,且解释器不开启性能检测功能。
  • 第1层:开始使用客户端编译器,进行简单可靠的稳定优化,不开启性能检测功能。
  • 第2层:任然使用客户端编译器,仅开启方法及回边次数统计等有限的性能检测功能。
  • 第3层:在第2层的基础上增加了其他的性能检测功能,包括分支跳转、虚方法调用等全部的统计信息。
  • 第4层:使用服务端编译器,服务器编译器会进行更多编译耗时更长的优化。

以上分层的主要目的是,用客户端编译器获得更高的编译速度,用服务器编译器获得更好的编译质量,客户端编译器为服务器开启高复杂度的优化算法争取更多的编译时间。

热点代码

        热点代码主要有两类:

  • 被多次调用的方法
  • 被多次执行的循环体(以它的方法体作为整体)

    hotspot采用了基于计数器的热点检测技术,为每个方法准备了两类计数器:方法调用计数器回边计数器(循环边界往回跳的次数),这两个计数器都有明确的阈值,达到阈值后就立即启动即时编译器进行编译。      

        客户端的默认阈值为1500次,服务端10000次,可以通过-XX:CompileThreshold来设定。

        具体细节为:当方法被调用时,检查该方法是否有被编译过的版本,如果有的话执行它,如果没有被编译,则会将它的计数器加一,然后再判断调用计数器和回边计数器是否达到阈值,如果达到则向即时编译器提交一个该方法的编译请求。每个没有达到,就会使用解释器解释执行。

        当然如果按照这种方法一直执行的话,时间久了所有的方法都会被编译,显然这不是好事,所以虚拟机是使用的相对次数,而不是绝对次数,相对次数指过了一段时间如果某方法还是没有被编译,则将它的计数器全部减一半,这也叫做热衰减

演示:

public class Main{
    static int j=0;
//    -XX:+PrintCompilation 用于打印被编译的方法
    public static void main(String[] args) {
        for (int i = 0; i < 1000; i++) {
            long startTime = System.nanoTime();
            f();
            long endTime =System.nanoTime();
            System.out.println("第"+i+"次耗费的时间:"+(endTime-startTime));
        }
        j=1;
    }
    public static void f(){
        for (int i = 0; i < 50; i++) {
            j=1*4*6*3*4*9*4;
        }
    }
}

如上一段代码结果为

 当方法被执行到一定的次数时,方法就被编译了,再往后执行的话,执行时间降低了很多,这就是热点代码检测和编译。

优化技术

        jvm中有很多的优化手段,我们这里举例几种比较重要的优化。

方法内联

        方法内联是运行期的一个优化手段,非常简单。

public static int square(int i) {
       return i * i;
}
System.out.println(square(9));

如果发现square是热点方法,并且长度不太长时,会进行内联,所谓的内联就是把方法内代码拷贝、粘贴到调用者的位置:        

System.out.println(9*9);

方法内联避免了真实方法的调用,且为很多的优化建立了最基本的基础。

逃逸分析

        逃逸分析的基本原理是:分析对象的动态作用域,当一个对象在方法中被创建后,它可能会被其他的方法或者其他的线程访问到,一个对象可能的作用域有:从不逃逸(只在本方法中使用到),方法逃逸(逃出了本方法,可能被其他方法访问到),线程逃逸(逃出了本线程,可能被其他线程访问到)。

        根据逃程度的高低,可以做不同程度的优化,比如:

  • 栈上分配:如果一个对象不会逃逸出本线程,那么虚拟机可以直接将一个对象分配在虚拟机栈中,而不是堆中,这样的好处是:减轻了垃圾回收的压力,该对象随着栈的摧毁而自动销毁。
  • 标量替换:把一个对象分解出来,将其用到的成员变量恢复为原始类型(int long……),毕竟创建对象的耗费比较大,且分解出来的变量也可以栈上分配,进一步的优化。该优化不允许对象逃出方法范围。
  • 同步消除:线程同步是一个很耗时的过程,如果能给分析出该变量不会被其他线程访问,那么同步措施也就没有意义了,可以对这个变量实施的同步措施也就可以安全的线程掉。

我们来根据伪代码来模拟逃逸分析:

pulic int test(int x){
    int xx=x+2;
    Point p=new Point(xx,42);//Point类有两个成员变量x和y
    return p.getX();
}

第一步,将Point的构造方法和getX()方法进行内联优化:

pulic int test(int x){
    int xx=x+2;
    Point p=new Point();
    p.x=xx;
    p.y=42;
    return p.x;
}

第二步,进过逃逸分析,发现point对象不会逃出方法外,所以可以进行标量替换:直接把point内部的变量x、y分离出来,也不用构造Point这个对象了:

pulic int test(int x){
    int xx=x+2;
    int px=xx;
    int py=42;
    return px;
}

第三步,通过分析,发现py的值对方法不会发生任何影响,可以放心的去做无效代码的消除,最终得到结果:

pulic int test(int x){
    return x+2;
}

-XX:+DoEscapeAnalysis 开启逃逸分析(jdk7之后默认开启)

公共子表达式消除

        如果一个表达式E之前已经被计算过了,并且先前的计算到现在变量的值都未发生变换,那么E就成为公共表达式,没有必要再重新进行计算了,可以直接使用先前的算好的值。

        这个就不举例了,另外的jvm还可以进行代数化简功能,提取公因式。

        

数组边界消除

        当我们运行时访问数组时,如果越界了就会得到一个越界异常,这就是每次在访问数组时,jvm进行都会进行隐式的越界的检查操作,这也保证了安全性,但是如果访问数组的次数很多,每一次都检查是否越界,肯定会有所消耗,所以jvm出现了一种优化就是数组下标如果为常量,如array[3],它会在编译器就进行判断,运行期就不需要判断了。更常见的就是循环,如果它分析出该循环变量的取值范围不会越界,那么整个循环的数组访问就不需要判别是否越界了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值