JIT和逃逸分析

前一篇编译原理提到了后端编译,它是一个把class文件编译成字节指令的过程,在这个过程当中,能够走解释执行和编译执行这两条路:java

1.若是单走解释执行,每执行一个方法,都要解释一遍,执行效率上会慢一些后端

javac把java的源文件翻译成了class文件,而class文件中全都是Java字节码。那么,JVM在加载了这些class文件之后,针对这些字节码,逐条取出,逐条执行,这种方法就是解释执行。

2.若是所有走编译执行,那么就特别耗内存,每走到一个方法就编译一遍,并放到内存中,那么问题来了,内存的宝贵,使得执行频率低的代码编译占用了内存数组

还有一种,就是把这些Java字节码从新编译优化,生成机器码,让CPU直接执行。这样编出来的代码效率会更高。一般,咱们没必要把全部的Java方法都编译成机器码,只须要把调用最频繁,占据CPU时间最长的方法找出来将其编译成机器码。这种调用最频繁的Java方法就是咱们常说的热点方法(Hotspot,说不定这个虚拟机的名字就是从这里来的)。这种在运行时按需编译的方式就是Just In Time。

目前的主流商用虚拟机HotSpot使用的是混合模式(解释器与编译器的搭配使用方式都是混合模式),可能会使用到JIT(Just In Time)技术——即时编译,JIT的引入,是一项编译的优化,在JVM的执行效率和内存空间中寻找的平衡。JIT会对代码作不少优化。其中有一部分优化的目的就是减小内存堆分配压力,其中一种重要的技术叫作逃逸分析。安全

掘金上看到一篇对逃逸分析讲的很透彻的文章,转自:juejin.im/post/5b4d47…bash

什么是逃逸分析

逃逸分析的基本行为就是分析对象动态做用域:当一个对象在方法中被定义后,它可能被外部方法所引用,例如做为调用参数传递到其余地方中,称为方法逃逸。app

public static StringBuffer craeteStringBuffer(String s1, String s2) {
    StringBuffer sb = new StringBuffer();
    sb.append(s1);
    sb.append(s2);
    return sb;
}

public static String craeteStringBuffer(String s1, String s2) {
    StringBuffer sb = new StringBuffer();
    sb.append(s1);
    sb.append(s2);
    return sb.toString();
}

复制代码

第一段代码中的sb就逃逸了,而第二段代码中的sb就没有逃逸。post

使用逃逸分析,编译器能够对代码作以下优化:性能

1、同步省略。若是一个对象被发现只能从一个线程被访问到,那么对于这个对象的操做能够不考虑同步。优化

2、将堆分配转化为栈分配。若是一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象多是栈分配的候选,而不是堆分配。ui

3、分离对象或标量替换。有的对象可能不须要做为一个连续的内存结构存在也能够被访问到,那么对象的部分(或所有)能够不存储在内存,而是存储在CPU寄存器中。

在Java代码运行时,经过JVM参数可指定是否开启逃逸分析,

-XX:+DoEscapeAnalysis : 表示开启逃逸分析

-XX:-DoEscapeAnalysis : 表示关闭逃逸分析

同步省略

从jdk 1.7开始已经默认开始逃逸分析在动态编译同步块的时候,JIT编译器能够借助逃逸分析来判断同步块所使用的锁对象是否只可以被一个线程访问而没有被发布到其余线程。若是同步块所使用的锁对象经过这种分析被证明只可以被一个线程访问,那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步。这个取消同步的过程就叫同步省略,也叫锁消除。 如如下代码:

public void f() {
    Object hollis = new Object();
    synchronized(hollis) {
        System.out.println(hollis);
    }
}
复制代码

代码中对hollis这个对象进行加锁,可是hollis对象的生命周期只在f()方法中,并不会被其余线程所访问到,因此在JIT编译阶段就会被优化掉。优化成:

public void f() {
    Object hollis = new Object();
    System.out.println(hollis);
}
复制代码

因此,在使用synchronized的时候,若是JIT通过逃逸分析以后发现并没有线程安全问题的话,就会作锁消除。

标量替换

标量(Scalar)是指一个没法再分解成更小的数据的数据。Java中的原始数据类型就是标量。相对的,那些还能够分解的数据叫作聚合量(Aggregate),Java中的对象就是聚合量,由于他能够分解成其余聚合量和标量。 在JIT阶段,若是通过逃逸分析,发现一个对象不会被外界访问的话,那么通过JIT优化,就会把这个对象拆解成若干个其中包含的若干个成员变量来代替。这个过程就是标量替换。

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

private static void alloc() {
   Point point = new Point(1,2);
   System.out.println("point.x="+point.x+"; point.y="+point.y);
}
class Point{
    private int x;
    private int y;
}
复制代码

以上代码中,point对象并无逃逸出alloc方法,而且point对象是能够拆解成标量的。那么,JIT就会不会直接建立Point对象,而是直接使用两个标量int x ,int y来替代Point对象。 以上代码,通过标量替换后,就会变成:

private static void alloc() {
   int x = 1;
   int y = 2;
   System.out.println("point.x="+x+"; point.y="+y);
}
复制代码

能够看到,Point这个聚合量通过逃逸分析后,发现他并无逃逸,就被替换成两个聚合量了。那么标量替换有什么好处呢?就是能够大大减小堆内存的占用。由于一旦不须要建立对象了,那么就再也不须要分配堆内存了。

标量替换为栈上分配提供了很好的基础。

栈上分配

在Java虚拟机中,对象是在Java堆中分配内存的,这是一个广泛的常识。可是,有一种特殊状况,那就是若是通过逃逸分析后发现,一个对象并无逃逸出方法的话,那么就可能被优化成栈上分配。这样就无需在堆上分配内存,也无须进行垃圾回收了。

关于栈上分配的详细介绍,能够参考对象和数组并非都在堆上分配内存的

这里,仍是要简单说一下,其实在现有的虚拟机中,并无真正的实现栈上分配,在对象和数组并非都在堆上分配内存的的例子中,对象没有在堆上分配,实际上是标量替换实现的。

逃逸分析并不成熟关于逃逸分析的论文在1999年就已经发表了,但直到JDK 1.6才有实现,并且这项技术到现在也并非十分红熟的。 其根本缘由就是没法保证逃逸分析的性能消耗必定能高于他的消耗。虽然通过逃逸分析能够作标量替换、栈上分配、和锁消除。可是逃逸分析自身也是须要进行一系列复杂的分析的,这其实也是一个相对耗时的过程。

一个极端的例子,就是通过逃逸分析以后,发现没有一个对象是不逃逸的。那这个逃逸分析的过程就白白浪费掉了。 虽然这项技术并不十分红熟,可是他也是即时编译器优化技术中一个十分重要的手段。

相关文章
相关标签/搜索