逃逸分析——编译优化技术(最前沿的优化技术之一)

在Java中,典型的对象不再堆上分配的情况有两种:TLAB和栈上分配。

  • 一、为什么不在堆上分配
    我们知道堆是由所有线程共享的,既然如此那它就是竞争资源,对于竞争资源,必须采取必要的同步,所以当使用new关键字在堆上分配对象时,是需要锁的。既然有锁,就必定存在锁带来的开销,而且由于是对整个堆加锁,相对而言锁的粒度还是比较大的,当对象频繁分配时,不免影响效率。

所以对于某些特殊情况,可以采取避免在堆上分配对象的办法,以提高对象创建和销毁的效率。

  • 二、TLAB分配
    JVM在内存新生代Eden Space中开辟了一小块区域,由线程私有,称作TLAB(Thread-local allocation buffer),默认设定为占用Eden Space的1%。在Java程序中很多对象都是小对象且用过即丢,它们不存在线程共享也适合被快速GC,所以对于小对象通常JVM会优先分配在TLAB上,并且TLAB上的分配由于是线程私有所以没有锁开销。因此在实践中分配多个小对象的效率通常比分配一个大对象的效率要高。

  • 也就是说,Java中每个线程都会有自己的缓冲区称作TLAB(Thread-local allocation buffer),每个TLAB都只有一个线程可以操作,TLAB结合bump-the-pointer技术可以实现快速的对象分配,而不需要任何的锁进行同步,也就是说,在对象分配的时候不用锁住整个堆,而只需要在自己的缓冲区分配即可。

  • 三、栈上分配
    JVM在Server模式下的逃逸分析可以分析出某个对象是否永远只在某个方法、线程的范围内,并没有“逃逸”出这个范围,逃逸分析的一个结果就是对于某些未逃逸对象可以直接在栈上分配,由于该对象一定是局部的,所以栈上分配不会有问题。

  • 四、对象非堆上分配的思想和启发
    对象不在堆上分配主要的原因还是堆是共享的,在堆上分配有锁的开销。无论是TLAB还是栈都是线程私有的,私有即避免了竞争(当然也可能产生额外的问题例如可见性问题),这是典型的用空间换效率的做法。
    在实践中,类似的做法还有很多,例如Hadoop中对于Map过程在节点的本地内存中处理,直到最后Reduce过程再合并数据。对于任务之间可以分解到不同线程、进程的情况,就可以采用类似的做法用空间换效率,对吞吐率的提升有很大帮助。

JVM即时编译器(晚期)的优化技术:公共子表达式消除,数组范围检查消除,方法内联,逃逸分析参考运行期优化。这里主讲逃逸分析。

1.逃逸分析:

逃逸分析的基本行为是:分析对象动态作用域:当一个对象在方法中被定义后,它可能被外部方法引用,作文参数传递到其他方法中,称为:方法逃逸。或者被线程访问,称为线程逃逸。 而如果一个对象不会逃逸到方法或者线程之外。就是别的方法无法通过任何途径访问到这个对象,这个对象或变量就可以进行一些高效优化。

逃逸分析,是一种可以有效减少Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法

优化方法:

编译器可以使用逃逸分析的结果,对程序进行一下优化。

  1. 堆分配对象变成栈分配对象。一个方法当中的对象,对象的引用没有发生逃逸,那么这个方法可能会被分配在栈内存上而非常见的堆内存上。

  2. 消除同步。线程同步的代价是相当高的,同步的后果是降低并发性和性能。逃逸分析可以判断出某个对象是否始终只被一个线程访问,如果只被一个线程访问,那么对该对象的同步操作就可以转化成没有同步保护的操作,这样就能大大提高并发程度和性能。

  3. 矢量替代。逃逸分析方法如果发现对象的内存存储结构不需要连续进行的话,就可以将对象的部分甚至全部都保存在CPU寄存器内,这样能大大提高访问速度。

2.TLAB

JVM在内存新生代Eden Space中开辟了一小块线程私有的区域,称作TLAB(Thread-local allocation buffer)。默认设定为占用Eden Space的1%。在Java程序中很多对象都是小对象且用过即丢,它们不存在线程共享也适合被快速GC,所以对于小对象通常JVM会优先分配在TLAB上,并且TLAB上的分配由于是线程私有所以没有锁开销。因此在实践中分配多个小对象的效率通常比分配一个大对象的效率要高。
也就是说,Java中每个线程都会有自己的缓冲区称作TLAB(Thread-local allocation buffer),每个TLAB都只有一个线程可以操作,TLAB结合bump-the-pointer技术可以实现快速的对象分配,而不需要任何的锁进行同步,也就是说,在对象分配的时候不用锁住整个堆,而只需要在自己的缓冲区分配即可。

3. Java对象分配的过程

  1. 编译器通过逃逸分析,确定对象是在栈上分配还是在堆上分配。如果是在堆上分配,则进入选项2.

  2. 如果tlab_top + size <= tlab_end,则在在TLAB上直接分配对象并增加tlab_top 的值,如果现有的TLAB不足以存放当前对象则3.

  3. 重新申请一个TLAB,并再次尝试存放当前对象。如果放不下,则4.

  4. 在Eden区加锁(这个区是多线程共享的),如果eden_top + size <= eden_end则将对象存放在Eden区,增加eden_top 的值,如果Eden区不足以存放,则5.

  5. 执行一次Young GC(minor collection)。

  6. 经过Young GC之后,如果Eden区任然不足以存放当前对象,则直接分配到老年代。

原文链接

阅读更多
上一篇关于泛型——java自动拆箱,装箱,遍历循环(foreach)的理解
下一篇HashMap和HashTable比较
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭
关闭