JIT—逃逸分析

    技术2022-07-11  115

    逃逸分析(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控制台截图对比发现,栈中实例数量远少于堆内存中的实例数量

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

    三、 参数设置

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

    四、特殊说明

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

    Processed: 0.013, SQL: 9