1. 逃逸分析
1.1 什么是逃逸分析
逃逸分析,是一种可以有效减少Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。
1.2 逃逸的两种方式
- 方法逃逸:在一个方法体内,定义一个局部变量,而它可能被外部方法引用,比如作为调用参数传递给方法,或作为对象直接返回,可以理解成对象逃出了方法
- 线程逃逸:这个对象被其他线程访问到,比如赋值给了实例变量,并被其他线程访问到了,可以理解为对象逃出了当前线程
1.3 基于逃逸分析的优化
如果一个对象不会在方法体内,或线程内发生逃逸,那么我们可以对程序进行优化
- 栈上分配:一般情况下,有大量对象只在方法内使用,并不会发生逃逸,对象占用空间小,使用频率高,生命周期短,如果能使用栈上的空间,那么大量的对象将随方法的结束而销毁,减轻了GC压力
- 同步消除:如果类的方法上有同步锁,但在运行时却只有一个线程在访问,这时通过逃逸分析,将会去掉同步锁
- 标量替换:如果一个对象不会被外部访问,并且这个对象是可分解的,那程序真正执行的时候将可能不创建这个对象,而改为直接创建它的若干个被这个方法使用到的成员变量来代替。拆散后的变量便可以被单独分析与优化,可以各自分别在栈帧或寄存器上分配空间,原本的对象就无需整体分配空间了。
- 标量:Java虚拟机中的原始数据类型(int,long等数值类型以及reference类型等)都不能再进一步分解
- 聚合量:如果一个数据可以继续分解,那它称为聚合量,Java中最典型的聚合量是对象
2. TLAB(Thread Local Allocation Buffer)
2.1 为新生对象分配堆内存的两种方式
- 指针碰撞:在Java堆中的内存是规整的情况下,内存分配方式采用“指针碰撞”
假设JVM虚拟机上,堆内存都是规整的。堆内存被一个指针一分为二,指针的左边都被塞满了对象,指针的右变是未使用的区域。每一次有新的对象创建,指针就会向右移动一个对象size的距离。这就被称为指针碰撞。
如果我们多线程执行时,一个线程正在给A对象分配内存,指针还没有来的及修改,另一个线程为B对象分配内存的线程,而且还是引用这之前的指针指向,这个时候就出问题了
- 空闲列表:在Java堆中的内存不规整的情况下,内存分配方式采用“空闲列表”
假如Java堆中的内存不是规整的,已使用的内存和空闲的内存相互交错,这样就无法进行简单地指针碰撞了,这时候虚拟机会维护一张列表,列表中记录哪些内存块是可用的,在分配内存的时候就从列表中找到一块足够大的空间划分给对象实例,同时更新列表上的记录,这种内存分配的方式叫做“空闲列表”。
2.2 指针碰撞和空闲列表的优化方案
无论是指针碰撞还是空闲列表的内存分配策略,在多线程下进行Java堆内存分配的时候,可能存在正在给A对象分配内存,但指针还没来得及修改而对象B又使用了原来的指针来分配内存的这种情况。
优化方案
- 对分配内存空间的动作进行同步处理以保证操作的原子性,也就是说同一时刻只有一个线程能进行分配内存的操作。这个明显不符合现实,效率极低
- 把内存分配的操作按照线程划分在不同的空间中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer, TLAB),线程在自己的TLAB中分配内存,只有在TLAB用完了,在分配新的TLAB时才需要同步锁定
实际我们采用了TLAB方案。
2.2 TLAB(Thread-local allocation buffer)
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技术可以实现快速的对象分配,而不需要任何的锁进行同步,也就是说,在对象分配的时候不用锁住整个堆,而只需要在自己的缓冲区分配即可。
2.3 TLAB的缺点
- TLAB空间小时,大小是固定的
- TLAB允许小部分空间的浪费
3. 对象分配内存过程
编译器通过逃逸分析,确定对象是在栈上分配还是在堆上分配内存;通过TLAB方案,确定对象是在TLAB分配还是普通堆上分配。
- 如果没有逃逸,直接栈上分配内存。优势:没有对象所需要的对象头、字段,没有gc
- 如果发生逃逸
- 符合TLAB分配条件,在TLAB上分配内存。优势:线程私有,没有竞争,没有锁开锁,效率高
- 不符合TLAB分配条件,直接在堆上分配内存。优势:在堆上分配有锁的开销,效率低