[TOC]
Java堆是被所有线程共享的一块内存区域,主要用于存放对象实例,在堆上为对象分配内存就是把一块大小确定的内存从堆内存中划分出来,将对象放进去。常用的分配方法有指针碰撞和空闲列表以及TLAB
指针碰撞
适用于堆内存完整的情况,已分配的内存和空闲内存分表在不同的一侧,通过一个指针指向分界点,当需要分配内存时,把指针往空闲的一端移动与对象大小相等的距离即可,用于Serial和ParNew等不会产生内存碎片的垃圾收集器。
空闲列表
适用于堆内存不完整的情况,已分配的内存和空闲内存相互交错,JVM通过维护一张内存列表记录可用的内存块信息,当分配内存时,从列表中找到一个足够大的内存块分配给对象实例,并更新列表上的记录,最常见的使用此方案的垃圾收集器就是CMS。
TLAB
全称: (Thread Local Allocation Buffer 线程本地分配缓存区)
在使用指针碰撞的场景下, 由于堆内存本身是线程共享的,在多线程场景下,当一个线程需要创建对象,这时指针还没来得及修改(指针是在新对象占完位之后才能进行修改),如果另一个线程也需要分配空间,就会造成两个对象空间冲突,TLAB可以解决这种场景.
TALB的思路
TALB的解决思路比较简单粗暴,既然是因为堆内存多线程共享引发了问题,那我就直接给每个线程一个私有的区域分配对象不就解决了吗?
TALB的原理
TALB就是在堆内存上额外为每个线程分配一块线程私有区域,其大小一般比较小,默认占Eden区的1%。其本质就是通过start、top、end三个指针实现,其中start和end分别指向这个TALB的开始和结尾位置,用于确定该TALB在堆上对应区域,避免其他线程再过来分配内存,top实时指向TALB区域内当前可分配的第一个位置,当一个TALB满了或剩余空间不足以存储新申请的对象时,线程会向JVM再申请一块TALB。
到这里为止,指针碰撞多线程问题似乎已经得到了解决,不过由于TALB空间本身较小(默认只占Eden区1%),所以就很容易出现TALB剩余区域不足以存储新对象的情况,这时线程会把新对象存到新申请的TALB中,这样原有的TALB中剩余区域就会被浪费,造成内存泄漏。那么如何解决内存泄漏呢?
最大浪费空间
由于TALB内存浪费现象较为严重,所以JVM开发人员提出了一个最大浪费空间对TALB进行约束。 当TALB剩余空间存不下新对象时,会进行一个判断: ① 如果当前TALB剩余空间小于最大浪费空间,则TALB所属线程会向JVM申请一个新的TALB区域存储新对象,如果依旧存储不下,则对象会放在Eden区创建。 ② 如果当前TALB剩余空间大于最大浪费空间,则对象直接去Eden区创建。
TALB的局限性
虽然TALB解决了指针碰撞在多线程场景下的问题,并且通过最大浪费空间可以减少内存泄漏,但其本身依旧有一些缺点: ① GC更频繁: 由于每个TALB所占用的空间都要比线程实际需要的空间大小大一些(因为不可能每个TALB都刚好存满,也就是TALB空间浪费更严重),所以一批对象直接存储在Eden区会比存储在TALB区占用更少的空间,进而容易引发Minor GC。 ② TALB允许内存浪费,会导致Eden区内存不连续。