今天,又是干货满满的一天。这是全网最硬核 JVM 系列的开篇,首先从 TLAB 开始。因为文章很长,每一个人阅读习惯不一样,因此特此拆成单篇版和多篇版git
- 全网最硬核 JVM TLAB 分析(单篇版不包含额外加菜)
- 全网最硬核 JVM TLAB 分析 1. 内存分配思想引入
- 全网最硬核 JVM TLAB 分析 2. TLAB生命周期与带来的问题思考
- 全网最硬核 JVM TLAB 分析 3. JVM EMA指望算法与TLAB相关JVM启动参数
- 全网最硬核 JVM TLAB 分析 4. TLAB 基本流程全分析
- 全网最硬核 JVM TLAB 分析 5. TLAB 源代码全解析
- 全网最硬核 JVM TLAB 分析 6. TLAB 相关热门Q&A汇总
- 全网最硬核 JVM TLAB 分析(额外加菜) 7. TLAB 相关 JVM 日志解析
- 全网最硬核 JVM TLAB 分析(额外加菜) 8. 经过 JFR 监控 TLAB
以前咱们提到了引入 TLAB 要面临的问题以及解决方式,根据这些咱们能够这么设计 TLAB。github
首先,TLAB 的初始大小,应该和每一个 GC 内须要对象分配的线程个数相关。可是,要分配的线程个数并不必定是稳定的,可能这个时间段线程数多,下个阶段线程数就不那么多了,因此,须要用 EMA 的算法采集每一个 GC 内须要对象分配的线程个数来计算这个个数指望。算法
接着,咱们最理想的状况下,是每一个 GC 内,全部用来分配对象的内存都处于对应线程的 TLAB 中。每一个 GC 内用来分配对象的内存从 JVM 设计上来说,其实就是 Eden 区大小。在 最理想的状况下,最好只有Eden 区满了的时候才会 GC,不会有其余缘由致使的 GC,这样是最高效的状况。Eden 区被用光,若是全都是 TLAB 内分配,也就是 Eden 区被全部线程的 TLAB 占满了,这样分配是最快的。数组
而后,每轮 GC 分配内存的线程个数以及大小是不必定的,若是一会儿分配一大块会形成浪费,若是过小则会频繁从 Eden 申请 TLAB,下降效率。这个大小比较难以控制,可是咱们能够限制每一个线程究竟在一轮 GC 内,最多从 Eden 申请多少次 TLAB,这样对于用户来讲更好控制。缓存
最后,每一个线程分配的内存大小,在每轮 GC 并不必定稳定,只用初始大小来指导以后的 TLAB 大小,显然不够。咱们换个思路,每一个线程分配的内存和历史有必定关系所以咱们能够从历史分配中推测,因此每一个线程也须要采用 EMA 的算法采集这个线程每次 GC 分配的内存,用于指导下次指望的 TLAB 的大小。markdown
综上所述,咱们能够得出这样一个近似的 TLAB 计算公式:oop
每一个线程 TLAB 初始大小 = Eden区大小
/ (线程单个 GC 轮次内最多从 Eden 申请多少次 TLAB
* 当前 GC 分配线程个数 EMA
)post
GC 后,从新计算 TLAB 大小 = Eden区大小
/ (线程单个 GC 轮次内最多从 Eden 申请多少次 TLAB
* 当前 GC 分配线程个数 EMA
)fetch
接下来,咱们来详细分析 TLAB 的整个生命周期的每一个流程。spa
线程初始化的时候,若是 JVM 启用了 TLAB(默认是启用的, 能够经过 -XX:-UseTLAB
关闭),则会初始化 TLAB,在发生对象分配时,会根据指望大小申请 TLAB 内存。同时,在 GC 扫描对象发生以后,线程第一次尝试分配对象的时候,也会从新申请 TLAB 内存。咱们先只关心初始化,初始化的流程图如 图08 所示:
初始化时候会计算 TLAB 初始指望大小。这涉及到了 TLAB 大小的限制:
MinTLABSize
指定以后的流程里面,不管什么时候,TLAB 的大小都会在这个 TLAB 的最小大小 到 TLAB 的最大大小 的范围内,为了不啰嗦,咱们不会再强调这个限制~~~!!! 以后的流程里面,不管什么时候,TLAB 的大小都会在这个 TLAB 的最小大小 到 TLAB 的最大大小 的范围内,为了不啰嗦,咱们不会再强调这个限制~~~!!! 以后的流程里面,不管什么时候,TLAB 的大小都会在这个 TLAB 的最小大小 到 TLAB 的最大大小 的范围内,为了不啰嗦,咱们不会再强调这个限制~~~!!! 重要的事情说三遍~
TLAB 指望大小(desired size) 在初始化的时候会计算 TLAB 指望大小,以后再 GC 等操做回收掉 TLAB 须要重计算这个指望大小。根据这个指望大小,TLAB 在申请空间的时候每次申请都会以这个指望大小做为基准的空间做为 TLAB 分配空间。
如 图08 所示,若是指定了 TLABSize,就用这个大小做为初始指望大小。若是没有指定,则按照以下的公式进行计算:
堆给TLAB的空间总大小
/(当前有效分配线程个数指望
*重填次数配置
)
如 图08 所示,接下来会计算TLAB 初始分配比例。
线程私有分配比例 EMA:与有效分配线程个数 EMA对应,有效分配线程个数 EMA是对于全局来讲,每一个线程应该占用多大的 TLAB 的描述,而分配比例 EMA 至关于对于当前线程应该占用的总 TLAB 空间的大小的一种动态控制。
初始化的时候,分配比例其实就是等于 1/当前有效分配线程个数
。图08 的公式,代入以前的计算 TLAB 指望大小的公式,消参简化以后就是1/当前有效分配线程个数
。这个值做为初始值,采集如线程私有的分配比例 EMA。
这些采集数据会用于以后的当前线程的分配比例的计算与采集,从而影响以后的当前线程 TLAB 指望大小。
TLAB 分配流程如 图09 所示。
若是启用了 TLAB(默认是启用的, 能够经过 -XX:-UseTLAB
关闭),则首先从线程当前 TLAB 分配内存,若是分配成功则返回,不然根据当前 TLAB 剩余空间与当前最大浪费空间限制大小进行不一样的分配策略。在下一个流程,就会提到这个限制到底是什么。
若是当前 TLAB 剩余空间大于当前最大浪费空间限制(根据 图08 的流程,咱们知道这个初始值为 指望大小/TLABRefillWasteFraction),直接在堆上分配。不然,从新申请一个 TLAB 分配。 为何须要最大浪费空间呢?
当从新分配一个 TLAB 的时候,原有的 TLAB 可能还有空间剩余。原有的 TLAB 被退回堆以前,须要填充好 dummy object。因为 TLAB 仅线程内知道哪些被分配了,在 GC 扫描发生时返回 Eden 区,若是不填充的话,外部并不知道哪一部分被使用哪一部分没有,须要作额外的检查,若是填充已经确认会被回收的对象,也就是 dummy object, GC 会直接标记以后跳过这块内存,增长扫描效率。反正这块内存已经属于 TLAB,其余线程在下次扫描结束前是没法使用的。这个 dummy object 就是 int 数组。为了必定能有填充 dummy object 的空间,通常 TLAB 大小都会预留一个 dummy object 的 header 的空间,也是一个 int[]
的 header,因此 TLAB 的大小不能超过int 数组的最大大小,不然没法用 dummy object 填满未使用的空间。
可是,填充 dummy 也形成了空间的浪费,这种浪费不能太多,因此经过最大浪费空间限制来限制这种浪费。
新的 TLAB 大小,取以下两个值中较小的那个:
当分配出来 TLAB 以后,根据 ZeroTLAB 配置,决定是否将每一个字节赋 0。在建立对象的时候,原本也要对每一个字段赋初始值,大部分字段初始值都是 0,而且,在 TLAB 返还到堆时,剩余空间填充的也是 int[] 数组,里面都是 0。因此其实能够提早填充好。而且,TLAB 刚分配出来的时候,赋 0 也能利用好 Allocation prefetch 的机制适应 CPU 缓存行(Allocation prefetch 的机制会在另外一个系列说明),因此能够经过打开 ZeroTLAB 来在分配 TLAB 空间以后马上赋 0。
直接从堆上分配是最慢的分配方式。一种状况就是,若是当前 TLAB 剩余空间大于当前最大浪费空间限制,直接在堆上分配。而且,还会增长当前最大浪费空间限制,每次有这样的分配就会增长 TLABWasteIncrement 的大小,这样在必定次数的直接堆上分配以后,当前最大浪费空间限制一直增大会致使当前 TLAB 剩余空间小于当前最大浪费空间限制,从而申请新的 TLAB 进行分配。
相关流程如 图10 所示,在 GC 前与 GC 后,都会对 TLAB 作一些操做。
在 GC 前,若是启用了 TLAB(默认是启用的, 能够经过 -XX:-UseTLAB
关闭),则须要将全部线程的 TLAB 填充 dummy Object 退还给堆,并计算并采样一些东西用于之后的 TLAB 大小计算。
首先为了保证本次计算具备参考意义,须要先判断是否堆上 TLAB 空间被用了一半以上,假设不足,那么认为本轮 GC 的数据没有参考意义。若是被用了一半以上,那么计算新的分配比例,新的分配比例 = 线程本轮 GC 分配空间的大小 / 堆上全部线程 TLAB 使用的空间,这么计算主要由于分配比例描述的是当前线程占用堆上全部给 TLAB 的空间的比例,每一个线程不同,经过这个比例动态控制不一样业务线程的 TLAB 大小。
线程本轮 GC 分配空间的大小包含 TLAB 中分配的和 TLAB 外分配的,从 图八、图九、图10 流程图中对于线程记录中的线程分配空间大小的记录就能看出,读取出线程分配空间大小减去上一轮 GC 结束时线程分配空间大小就是线程本轮 GC 分配空间的大小。
最后,将当前 TLAB 填充好 dummy object 以后,返还给堆。
若是启用了 TLAB(默认是启用的, 能够经过 -XX:-UseTLAB
关闭),以及 TLAB 大小可变(默认是启用的, 能够经过 -XX:-ResizeTLAB
关闭),那么在 GC 后会从新计算每一个线程 TLAB 的指望大小,新的指望大小 = 堆给TLAB的空间总大小 * 当前分配比例 EMA / 重填次数配置。而后会重置最大浪费空间限制,为当前 指望大小 / TLABRefillWasteFraction。