咱们在学习使用Java的过程当中,通常认为new出来的对象都是被分配在堆上,可是这个结论不是那么的绝对,经过对Java对象分配的过程分析,能够知道有两个地方会致使Java中new出来的对象并必定分别在所认为的堆上。这两个点分别是Java中的逃逸分析和TLAB(Thread Local Allocation Buffer)。本文首先对这二者进行介绍,然后对Java对象分配过程进行介绍。html
逃逸分析,是一种能够有效减小Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。经过逃逸分析,Java Hotspot编译器可以分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。java
在计算机语言编译器优化原理中,逃逸分析是指分析指针动态范围的方法,它同编译器优化原理的指针分析和外形分析相关联。当变量(或者对象)在方法中分配后,其指针有可能被返回或者被全局引用,这样就会被其余过程或者线程所引用,这种现象称做指针(或者引用)的逃逸(Escape)。算法
Java在Java SE 6u23以及之后的版本中支持并默认开启了逃逸分析的选项。Java的 HotSpot JIT编译器,可以在方法重载或者动态加载代码的时候对代码进行逃逸分析,同时Java对象在堆上分配和内置线程的特色使得逃逸分析成Java的重要功能。编程
Java Hotspot编译器使用的是多线程
Choi J D, Gupta M, Serrano M, et al. Escape analysis for Java[J]. Acm Sigplan Notices, 1999, 34(10): 1-19. 并发
Jong-Deok Choi, Manish Gupta, Mauricio Seffano,Vugranam C. Sreedhar, Sam Midkiff等在论文《Escape Analysis for Java》中描述的算法进行逃逸分析的。该算法引入了连通图,用连通图来构建对象和对象引用之间的可达性关系,并在次基础上,提出一种组合数据流分析法。因为算法是上下文相关和流敏感的,而且模拟了对象任意层次的嵌套关系,因此分析精度较高,只是运行时间和内存消耗相对较大。编程语言
绝大多数逃逸分析的实现都基于一个所谓“封闭世界(closed world)”的前提:全部可能被执行的,方法在作逃逸分析前都已经得知,而且,程序的实际运行不会改变它们之间的调用关系 。但当真实的 Java 程序运行时,这样的假设并不成立。Java 程序拥有的许多特性,例如动态类加载、调用本地函数以及反射程序调用等等,都将打破所谓“封闭世界”的约定。函数
不论是在“封闭世界”仍是在“开放世界”,逃逸分析,做为一种算法而非编程语言的存在,吸引了国内外大量的学者对其进行研究。在这里本文就不进行学术上了论述了,有须要的能够参见谷歌学术搜索:http://www.gfsoso.com/scholar?q=Escape%20Analysis。高并发
通过逃逸分析以后,能够获得三种对象的逃逸状态。性能
GlobalEscape(全局逃逸), 即一个对象的引用逃出了方法或者线程。例如,一个对象的引用是复制给了一个类变量,或者存储在在一个已经逃逸的对象当中,或者这个对象的引用做为方法的返回值返回给了调用方法。
ArgEscape(参数级逃逸),即在方法调用过程中传递对象的应用给一个方法。这种状态能够经过分析被调方法的二进制代码肯定。
NoEscape(没有逃逸),一个能够进行标量替换的对象。能够不将这种对象分配在传统的堆上。
编译器可使用逃逸分析的结果,对程序进行一下优化。
堆分配对象变成栈分配对象。一个方法当中的对象,对象的引用没有发生逃逸,那么这个方法可能会被分配在栈内存上而很是见的堆内存上。
消除同步。线程同步的代价是至关高的,同步的后果是下降并发性和性能。逃逸分析能够判断出某个对象是否始终只被一个线程访问,若是只被一个线程访问,那么对该对象的同步操做就能够转化成没有同步保护的操做,这样就能大大提升并发程度和性能。
矢量替代。逃逸分析方法若是发现对象的内存存储结构不须要连续进行的话,就能够将对象的部分甚至所有都保存在CPU寄存器内,这样能大大提升访问速度。
下面,咱们看一下逃逸分析的例子。
[java] view plain copy
class Main {
public static void main(String[] args) {
example();
}
public static void example() {
Foo foo = new Foo(); //alloc
Bar bar = new Bar(); //alloc
bar.setFoo(foo);
}
}
class Foo {}
class Bar {
private Foo foo;
public void setFoo(Foo foo) {
this.foo = foo;
}
}
在这个例子当中,咱们建立了两个对象,Foo对象和Bar对象,同时咱们把Foo对象的应用赋值给了Bar对象的方法。此时,若是Bar对在堆上就会引发Foo对象的逃逸,可是,在本例当中,编译器经过逃逸分析,能够知道Bar对象没有逃出example()方法,所以这也意味着Foo也没有逃出example方法。所以,编译器能够将这两个对象分配到栈上。
测试代码:
package com.yang.test2;
/**
* Created by yangzl2008 on 2015/1/29.
*/
class EscapeAnalysis {
private static class Foo {
private int x;
private static int counter;
public Foo() {
x = (++counter);
}
}
public static void main(String[] args) {
long start = System.nanoTime();
for (int i = 0; i < 1000 * 1000 * 10; ++i) {
Foo foo = new Foo();
}
long end = System.nanoTime();
System.out.println("Time cost is " + (end - start));
}
}
设置Idea JVM运行参数:
未开启逃逸分析设置为:
-server -verbose:gc
开启逃逸分析设置为:
-server -verbose:gc -XX:+DoEscapeAnalysis
在未开启逃逸分析的情况下运行状况以下:
[GC 5376K->427K(63872K), 0.0006051 secs]
[GC 5803K->427K(63872K), 0.0003928 secs]
[GC 5803K->427K(63872K), 0.0003639 secs]
[GC 5803K->427K(69248K), 0.0003770 secs]
[GC 11179K->427K(69248K), 0.0003987 secs]
[GC 11179K->427K(79552K), 0.0003817 secs]
[GC 21931K->399K(79552K), 0.0004342 secs]
[GC 21903K->399K(101120K), 0.0002175 secs]
[GC 43343K->399K(101184K), 0.0001421 secs]
Time cost is 58514571
开启逃逸分析的情况下,运行状况以下:
Time cost is 10031306
未开启逃逸分析时,运行上诉代码,JVM执行了GC操做,而在开启逃逸分析状况下,JVM并无执行GC操做。同时,操做时间上,开启逃逸分析的程序运行时间是未开启逃逸分析时间的1/5。
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技术能够实现快速的对象分配,而不须要任何的锁进行同步,也就是说,在对象分配的时候不用锁住整个堆,而只须要在本身的缓冲区分配便可。
关于对象分配的JDK源码能够参见JVM 之 Java对象建立[初始化]中对OpenJDK源码的分析。
编译器经过逃逸分析,肯定对象是在栈上分配仍是在堆上分配。若是是在堆上分配,则进入选项2.
若是tlab_top + size <= tlab_end,则在在TLAB上直接分配对象并增长tlab_top 的值,若是现有的TLAB不足以存放当前对象则3.
从新申请一个TLAB,并再次尝试存放当前对象。若是放不下,则4.
在Eden区加锁(这个区是多线程共享的),若是eden_top + size <= eden_end则将对象存放在Eden区,增长eden_top 的值,若是Eden区不足以存放,则5.
执行一次Young GC(minor collection)。
通过Young GC以后,若是Eden区任然不足以存放当前对象,则直接分配到老年代。
对象不在堆上分配主要的缘由仍是堆是共享的,在堆上分配有锁的开销。不管是TLAB仍是栈都是线程私有的,私有即避免了竞争(固然也可能产生额外的问题例如可见性问题),这是典型的用空间换效率的作法。