关于jvm:聊聊JIT是如何影响JVM性能的

63次阅读

共计 3832 个字符,预计需要花费 10 分钟才能阅读完成。

花半秒钟就能看透事物本质的人,和花一辈子都看不清事物本质的人,注定是截然不同的命运 – 教父

集体公众号:月伴飞鱼,欢送关注

之前说好的这期解说并发工具类,不过 ReentrantLock 源码还没肝完,理由嘛,太忙了,身材不难受,脑袋没货,睡眠不足,剧还没追完 ……..

但说好的每周一篇干货,不能停,明天就先介绍一篇 JVM 相干常识

咱们晓得 Java 虚拟机栈是线程公有的,每个线程对应一个栈,每个线程在执行一个办法时会创立一个对应的栈帧,栈帧负责存储局部变量变量表、操作数栈、动静链接和办法返回地址等信息,每个办法的调用过程,相当于栈帧在 Java 栈的入栈和出栈过程

然而栈帧的创立是须要消耗资源的,尤其是对于 Java 中常见的 getter、setter 办法来说,这些代码通常只有一行,每次都创立栈帧的话就太节约了。

另外,Java 虚拟机栈对代码的执行,采纳的是字节码解释执行的形式,思考到上面这段代码,变量 a 申明之后,就再也不被应用,要是依照字节码指令解释执行的话,就要做很多无用功。

public class A{
    int attr = 0;
    public void test(){
        int a = attr;
        System.out.println("月伴飞鱼");
    }
}

执行如下命令:

javap -v A

能够看到这段代码的字节码指令

咱们可能看到 aload_0,getfield,istore_1 这三个无用的字节码指令操作。

aload_0 从局部变量 0 中装载援用类型值,getfield 从对象中获取字段,istore_1 将 int 类型值存入局部变量 1

另外,咱们晓得垃圾回收器回收的指标区域次要是堆,堆上创立的对象越多,GC 的压力就越大。要是能把一些变量,间接在栈上调配,那 GC 的压力就会小一些。

其实,咱们说的这几个优化的可能性,JVM 曾经通过 JIT 编译器(Just In Time Compiler)去做了,JIT 最次要的指标是把解释执行变成编译执行。

为了进步热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相干的机器码,并进行各种档次的优化,这就是 JIT 编译器的性能。

如上图,JVM 会将调用次数很高,或者在 for 循环里频繁被应用的代码,编译成机器码,而后缓存起来,下次调用雷同办法的时候,就能够间接应用。

那 JIT 编译都有哪些伎俩呢?接下来咱们具体介绍。

办法内联

办法内联它会把一些短小的办法体,间接纳入指标办法的作用范畴之内,就像是间接在代码块中追加代码。这样,就少了一次办法调用,执行速度就可能失去晋升,这就是办法内联的概念。

能够应用 -XX:-Inline 参数来禁用办法内联,如果想要更细粒度的管制,能够应用 CompileCommand 参数,例如:

-XX:CompileCommand=exclude,java/lang/String.indexOf

在 JDK 的源码里,也有很多被 @ForceInline 注解的办法,这些办法,会在执行的时候被强制进行内联;而被 @DontInline 注解的办法,则始终不会被内联。

JIT 编译之后的二进制代码,是放在 Code Cache 区域里的。这个区域的大小是固定的,而且一旦启动无奈扩容。如果 Code Cache 满了,JVM 并不会报错,但会进行编译。所以编译执行就会进化为解释执行,性能就会升高。不仅如此,JIT 编译器会始终尝试去优化你的代码,造成 CPU 占用回升。

通过参数 -XX:ReservedCodeCacheSize 能够指定 Code Cache 区域的大小,如果你通过监控发现空间达到了下限,就要适当的减少它的大小。

分层编译

HotSpot 虚拟机蕴含多个即时编译器,有 C1,C2 和 Graal,JDK8 当前采纳的是分层编译的模式。

JMV 应用额定线程进行即时编译,能够不必阻塞解释执行的逻辑。JIT 通常会在触发之后就在后盾运行,编译实现之后就将相应的字节码替换为编译后的代码。

JIT 编译形式有两种:一种是编译办法,另一种是编译循环。

具体介绍下几个编译器

C1 编译器

C1 编译器是一个简略疾速的编译器,次要的关注点在于局部性的优化,实用于执行工夫较短或对启动性能有要求的程序,也称为 Client Compiler,例如,GUI 利用对界面启动速度就有肯定要求。

C2 编译器

C2 编译器是为长期运行的服务器端应用程序做性能调优的编译器,实用于执行工夫较长或对峰值性能有要求的程序,也称为 Server Compiler,例如,服务器上长期运行的 Java 利用对稳固运行就有肯定的要求。

