TLAB的全称是Thread Local Allocation Buffer,翻译过来就是线程本地分配缓存。
1.TLAB是线程专用的内存分配区域,可以解决内存分配冲突问题。
在线程初始化时,同时也会申请一块指定大小的内存,只给当前线程使用,这样每个线程都单独拥有一个空间,如果需要分配内存,就在自己的空间上分配,这样就不存在竞争的情况,可以大大提升分配效率 TLAB空间的内存非常小,缺省情况下仅占有整个Eden空间的1%,也可以通过选项-XX:TLABWasteTargetPercent设置TLAB空间所占用Eden空间的百分比大小。
2. TLAB的本质
其实是三个指针管理的区域:start
,top
和 end
,每个线程都会从Eden分配一块空间,例如说100KB,作为自己的TLAB,其中 start 和 end 是占位用的,标识出 eden 里被这个 TLAB 所管理的区域,卡住eden里的一块空间不让其它线程来这里分配。TLAB只是让每个线程有私有的分配指针,但底下存对象的内存空间还是给所有线程访问的,只是其它线程无法在这个区域分配而已。 从这一点看,它被翻译为 线程私有分配区更为合理一点
当一个TLAB用满(分配指针top撞上分配极限end了),就新申请一个TLAB,而在老TLAB里的对象还留在原地什么都不用管——它们无法感知自己是否是曾经从TLAB分配出来的,而只关心自己是在eden里分配的。由于TALB空间本身较小(默认只占Eden区1%),所以就很容易出现TALB剩余区域不足以存储新对象的情况,这时线程会把新对象存到新申请的TALB中,这样原有的TALB中剩余区域就会被浪费,造成内存泄漏。那么如何解决内存泄漏呢?
3.最大浪费空间
由于TALB内存浪费现象较为严重,所以JVM开发人员提出了一个最大浪费空间对TALB进行约束。 当TALB剩余空间存不下新对象时,会进行一个判断:
- ① 如果当前TALB剩余空间小于最大浪费空间,则TALB所属线程会向JVM申请一个新的TALB区域存储新对象,如果依旧存储不下,则对象会放在Eden区创建。
- ② 如果当前TALB剩余空间大于最大浪费空间,则对象直接去Eden区创建
4. TLAB的局限性
虽然TALB解决了指针碰撞在多线程场景下的问题,并且通过最大浪费空间可以减少内存泄漏,但其本身依旧有一些缺点: ① GC更频繁: 由于每个TALB所占用的空间都要比线程实际需要的空间大小大一些(因为不可能每个TALB都刚好存满,也就是TALB空间浪费更严重),所以一批对象直接存储在Eden区会比存储在TALB区占用更多的空间,进而容易引发Minor GC。 ② TALB允许内存浪费,会导致Eden区内存不连续
5 拓展
5.1 创建对象的内存分配
- 编译器通过逃逸分析判断对象是在栈上分配还是堆上分配,如果是堆上分配则进入下一步。(开启逃逸分析需要设置jvm参数)
- 如果tlab可以放下该对象则在tlab上分配,否则进入下一步。
- 重新申请一个tlab,再尝试存放该对象,如果放不下则进入下一步。
- 在eden区加锁,尝试在eden区存放,若存放不下则进入下一步。
- 执行一次Young GC。
- Young GC后若eden区仍放不下该对象,则直接在老年代分配。
6测试
测试代码
* 测试TLAB
*/
public class TLABTest {
public static void get() {
byte[] bytes = new byte[2];
}
public static void main(String[] args) {
long start = System.currentTimeMillis();
for (int i = 0; i < 10000000; i++) {
get();
}
long end = System.currentTimeMillis();
System.out.println(end - start);
}
}
先测试禁用TLAB的情况:
调整虚拟机参数如下:
- -server:以server模式运行,支持逃逸分析参数DoEscapeAnalysis
- -XX:-DoEscapeAnalysis:关闭逃逸分析,避免出现栈上分配影响效果
- -XX:-BackgroundCompilation:禁止后台编译
- -XX:-UseTLAB:禁用TLAB 运行结果如下:
再测试启用TLAB的情况:
调整虚拟机参数如下:再测试启用TLAB的情况:
-XX:+UseTLAB:启用TLAB
运行结果如下:
对比两个时间,可以发现TLAB是否启用确实影响着对象分配的效率。