浅谈Java中的逃逸分析

Java逃逸分析


1. JIT

我们可以将java程序变成计算机可执行的机器指令拆分为两个步骤:

  • 首先是把.java文件转换成.class文件。
  • 然后是把.class转化成机器指令的过程。

第一段编译就是javac命令。
在第二编译阶段,JVM 通过解释字节码将其翻译成对应的机器指令,逐条读入,逐条解释翻译。这就是传统JVM解释器的功能,速度非常慢,为了解决这种效率问题,在JDK1.6中引入了JIT技术。

关于JIT技术的介绍可以看这里:
https://www.cnblogs.com/msymm/p/9395234.html

其中JIT优化中最重要的一个就是逃逸分析


2. 逃逸分析


逃逸分析(Escape Analysis)是一种可以有效减少Java 程序中同步负载和内存堆分配压力的算法。
通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围,从而决定是否要将这个对象分配到堆上。

简而言之就是:

通过逃逸分析来决定某些实例或者变量是否要在堆中进行分配,如果开启了逃逸分析,即可将这些变量直接在栈上进行分配,而非堆上进行分配。这些变量的指针可以被全局所引用,或者被其它线程所引用。


2.1 主要依据


JVM判断新创建的对象是否逃逸的依据有:

  • 对象被赋值给堆中对象的字段和类的静态变量。
  • 对象被传进了不确定的代码中去运行。

对于第一种情况,因为对象被放进堆中,则其它线程就可以对其进行访问,所以对象的使用情况,编译器就无法再进行追踪。第二种情况相当于JVM在解析普通的字节码的时候,如果没有发生JIT即时编译,编译器是不能事先完整知道这段代码会对对象做什么操作。保守一点,这个时候也只能把对象是当作是逃逸来处理。


2.2 举例


public class EscapeTest {

    public static Object globalVariableObject;

    public Object instanceObject;

    public void globalVariableEscape(){
        globalVariableObject = new Object(); //静态变量,外部线程可见,发生逃逸
    }

    public void instanceObjectEscape(){
        instanceObject = new Object(); //赋值给堆中实例字段,外部线程可见,发生逃逸
    }
    
    public Object returnObjectEscape(){
        return new Object();  //返回实例,外部线程可见,发生逃逸
    }

    public void noEscape(){
        synchronized (new Object()){
            //仅创建线程可见,对象无逃逸
        }
        Object noEscape = new Object();  //仅创建线程可见,对象无逃逸
    }

}

3. 基于逃逸分析的优化


3.1 同步省略


如果同步块所使用的锁对象通过这种分析被证实只能够被一个线程访问,那么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经过逃逸分析之后发现并无线程安全问题的话,就会做锁消除。


3.2 标量替换


在JIT阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过JIT优化,就会把这个对象拆解成若干个其中包含的若干个成员变量来代替。这个过程就是标量替换。

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

private static void alloc() {
   Point point = new Point1,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);
}

这样做的好处就是可以大大减少堆内存的占用。


3.3 栈上分配


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



参考博客:
https://blog.csdn.net/hollis_chuang/article/details/80922794

https://zhuanlan.zhihu.com/p/59215831

https://blog.csdn.net/blueheart20/article/details/76167489

posted @ 2021-11-08 20:33  Dawnlight-_-  阅读(492)  评论(0编辑  收藏  举报