在 Java7 之前,须要依据程序的个性来抉择对应的 JIT,虚拟机默认采纳解释器和其中一个编译器配合工作。

分层编译

Java7 引入了分层编译,这种形式综合了 C1 的启动性能劣势和 C2 的峰值性能劣势,咱们也能够通过参数 -client 或者 -server 强制指定虚拟机的即时编译模式。

通常状况下,C2 的执行效率比 C1 高出 30% 以上。

留神:在 Java8 中,默认开启分层编译,-client 和 -server 的设置曾经是有效的了。

如果只想开启 C2,能够敞开分层编译 (-XX:-TieredCompilation),如果只想用 C1,能够在关上分层编译的同时,应用参数:-XX:TieredStopAtLevel=1

咱们能够通过 java -version 命令行能够间接查看到以后零碎应用的编译模式:

C:\Users\Administrator>java -version
java version "1.8.0_45"
Java(TM) SE Runtime Environment (build 1.8.0_45-b14)
Java HotSpot(TM) 64-Bit Server VM (build 25.45-b02, mixed mode)

mixed mode 代表是默认的混合编译模式,除了这种模式外,咱们还能够应用 -Xint 参数强制虚拟机运行于只有解释器的编译模式下,这时 JIT 齐全不染指工作;也能够应用参数 -Xcomp 强制虚拟机运行于只有 JIT 的编译模式下

逃逸剖析

上面着重解说一下逃逸剖析,这个知识点在面试的时候常常会被问到。

有这样一个问题:咱们常说的对象,除了根本数据类型,肯定是在堆上调配的吗?

答案是否定的,通过逃逸剖析,JVM 可能剖析出一个新的对象的应用范畴,从而决定是否要将这个对象调配到堆上。逃逸剖析当初是 JVM 的默认行为,能够通过参数 -XX:-DoEscapeAnalysis 关掉它。

那什么样的对象算是逃逸的呢?能够看一下上面的两种典型状况。

如代码所示,对象被赋值给成员变量或者动态变量,可能被内部应用,变量就产生了逃逸。

public class EscapeAttr {
    Object attr;
    public void test() {attr = new Object();
    }
}

再看上面这段代码,对象通过 return 语句返回。因为程序并不能确定这个对象后续会不会被应用,内部的线程可能拜访到这个后果,对象也产生了逃逸。

public class EscapeReturn {
    Object attr;
    public Object test() {Object obj = new Object();
        return obj;
    }
}

那逃逸剖析有什么益处呢?

1. 栈上调配

如果一个对象在子程序中被调配,指向该对象的指针永远不会逃逸,对象有可能会被优化为栈调配。栈调配能够疾速地在栈帧上创立和销毁对象,不必再调配到堆空间,能够无效地缩小 GC 的压力。

2. 拆散对象或标量替换

但对象构造通常都比较复杂,如何将对象保留在栈上呢?

JIT 能够将对象打散,全副替换为一个个小的局部变量,这个打散的过程,就叫作标量替换(标量就是不能被进一步宰割的变量,比方 int、long 等根本类型)。也就是说,标量替换后的对象,全副变成了局部变量,能够不便地进行栈上调配,而无须改变其余的代码。

从下面的形容咱们能够看到,并不是所有的对象或者数组,都会在堆上调配。因为 JIT 的存在,如果发现某些对象没有逃逸出办法,那么就有可能被优化成栈调配。

3. 同步打消

如果一个对象被发现只能从一个线程被拜访到,那么对于这个对象的操作能够不思考同步。

留神这是针对 synchronized 来说的,JUC 中的 Lock 并不能被打消。

要开启同步打消,须要加上 -XX:+EliminateLocks 参数。因为这个参数依赖逃逸剖析,所以同时要关上 -XX:+DoEscapeAnalysis 选项。

比方上面这段代码,JIT 判断对象锁只能被一个线程拜访,就能够去掉这个同步的影响。

public class SyncEliminate {public void test() {synchronized (new Object()) {}}
}

小结

JIT 是古代 JVM 次要的优化点,可能显著地晋升程序的执行效率。从解释执行到最高档次的 C2,一个数量级的性能晋升也是有可能的。

留神:JIT 优化并不见得每次都有用,比方代码中如果产生死循环。但如果你在启动的时候,加上 -Djava.compiler=NONE 参数,禁用 JIT,它就可能执行上来。

这篇文章中咱们次要看了办法内联、逃逸剖析等概念,理解到一些办法在被优化后,对象并不一定是在堆上调配的,它可能在被标量替换后,间接在栈上调配。这几个知识点也是在面试中常常被问到的。

JIT 的这些优化个别都是在后盾过程默默地去做了,咱们不须要关注太多。同时 Code Cache 的容量达到下限,会影响程序执行的效率,但除非你有特地多的代码,默认的 240M 一般来说,足够用了。

正文完
 0