1. 对象内存布局
在HotSpot虚拟机中,对象在内存中的存储包括三个部分:
- 对象头信息
- 实例数据信息
- 对齐填充信息
头信息(Header)
头信息是对象非常重要的身份信息,由对象标记和类型指针两部分组成,且大小固定。
- 对象标记:也就是MarkWord,是头信息中最重要的部分,记录着对象的各种锁状态、GC标记等。
- 类型指针:指向对象的类元数据的指针,可能存在指针压缩。
对头信息感兴趣可以参照这篇文章:详解Mark Word
实例数据(Instance Data)
记录着对象真正的数据,即代码中定义的各种类型的字段数据。
对齐填充(Padding)
HotSpot虚拟机要求对象在内存中的起始地址必须为8字节的整数倍,这就要求所有对象的大小必须为8字节的整数倍,因此当某个对象的数据长度不满足时,则通过此部分进行填充对齐。
2. 新对象内存分配
当我们要在堆上创建一个对象的时候,由于对象头信息的大小固定,根据要创建的对象的类型又可以拿到对象中各个字段的类型,从而确定实例数据的大小,再加上对齐填充部分会将最终长度填充到8字节的整数倍,这样就可以做到在创建对象之前就已经确定了对象的实际大小。
此时要做的,就是在堆上找到一块足够大的内存分配给这个对象。寻找分配内存的方式有指针碰撞和空闲列表两种。
2.1 指针碰撞(Bump The Pointer)
当内存排列规整、已占用内存总是从一侧依次排列的情况时,可以通过指针碰撞的方式寻找。
2.2 空闲列表(Free List)
当内存排列不规整、已占用内存和未占用内存互相交错的情况时,可以通过空闲列表的方式寻找。此时虚拟机维护一张空闲内存块的列表,当创建对象时,会从列表中找到一个足够大的内存块分配给对象。
2.3 组合使用
在实际环境中,内存分布很规整的情况很少,随着内存的分配与回收,内存碎片的存在非常常见,所以往往是通过指针碰撞和空闲列表结合使用的方式来进行管理。
3. TLAB(Thread Local Allocate Buffer)
3.1 含义
虚拟机管理着从操作系统申请到的一块堆内存,所有虚拟机内部的对象创建,都需要从这块堆内存中进行分配。而执行创建对象操作的请求可能来自于不同的线程,如果每个请求都直接从这一整块堆内存中进行分配的话,为了保证程序的正确,就需要频繁进行线程同步,这无疑是很浪费时间的。
为了解决这个问题,VM为每个线程都分配了一小块堆内存,这一小块堆内存归该线程私有,称为该线程的TLAB, 即 “线程本地分配缓存”。
在启用了TLAB的情况下,当线程需要创建对象时,会经过如下过程:
- 如果自身TLAB剩余空间足够,则直接从TLAB中进行分配,从而避免复杂的线程同步操作
- 如果自身TLAB剩余空间不足,检测此时TLAB剩余空间是否大于某个限定值(最大浪费空间限制)
- 如果小于限定值,将当前TLAB返还给虚拟机,然后虚拟机申请一块新的TLAB进行分配
- 如果大于限定值,保留当前TLAB,但本次直接从虚拟机持有的内存中进行分配
3.2 最大浪费空间限制
上面提到了一个限定值——最大浪费空间限制,为什么会有这个值,这个值的作用是什么呢?
当TLAB被某个线程返还给虚拟机的时候,绝大部分情况下,其中都会有一部分还没有被分配出去的内存,当整块TLAB被GC回收之前,这块空闲的内存不会再被利用,于是就成了被浪费掉的内存。
而设置最大浪费空间的意义就在于,当线程试图向虚拟机申请换新的TLAB之前,可以通过这个值来衡量当前要返还的TLAB剩余的空间是不是足够大。
如果剩余空间小于这个限定值,可以认为当前TLAB已经被利用的差不多了,这点剩下的空间,以后每次创建新对象可能都不够用了,于是可以给线程换一个新的。
但是如果剩余空间超过了限定值,那就认为当前TLAB还有足够的空间没被利用呢,只是这次创建的对象可能占地比较大,下回创建个别的对象的时候还能接着用,没必要换新的:你小子别败家,这个TLAB你留着下回接着用。
3.3 分配与回收时机
分配时机:
- 线程创建的时候
- 线程在GC结束后第一次申请内存时
- 线程创建对象时可能申请新的TLAB
回收时机
- GC时回收所有TLAB
- 线程创建对象时可能返还当前TLAB
3.4 TLAB的大小
理想状况下,我们希望:
- 在一个GC周期内,每个线程的创建对象工作都在自己的TLAB中完成,即TLAB足够大。
- 在GC开始时,每个线程的TLAB都几乎被分配完,即TLAB不会过大。
显然要完全达到上面两点是不可能的,但是可以通过算法结合历史经验数据对TLAB的大小做动态调整,从而使其分配的结果更加合理。
为此,JVM设计了一堆乱七八糟的公式,其中涉及到的参数也很多,比如线程数量,线程单个GC周期最多申请TLAB次数之类的。具体我也没看懂,有感兴趣的自己研究这篇文章吧:一箱关于TLAB的干货