JVM即时编译器JIT以及常见优化

写在前面

JIT,just in time是JVM的一种编译优化技术,在工作中直接接触不多,但是多了解一些内容总有时候总是很有必要。所以也来一起看下这部分内容吧!

1:什么是JIT,JIT有哪几种

Java源代码最终想要执行,仅仅通过javac编译为字节码是不够的,因为程序最终执行是在底层的操作系统执行的,而,底层操作系统是不能识别和执行class文件的字节码的,所以还需要进一步的将字节码编译为机器码,默认JVM在运行过程中会通过解析器interpreter来完成这个工作,如下图:

因为解释器一边解释一边执行是存在一定的性能问题的,特别是对于那种高频执行的热点方法,所以为了能够对于这些热点方法提高程序执行的性能,就引入了JIT,一次性的将字节码翻译为指令码,并存储起来,用时拿就行了。

javac的编译我们叫做前端编译,而JIT或者是解释器interpreter的编译我们叫做运行时编译,在看个图:

主流的JIT编译器有C1,C2,其中C1适用于对于启动速度有要求的场景,如桌面应用,C2适用于对于巅峰性能有要求的场景,如后端服务程序。

2:JIT相关的优化

2.1:方法内联

方法的执行伴随着方法的入栈和出栈操作,所以对于空间和时间都有额外的消耗,那么如果是直接将待调用方法的方法体移动到当前方法内,也就不用进行一次方法调用了,这就是方法内联。如下:

复制代码
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 add1(int x1, int x2, int x3, int x4) {
    return x1 + x2+ x3 + x4;
}

JVM会按照配置对热点方法进行方法内联,主要配置项如下:

复制代码
-XX:CompileThreshold 来设置热点方法的阈值
-XX:MaxFreqInlineSize=N 经常执行的方法的方法体大小,单位字节
-XX:MaxInlineSize=N 不经常执行的方法的方法体大小,单位字节
-XX:+PrintCompilation //在控制台打印编译过程信息
-XX:+UnlockDiagnosticVMOptions //解锁对JVM进行诊断的选项参数。默认是关闭的,开启后支持一些特定参数对JVM进行诊断
-XX:+PrintInlining //将内联方法打印出来

来测试下方法内联,如下代码:

java 复制代码
public static void main(String[] args) {
    for(int i=0; i<1000000; i++) {//方法调用计数器的默认阈值在C1模式下是1500次,在C2模式在是10000次,我们循环遍历超过需要阈值
      add1(1,2,3,4);
    }
}

输出:

2.2:逃逸分析

分析一个对象是否被外部线程,或者是外部方法引用的技术就叫做逃逸分析。如下场景对象就是逃逸的:

复制代码
外部方法访问:
public static void main(String[] args) {
    Object obj = new Object();
    // 这里因为obj会被method1访问,所以就是逃逸的
    method1(obj); 
    
}
public static void method1(Object obj) {
}

其他线程访问:
public static void main(String[] args) {
    Object obj = new Object();
    method1(obj); 
    new Thread(new Runnable() {
        sout(obj);
    }).start();
}

如果通过逃逸分析判定一个对象不会逃逸,则就可以进行一些相关的优化了。

2.2.1:栈上分配

直接在栈上创建对象就叫做栈上分配。

正常一个对象都是在堆上分配的,而堆上分配的对象在不需要使用的时候还需要通过gc进行垃圾回收才能清理,此时相比于栈上分配更消耗时间。所以如果是能够判断一个对象不会逃逸的话,就完全没必要在堆上来创建对象了(因为不会共用,只有自己用)

但目前的hotspot jvm还没有实现这项优化技术,原因是基于目前的jvm实现方式来实现栈上分配难度较大。

2.2.2:锁消除

如果判定了一个对象不会逃逸,则肯定不会出现并发问题,则就可以去掉方法执行过程中的锁操作,比如去掉synchronized关键字。这就叫做锁消除。

但是我们在实际编码时如果确定任何业务场景下都不会出现并发问题的话,最好是就不要使用锁操作了。比如能用非线程安全的StringBuilder而不要使用线程安全的StringBuffer。

2.2.3:标量替换

如果是程序的执行只是用到了对象的几个属性值,那么此时完全可以用定义局部变量的方式来进行等价操作,这叫做标量替换,如下:

复制代码
标量替换前:
private static void createXx(int x, int y) {
    Point point = new Point();
    point.x = 1;
    point.y = 2;
    System.out.println(x);
    System.out.println(y);   
}

标量替换后:
private static void createXx(int x, int y) {
    int x = 1;
    int y = 2;
    System.out.println(x);
    System.out.println(y);   
}

写在后面

参考文章列表

多知道一点

比较新的JIT编译器

Java9,AOT 编译器。Java10 graal编译器。

Class.forName和ClassLoader.loadClass的区别

Class.forName会执行完整的加载,链接,初始化,当然如果不想要执行初始化的话,可以使用重载方法java.lang.Class.forName(java.lang.String, boolean, java.lang.ClassLoader),并将第二个参数设置为false。

如下测试:

复制代码
package com.demo.xx;

public class Father {


    static {
        fatherStatic = 0;
        System.out.println("father 静态代码块");
    }

    private static int fatherStatic = 1;

    public static void main(String[] args) {
    }

} 
执行初始化:
Class.forName("com.demo.xx.Father"); 
输出:
father 静态代码块
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------

不执行初始化:
Class.forName("com.demo.xx.Father", false, ClassForName.class.getClassLoader());
啥都不输出

而ClassLoader.loadClass方法只会执行加载,链接和初始化都不会执行,如下:

复制代码
LoadByClassLoader.class.getClassLoader().loadClass("com.demo.xx.Father");

啥也不输出。
相关推荐
m0_528174452 小时前
用Python读取和处理NASA公开API数据
jvm·数据库·python
loading小马2 小时前
解决jdk17版本与seata冲突问题
java·jvm·jdk·intellij-idea
2401_857865233 小时前
使用XGBoost赢得Kaggle比赛
jvm·数据库·python
暮冬-  Gentle°3 小时前
使用Python进行网络设备自动配置
jvm·数据库·python
m0_743297423 小时前
将Python Web应用部署到服务器(Docker + Nginx)
jvm·数据库·python
小小怪7503 小时前
实战:用Python开发一个简单的区块链
jvm·数据库·python
小年糕是糕手3 小时前
【35天从0开始备战蓝桥杯 -- 刷题包】
c语言·jvm·数据结构·c++·算法·蓝桥杯
add45a3 小时前
为你的Python脚本添加图形界面(GUI)
jvm·数据库·python