JIT—逃逸分析

逃逸分析(JIT的优化之一)

一、 概述

在JVM中,对象实例是在堆中开辟内存,这是一个共识。但是有一个特殊情况,如果一个对象经过逃逸分析后,发现作用域只在方法内部有效,则这个对象就很可能在Java虚拟机栈上开辟内存,极大地提高了对象分配内存的性能,这样就不会有GC的可能性了。

简单来说,逃逸分析是JVM中JIT的性能优化技术,用来分析对象是否发生逃逸,而栈上分配、同步消除、标量替换是具体的优化手段。

逃逸分析的基本原理:分析对象的动态作用域

  • 一个变量在方法中被定义,它可能被外部的方法所引用,比如作为返回值返回给外部方法,这样就称为方法逃逸;

    public void method(){
        V v = new V();		//变量v没有发生逃逸
        //use V
        //......
        v = null;
    }
    public static StringBuffer createStringBuffer(String s1,String s2){
        StringBuffer sb = new StringBuffer();
        sb.append(s1);
        sb.append(s2);
        return sb;		//sb作为返回值给了外部方法,发生了逃逸,有可能是方法逃逸,也有可能是线程逃逸
    }
    
  • 甚至还有可能被外部线程访问到, 譬如赋值给可以在其他线程中访问的实例变量,这样称为线程逃逸

从未逃逸、方法逃逸、线程逃逸这是对象由低到高不同的逃逸程度。如果一个变量被证明不会逃逸到线程之外,则可以为这个变量进行不同程度的优化

/**
 * 逃逸分析
 *
 *  如何快速的判断是否发生了逃逸分析,就看new的对象实体是否有可能在方法外被调用。
 */
public class EscapeAnalysis {

    public EscapeAnalysis obj;

    /*
    方法返回EscapeAnalysis对象,发生逃逸
     */
    public EscapeAnalysis getInstance(){
        return obj == null? new EscapeAnalysis() : obj;
    }
    /*
    为成员属性赋值,发生逃逸
     */
    public void setObj(){
        this.obj = new EscapeAnalysis();
    }
    //思考:如果当前的obj引用声明为static的?仍然会发生逃逸。

    /*
    对象的作用域仅在当前方法中有效,没有发生逃逸
     */
    public void useEscapeAnalysis(){
        EscapeAnalysis e = new EscapeAnalysis();
    }
    /*
    引用成员变量的值,发生逃逸
     */
    public void useEscapeAnalysis1(){
        EscapeAnalysis e = getInstance();
        //getInstance().xxx()同样会发生逃逸
    }
}

二、 优化手段

