逃逸分析实例详解

tech2022-08-10  127

简介

逃逸分析是编译程序优化的一个方式,一个依据。

众所周知,java中的垃圾回收靠的是GC,但是GC所花的时间不容忽视,是影响我们程序性能的一大重要因素。为了减少GC的时间,我们得采取措施,避免产生一些不必要的对象在堆中。而逃逸分析,就是迈向巅峰的第一步。

简单的例子

逃逸分析,分析的是**对象指针(java中可以指引用)**的活动范围,比如,我们在fun()方法中创建一个变量a

public void fun(){ Integer a = 130; }

a对象的引用就在这个fun所在的栈中,它不会被返回给别的方法。随着fun函数执行完毕,退出方法栈,a对象的引用也一并被清楚了。因此我们可以判定,a对象的引用活动范围就只有fun(),它不会“逃逸”出去。

public Integer fun(){ Integer a = 130; return a; }

但是下面这个方法就不同了,a对象的引用会逃到fun()方法栈外的地方,我们可以说它“逃逸了”。

常见的3种逃逸

class A { public static B b; public void globalVariablePointerEscape() { // 给全局变量赋值,发生逃逸 b = new B(); } public B methodPointerEscape() { // 方法返回值,发生逃逸 return new B(); } public void instancePassPointerEscape() { methodPointerEscape().printClassName(this); // 实例引用传递,发生逃逸 } } class B { public void printClassName(A a) { // A对象逃到了这里! System.out.println(a.class.getName()); } }

逃逸分析优化JVM原理

减少堆对象–栈上分配!

前面我们知道了如何判断对象是否逃逸,那我们就可以动动脑子思考如何利用这些逃逸信息。

文章开头讲了,GC在遍历引用树回收对象时,会花费较多的时间,如果对象成千上万,这个时间可不得了。于是我们很容易想到一个思路: 减少对象的数量

没错,就是减少对象的数量,一旦对象少了,GC也自然会变快。从逃逸分析的例子我们可以发现,不会逃逸的对象是完全没必要在堆上创建的,因为它的引用随着方法栈弹出也灰飞烟灭了,GC就会在下次遍历时把它删除。于是JVM在逃逸分析开启后,在编译时会把“非逃逸”对象直接在栈上的局部变量表上创建,该对象生命周期和该方法生命周期同步,从而减少了堆上对象的数量,减轻了GC开销

去除竞争–锁消除

如果一个变量是“非逃逸”的,那它根本就不会存在被多个线程竞争的情况,在这种情况下,若该方法内对此对象加了锁,会被编译器自动消除。

干脆连对象都不要了–标量替换

在栈上分配中,我们把对象从堆移到了栈中,其实再进一步,若该对象的成员变量都是标量,则jvm不会在栈上创建该对象,而是创建该对象的成员变量,作为该方法的局部变量,放入局部方法表中,进一步节省了对象带来的额外空间~

代码实例

栈上分配
public class Main{ public static void main(String[] args) throws InterruptedException { for(int i=0;i<5_000_000;i++){ createObject(); } System.out.println("over!"); } public static void createObject(){ new MyObject(); } } class MyObject{ int a; int b; }
关闭逃逸分析

JVM参数配置

-Xlog:gc -Xms5M -Xmn5M -XX:-DoEscapeAnalysis

[0.014s][info][gc] Using G1 [0.030s][info][gc] Periodic GC disabled [0.077s][info][gc] GC(0) Pause Young (Normal) (G1 Evacuation Pause) 5M->0M(6M) 1.321ms [0.078s][info][gc] GC(1) Pause Young (Normal) (G1 Evacuation Pause) 4M->1M(7M) 0.844ms [0.079s][info][gc] GC(2) Pause Young (Normal) (G1 Evacuation Pause) 5M->0M(7M) 0.173ms [0.080s][info][gc] GC(3) Pause Young (Normal) (G1 Evacuation Pause) 4M->0M(7M) 0.268ms [0.081s][info][gc] GC(4) Pause Young (Normal) (G1 Evacuation Pause) 4M->0M(7M) 0.290ms [0.082s][info][gc] GC(5) Pause Young (Normal) (G1 Evacuation Pause) 4M->0M(9M) 0.323ms [0.083s][info][gc] GC(6) Pause Young (Normal) (G1 Evacuation Pause) 4M->0M(9M) 0.193ms [0.083s][info][gc] GC(7) Pause Young (Normal) (G1 Evacuation Pause) 4M->0M(9M) 0.336ms [0.084s][info][gc] GC(8) Pause Young (Normal) (G1 Evacuation Pause) 4M->0M(9M) 0.178ms [0.085s][info][gc] GC(9) Pause Young (Normal) (G1 Evacuation Pause) 4M->0M(12M) 0.267ms [0.086s][info][gc] GC(10) Pause Young (Normal) (G1 Evacuation Pause) 4M->0M(12M) 0.206ms [0.087s][info][gc] GC(11) Pause Young (Normal) (G1 Evacuation Pause) 4M->0M(12M) 0.177ms [0.088s][info][gc] GC(12) Pause Young (Normal) (G1 Evacuation Pause) 4M->0M(12M) 0.175ms [0.088s][info][gc] GC(13) Pause Young (Normal) (G1 Evacuation Pause) 4M->0M(26M) 0.286ms [0.090s][info][gc] GC(14) Pause Young (Normal) (G1 Evacuation Pause) 4M->0M(26M) 0.204ms [0.090s][info][gc] GC(15) Pause Young (Normal) (G1 Evacuation Pause) 4M->0M(26M) 0.175ms [0.091s][info][gc] GC(16) Pause Young (Normal) (G1 Evacuation Pause) 4M->0M(26M) 0.187ms [0.092s][info][gc] GC(17) Pause Young (Normal) (G1 Evacuation Pause) 4M->0M(68M) 0.546ms [0.094s][info][gc] GC(18) Pause Young (Normal) (G1 Evacuation Pause) 4M->0M(68M) 0.204ms [0.094s][info][gc] GC(19) Pause Young (Normal) (G1 Evacuation Pause) 4M->0M(68M) 0.182ms [0.095s][info][gc] GC(20) Pause Young (Normal) (G1 Evacuation Pause) 4M->1M(68M) 0.139ms [0.096s][info][gc] GC(21) Pause Young (Normal) (G1 Evacuation Pause) 5M->1M(202M) 1.140ms [0.098s][info][gc] GC(22) Pause Young (Normal) (G1 Evacuation Pause) 5M->1M(202M) 0.337ms [0.099s][info][gc] GC(23) Pause Young (Normal) (G1 Evacuation Pause) 5M->1M(202M) 0.126ms [0.100s][info][gc] GC(24) Pause Young (Normal) (G1 Evacuation Pause) 6M->1M(202M) 0.117ms [0.103s][info][gc] GC(25) Pause Young (Normal) (G1 Evacuation Pause) 6M->1M(606M) 3.061ms [0.105s][info][gc] GC(26) Pause Young (Normal) (G1 Evacuation Pause) 6M->1M(606M) 0.631ms [0.106s][info][gc] GC(27) Pause Young (Normal) (G1 Evacuation Pause) 6M->1M(606M) 0.240ms over! Process finished with exit code 0

从结果我们可以看到,jvm发生了超多次GC

开启逃逸分析

-Xlog:gc -Xms5M -Xmn5M -XX:+DoEscapeAnalysis

ps: jvm默认开启逃逸分析

[0.013s][info][gc] Using G1 [0.028s][info][gc] Periodic GC disabled over!

看一次GC都没有,因为对象在方法出栈时就没了~

假如我们把createObject方法改成这两样会怎么样

// 定义方法返回MyObject但是只返回null plan A public static MyObject createObject(){ MyObject myObject = new MyObject(); return null; } // 返回对象给main public static MyObject createObject(){ return new MyObject(); }

结果

// plan A [0.014s][info][gc] Using G1 [0.029s][info][gc] Periodic GC disabled [0.077s][info][gc] GC(0) Pause Young (Normal) (G1 Evacuation Pause) 5M->0M(6M) 1.166ms over! // plan B [0.014s][info][gc] Using G1 [0.030s][info][gc] Periodic GC disabled over!

从结果得出,如果返回的是创建的object,还是会被视作逃逸,但是只发生了1次GC,我也不知道为什么;如果返回的是永远是null,会被认为没有逃逸

锁消除

…有点尴尬,最开始的时候用StringBuffer的append,结果速度没太大变化,我也不知道为什么…,希望有评论区的小可爱能告诉我orz

现在用synchronized锁StringBuilder倒是有效果

-server -Xlog:gc -Xms200M -Xmn200M -XX:+DoEscapeAnalysis -XX:+EliminateLocks

public class Main{ static CountDownLatch lock = new CountDownLatch(4); public static void main(String[] args) throws InterruptedException { ExecutorService executor = new ThreadPoolExecutor(16,20,0, TimeUnit.HOURS,new ArrayBlockingQueue<Runnable>(10)); try{ Task task = new Task(lock); long start = System.currentTimeMillis(); executor.execute(task); executor.execute(task); executor.execute(task); executor.execute(task); lock.await(); long end = System.currentTimeMillis(); System.out.println("over! "+(end-start)); }finally { executor.shutdown(); } } } class Task implements Runnable{ CountDownLatch lock; public Task(CountDownLatch lock){ this.lock = lock; } @Override public void run() { for(int i=0;i<5_000_000;i++){ deleteSync(); } lock.countDown(); } public void deleteSync(){ StringBuilder sb = new StringBuilder(); synchronized (sb){ } } }

结果

// 开启锁消除前 [0.014s][info][gc] Using G1 [0.028s][info][gc] Periodic GC disabled [0.604s][info][gc] GC(0) Pause Young (Normal) (G1 Evacuation Pause) 200M->1M(202M) 1.437ms [1.147s][info][gc] GC(1) Pause Young (Normal) (G1 Evacuation Pause) 199M->1M(202M) 0.992ms [1.734s][info][gc] GC(2) Pause Young (Normal) (G1 Evacuation Pause) 199M->1M(202M) 1.011ms [2.196s][info][gc] GC(3) Pause Young (Normal) (G1 Evacuation Pause) 199M->1M(202M) 0.987ms [2.714s][info][gc] GC(4) Pause Young (Normal) (G1 Evacuation Pause) 199M->1M(202M) 1.030ms over! 2875 // 开启锁消除后 [0.014s][info][gc] Using G1 [0.029s][info][gc] Periodic GC disabled [0.116s][info][gc] GC(0) Pause Young (Normal) (G1 Evacuation Pause) 200M->1M(202M) 1.529ms [0.130s][info][gc] GC(1) Pause Young (Normal) (G1 Evacuation Pause) 199M->1M(202M) 0.830ms [0.145s][info][gc] GC(2) Pause Young (Normal) (G1 Evacuation Pause) 199M->1M(202M) 0.897ms over! 75

这个性能优化开始很可观的!

注意,如果你的对象A包含对象B,且在产生A对象实例时,也产生了B对象实例,那么A,B对象都会在堆上分配

class YourObject{ int a; int b; } class MyObject{ int a; int b; YourObject obj = new YourObject(); } // .....省略 [0.136s][info][gc] GC(16) Pause Young (Normal) (G1 Evacuation Pause) 21M->1M(348M) 1.923ms [0.140s][info][gc] GC(17) Pause Young (Normal) (G1 Evacuation Pause) 21M->1M(348M) 0.226ms [0.142s][info][gc] GC(18) Pause Young (Normal) (G1 Evacuation Pause) 21M->1M(348M) 0.181ms [0.143s][info][gc] GC(19) Pause Young (Normal) (G1 Evacuation Pause) 21M->1M(348M) 0.190ms [0.149s][info][gc] GC(20) Pause Young (Normal) (G1 Evacuation Pause) 21M->1M(937M) 4.293ms [0.152s][info][gc] GC(21) Pause Young (Normal) (G1 Evacuation Pause) 21M->1M(937M) 0.488ms [0.153s][info][gc] GC(22) Pause Young (Normal) (G1 Evacuation Pause) 21M->1M(937M) 0.205ms [0.155s][info][gc] GC(23) Pause Young (Normal) (G1 Evacuation Pause) 21M->1M(937M) 0.254ms over! 83 Process finished with exit code 0
标量替换

-XX:+EliminateAllocations

可以自己试试,默认开启。如果关闭,只要是对象的创建就都在堆上了。(虽然前面说的是)

从我多次实验结果来看,-XX:+EliminateAllocations和-XX:+DoEscapeAnalysis一起开才有效果,否则均会触发GC,不知道为啥…本来以为是方法栈对象放不下,可是调大栈空间也是如此…

遗留的问题

-XX:+EliminateLocks除了减少时间,可以也减少GC,为啥咧?难道synchronized加锁的对象必须在堆上创建吗?

为啥-XX:+EliminateAllocations和-XX:+DoEscapeAnalysis一起开才有效果,关掉标量替换,对象都跑到堆里去分配了

最新回复(0)