全网最硬核 JVM TLAB 分析(单篇版不包含额外加菜) (2)

image

TLAB 是线程私有的,线程初始化的时候,会创建并初始化 TLAB。同时,在 GC 扫描对象发生之后,线程第一次尝试分配对象的时候,也会创建并初始化 TLAB。
TLAB 生命周期停止(TLAB 声明周期停止不代表内存被回收,只是代表这个 TLAB 不再被这个线程私有管理)在:

当前 TLAB 不够分配,并且剩余空间小于最大浪费空间限制,那么这个 TLAB 会被退回 Eden,重新申请一个新的

发生 GC 的时候,TLAB 被回收。

5. TLAB 要解决的问题以及带来的问题与解决方案的思考

TLAB 要解决的问题很明显,尽量避免从堆上直接分配内存从而避免频繁的锁争用。

引入 TLAB 之后,TLAB 的设计上,也有很多值得考虑的问题。

5.1. 引入 TLAB 后,会有内存孔隙问题,还可能影响 GC 扫描性能

出现孔隙的情况:

当前 TLAB 不够分配时,如果剩余空间小于最大浪费空间限制,那么这个 TLAB 会被退回 Eden,重新申请一个新的。这个剩余空间就会成为孔隙。

当发生 GC 的时候,TLAB 没有用完,没有分配的内存也会成为孔隙。

image

如果不管这些孔隙,由于 TLAB 仅线程内知道哪些被分配了,在 GC 扫描发生时返回 Eden 区,如果不填充的话,外部并不知道哪一部分被使用哪一部分没有,需要做额外的检查,那么会影响 GC 扫描效率。所以 TLAB 回归 Eden 的时候,会将剩余可用的空间用一个 dummy object 填充满。如果填充已经确认会被回收的对象,也就是 dummy object, GC 会直接标记之后跳过这块内存,增加扫描效率。但是同时,由于需要填充这个 dummy object,所以需要预留出这个对象的对象头的空间

5.2. 某个线程在一轮 GC 内分配的内存并不稳定

如果我们能提前知道在这一轮内每个线程会分配多少内存,那么我们可以直接提前分配好。但是,这简直是痴人说梦。每个线程在每一轮 GC 的分配情况可能都是不一样的:

不同的线程业务场景不同导致分配对象大小不同。我们一般会按照业务区分不同的线程池,做好线程池隔离。对于用户请求,每次分配的对象可能比较小。对于后台分析请求,每次分配的对象相对大一些。

不同时间段内线程压力并不均匀。业务是有高峰有低谷的,高峰时间段内肯定分配对象更多。

同一时间段同一线程池内的线程的业务压力也不一定不能做到很均匀。很可能只有几个线程很忙,其他线程很闲。

所以,综合考虑以上情况,我们应该这么实现 TLAB:

不能一下子就给一个线程申请一个比较大的 TLAB,而是考虑这个线程 TLAB 分配满之后再申请新的,这样更加灵活。

每次申请 TLAB 的大小是变化的,并不是固定的。

每次申请 TLAB 的大小需要考虑当前 GC 轮次内会分配对象的线程的个数期望

每次申请 TLAB 的大小需要考虑所有线程期望 TLAB 分配满重新申请新的 TLAB 次数

6. JVM 中的期望计算 EMA

在上面提到的 TLAB 大小设计的时候,我们经常提到期望。这个期望是根据历史数据计算得出的,也就是每次输入采样值,根据历史采样值得出最新的期望值。不仅 TLAB 用到了这种期望计算,GC 和 JIT 等等 JVM 机制中都用到了。这里我们来看一种 TLAB 中经常用到的 EMA(Exponential Moving Average 指数平均数) 算法:

image

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wppzys.html