2.1 栈上分配
  • JIT编译器在编译期间根据逃逸分析的结果,如果发现一个变量没有发生方法逃逸,就可能被优化成栈上分配。分配完后,继续在栈内执行,最后方法执行完毕出栈,此对象也随之消亡,这样就不会触发GC。

  • 常见的栈上分配场景

    成员变量赋值 、方法返回值、实例引用传递

    关闭逃逸分析的例子

     /** 	栈上分配测试
     *      
     */
    public class StatckAllocation {
        public static void main(String[] args) throws InterruptedException {
            long start = System.currentTimeMillis();
    
            for (int i = 0; i < 10000000; i++) {
                alloc();
            }
            // 查看执行时间
            long end = System.currentTimeMillis();
            System.out.println("花费的时间为: " + (end - start) + " ms");
            // 为了方便查看堆内存中对象个数,线程sleep
            Thread.sleep(1000000);
        }
    
        private static void alloc() {
            //未发生逃逸
            User user = new User();
        }
       static class User{}
    }
    

    -XX:-DoEscapeAnalysis 关闭逃逸分析

    [GC (Allocation Failure) [PSYoungGen: 65536K->808K(76288K)] 65536K->816K(251392K), 0.0010812 secs] [Times: user=0.06 sys=0.06, real=0.00 secs] 
    [GC (Allocation Failure) [PSYoungGen: 66344K->760K(76288K)] 66352K->768K(251392K), 0.0011453 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
    花费的时间为: 49 ms
    

在这里插入图片描述

触发了两次GC操作,花费时间是49ms

-XX:+DoEscapeAnalysis 开启逃逸分析

花费的时间为: 4 ms

在这里插入图片描述

输出结果为4ms,并且没有GC操作,这也说明了Java虚拟机栈不会有GC操作,两个JVM控制台截图对比发现,栈中实例数量远少于堆内存中的实例数量。

通过对比发现,如果采用站上分配,则会大大提升性能。

2.2 同步消除
  • 线程同步付出的代价比较大,造成并发性能下降。

  • 如果发现一个变量没有发生线程逃逸,无法被其他线程所访问,那么这个变量的读写就不会产生线程安全性问题,对这个变量所实施的同步措施也就可以安全地消除掉

public class SynchronizedTest {
    public void f() {
        Object hollis = new Object();
        synchronized(hollis) {
            System.out.println(hollis);
        }
    }
    //代码中对hollis这个对象进行加锁,但是hollis对象的生命周期只在f()方法中
    //并不会被其他线程所访问控制,所以在JIT编译阶段就会被优化掉。
    //优化为 ↓
    public void f2() {
        Object hollis = new Object();
        System.out.println(hollis);
    }
}
2.3 标量替换
  • 如果一个数据已经无法再分解成更小的数据来表示了,原始类型都不能再分解了(基本数据类型int、long……),那么这些数据被称为标量。

  • 如果一个数据可以被分解,那它就被称为聚合量

  • Java对象就是典型的聚合量,如果把一个Java对象分解,根据程序访问情况,把用到的成员变量通过分解替换为原始类型,这个过程被称为标量替换

public class ScalarTest {
    public static void main(String[] args) {
        alloc();   
    }
    public static void alloc(){
        Point point = new Point(1,2);
    }
}
class Point{
    private int x;
    private int y;
    public Point(int x,int y){
        this.x = x;
        this.y = y;
    }
}

以上代码,经过标量替换后,就会变成

public static void alloc(){
    int x = 1;
    int y = 2;
}

测试代码

/**
 *  	标量替换测试
 */
public class ScalarReplace {
    public static void main(String[] args) throws InterruptedException {
        long start = System.currentTimeMillis();
        for (int i = 0; i < 10000000; i++) {
            alloc();
        }
        long end = System.currentTimeMillis();
        System.out.println("花费的时间为: " + (end - start) + " ms");
        Thread.sleep(1000_000);
    }
    public static class User {
        public int id;//标量(无法再分解成更小的数据)
        public String name;//聚合量(String还可以分解为char数组)
    }

    public static void alloc() {
        User u = new User();//未发生逃逸
        u.id = 5;
        u.name = "Hello World!";
    }
}

-XX:+DoEscapeAnalysis 开启逃逸分析

花费的时间为: 5 ms

在这里插入图片描述

-XX:-DoEscapeAnalysis 关闭逃逸分析

花费的时间为: 59 ms

在这里插入图片描述

没有GC操作,这也说明了Java虚拟机栈不会有GC操作,两个JVM控制台截图对比发现,栈中实例数量远少于堆内存中的实例数量

三、 参数设置

  • 在JDK 6u23版本之后,HotSpot中默认就已经开启了逃逸分析
  • 如果使用了较早的版本,开发人员可以通过
    • -XX:+/-DoEscapeAnalysis 显式开启/关闭逃逸分析
    • -XX:+PrintEscapeAnalysis查看逃逸分析的筛选结果
    • -XX:+EliminateAllocations 开启标量替换
    • +XX:+EliminateLocks来开启同步消除
    • -XX:+PrintEliminateAllocations查看标量的替换情况

四、特殊说明

注意有一些观点,认为通过逃逸分析,JVM会在栈上分配那些不会逃逸的对象,这在理论上是可行的,但是取决于JVM设计者的选择。 Oracle HotspotJVM中并未这么做,这一点在逃逸分析相关的文档里已经说明,所以可以明确所有的对象实例都是创建在堆上。 Hotspot使用的是标量替换的优化手段

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值