我们在学习java的过程中,一般认为一个new一个对象,肯定会分配在堆上,由于有了java的逃逸分析和TLAB(Thread local allocation buffer),可能这个结论并不一定那么准确。首先先来了解下java中new一个新对象的流程:
对象创建流程
1.如果开启了逃逸分析,如果分析出某个对象没有逃逸,则会尝试将对象分配在栈上,如果不成功,则会尝试2
2.尝试分配在TLAB上,不成功则继续3
3.是否满足进入老年代(对象是否足够大)
4.eden区分配
首先来了解下逃逸分析:
经过逃逸分析之后,可以得到三种对象的逃逸状态。
1. GlobalEscape(全局逃逸), 即一个对象的引用逃出了方法或者线程。例如,一个对象的引用是复制给了一个类变量,或者存储在在一个已经逃逸的对象当中,或者这个对象的引用作为方法的返回值返回给了调用方法。
2. ArgEscape(参数级逃逸),即在方法调用过程当中传递对象的应用给一个方法。这种状态可以通过分析被调方法的二进制代码确定。
3. NoEscape(没有逃逸),一个可以进行标量替换的对象。可以不将这种对象分配在传统的堆上。
编译器可以使用逃逸分析的结果,对程序进行一下优化。
1. 堆分配对象变成栈分配对象。一个方法当中的对象,对象的引用没有发生逃逸,那么这个方法可能会被分配在栈内存上而非常见的堆内存上。不需要gc回收对象,对象会随着线程一起消亡。
2. 消除同步。线程同步的代价是相当高的,同步的后果是降低并发性和性能。逃逸分析可以判断出某个对象是否始终只被一个线程访问,如果只被一个线程访问,那么对该对象的同步操作就可以转化成没有同步保护的操作,这样就能大大提高并发程度和性能。(vector的add()、StringBuffer的append())
3. 矢量替代。Java虚拟机中的原始数据类型(int,long等数值类型以及reference类型等)都不能再进一步分解,它们就可以称为标量。相对的,如果一个数据可以继续分解,那它称为聚合量,Java中最典型的聚合量是对象。如果逃逸分析证明一个对象不会被外部访问,并且这个对象是可分解的,那程序真正执行的时候将可能不创建这个对象,而改为直接创建它的若干个被这个方法使用到的成员变量来代替。拆散后的变量便可以被单独分析与优化,可以各自分别在栈帧或寄存器上分配空间,原本的对象就无需整体分配空间了。
下面一起看个逃逸分析的例子
public class EscapeAnalysisDemo {
public static void main(String[] args) {
escapeAnalysis();
}
private static void escapeAnalysis() {
Apple apple = new Apple();
Person person = new Person(apple);
}
public static class Person {
Apple apple;
public Person(Apple apple) {
this.apple = apple;
}
}
public static class Apple {
}
}
经过逃逸分析,返现Person和Apple都没有逃出escapeAnalysis()方法,因此编译器可以将这两个对象分配在栈上。
再看一个逃逸分析优化后的代码例子,代码如下:
public class EscapeAnalysis {
public static void main(String[] args) {
long start=System.currentTimeMillis();
for (int i=0;i<100000*100000*10;i++){
Foo foo=new Foo();
}
System.out.println(System.currentTimeMillis()-start);
}
public static class Foo{
private int x;
private static int counter;
public Foo() {
this.x=(++counter);
}
}
}
jvm参数设置如下:
没有开启逃逸分析:-server -verbose:gc -Xmx10m -Xms10m
运行结果:
....
[GC (Allocation Failure) 3548K->1500K(9728K), 0.0003464 secs]
[GC (Allocation Failure) 3548K->1500K(9728K), 0.0003518 secs]
[GC (Allocation Failure) 3548K->1500K(9728K), 0.0004652 secs]
[GC (Allocation Failure) 3548K->1500K(9728K), 0.0003770 secs]
[GC (Allocation Failure) 3548K->1500K(9728K), 0.0003569 secs]
[GC (Allocation Failure) 3548K->1500K(9728K), 0.0003963 secs]
[GC (Allocation Failure) 3548K->1500K(9728K), 0.0003718 secs]
[GC (Allocation Failure) 3548K->1500K(9728K), 0.0003799 secs]
7191
开启逃逸分析:-server -verbose:gc -Xmx10m -Xms10m -Xss1024m -XX:+DoEscapeAnalysis
结果:
[GC (Allocation Failure) 2048K->884K(9728K), 0.0012021 secs]
[GC (Allocation Failure) 2932K->1146K(9728K), 0.0011442 secs]
[GC (Allocation Failure) 3194K->1225K(9728K), 0.0010019 secs]
[GC (Allocation Failure) 3273K->1233K(9728K), 0.0006168 secs]
13
那么,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技术可以实现快速的对象分配,而不需要任何的锁进行同步,也就是说,在对象分配的时候不用锁住整个堆,而只需要在自己的缓冲区分配即可。
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区任然不足以存放当前对象,则直接分配到老年代。
TLAB相关参数
虚拟机针对体积不大的对象,会优先分配到tlab区域,因此就失去了分配到老年代的机会,jvm默认开启
-XX:-UseTLAB使用该参数,禁止TLAB
TLAB:thread local allocation buffer线程本地分配缓存,线程专用的内存分配区域,为了加速对象分配,每个线程都会对应一个TLAB,独享,用来避免多线程冲突,提高分配效率,TLAB一般不会太大,当大对象无法分配在TLAB上时,则会分配在堆上。
-XX:+UseTLAB 使用TLAb
-XX:TLABSize 设置TLAB的大小
-XX:TLABRefillWasterFraction 设置进入TLAB空间的单个对象大小,它是一个比例值,默认为64,即如果对象大于整个TLAB的1/64,则在堆上创建对象(线程专用的内存分配区域,为了加速对象分配,每个线程都会对应一个TLAB空间)
-XX:PrintTLAB 查看TLAB
-XX:ResizeTLAB自调整TLABRefillWasterFraction的阈值 【实例jvm20180616-TEST02.java】
-XX:+UseTLAB -XX:TLABSize=102400 -XX:TLABRefillWasteFraction=100 -XX:-DoEscapeAnalysis
开启逃逸分析后,如果满足逃逸分析,则会在TLAB上创建,代码示例如下:
public class Test02 {
static void alloc() {
byte[] bytes = new byte[2];
}
public static void main(String[] args) {
/**
* -XX:+UseTLAB -XX:TLABSize=102400 -XX:TLABRefillWasteFraction=100 - XX:+DoEscapeAnalysis
* 必须配置最后一个参数 禁用逃逸分析,
* 启用逃逸分析,表明所有的对象都可以在栈上分配
*/
long start = System.currentTimeMillis();
for (int i = 0; i < 1000000000; i++) {
alloc();
}
System.out.println("cost time :"+(System.currentTimeMillis() - start));
}
}
未开启逃逸分析:-XX:+UseTLAB -XX:TLABSize=102400 -XX:TLABRefillWasteFraction=100 -XX:-DoEscapeAnalysis
结果:cost time :4507
开启逃逸分析:-XX:+UseTLAB -XX:TLABSize=102400 -XX:TLABRefillWasteFraction=100 -XX:+DoEscapeAnalysis
结果:cost time :11
【参考博客】
1.https://blog.csdn.net/yangzl2008/article/details/43202969
2.http://www.importnew.com/23150.html
网友评论