new · TLAB · 指针碰撞 · 快速路径 / 慢速路径 · refill_waste_limit · 清零 · mark word · 压缩类指针 · 字段重排 · best fit · 对象对齐 · 压缩指针 · 32 GB · JOL · 紧凑对象头 · JEP 450 / 519 / 534

new 一个对象的全过程:在自己的 TLAB 里挪一下指针,再按规则把字段排进 48 字节

new Order(...) 这一行背后有五步:确认类已加载、分内存、清零、写对象头、跑构造方法。它快,是因为每个线程在 Eden 里有一块自己专用的缓冲区(TLAB),分内存只是把一个指针往后挪,不用和别的线程抢。对象多大由三样东西决定:12 字节对象头,按大小重排、见缝插针的字段,最后补齐到 8 的倍数。实测 单线程 new 5000 万个 Order:开着 TLAB 用 290 ms,关掉要 5628 ms;关掉以后,两个线程一起分配,总吞吐和一个线程一样。Order 实测 48 字节,堆调到 32 GB 就变成 56 字节。

全景:一个对象从 new 到被回收 · 第 3 站(也涉及第 1、2、10 站)地图:J03 · 内存结构 + 对象创建、对象头与内存布局(第 1、2 个知识点,下一圈)上一圈:JVM 内存结构实验:JDK 17.0.20 和 21.0.12,2 核 ARM 实测

①为什么需要:new 是最频繁的操作,还要算得清占多少

在全景第 3 站,这一步要解决两件事。第一,快:下单高峰时,几十个 Tomcat 线程同时在 new 订单、DTO、字符串、集合节点,每秒几百万次分配;如果每次都要和别的线程抢锁,CPU 都会耗在排队上。第二,算得清:第 6 站要把 100 万个订单放进本地缓存,到底占 48 MB 还是 96 MB?堆从 31 GB 调到 32 GB,为什么反而更容易满?

5.8 ns

开着 TLAB,单线程 new 一个 Order 的平均时间(含构造方法) 实测

113 ns

关掉 TLAB,每次都走慢速路径 实测

48 字节

一个 Order:字段加起来 36 字节,对象头 12 字节 实测

56 字节

同一个 Order,堆设成 32 GB,压缩指针关掉以后 实测

需求上一圈(JVM 内存结构)讲了五块区域、栈帧里只放引用,并画了一张对象头的示意图。这一圈是“懂机制”,地图上的第 1、2 个知识点合成一圈来讲:new 这一行字节码之后 JVM 做了哪五步,TLAB 的快速路径和慢速路径,对象头里放了什么,字段为什么不按声明的顺序排,压缩指针为什么卡在 32 GB。

②没有它会怎样:所有线程抢同一个指针

Eden 是一整片连续的空闲内存。最朴素的分配方法是:堆里放一个指针 top,谁要 48 字节,就把 top 往后挪 48。单线程时这已经很快;可几十个线程同时挪同一个指针,就必须用 CAS 或者加锁,大家在同一个缓存行上排队。

没有 TLAB:一个 top,大家抢 Eden(共享) 对象对象对象 top 下单线程 A 下单线程 B CAS 失败,重来 每次 new 都碰同一个变量:多一个线程,并不多一份吞吐 实测(关掉 TLAB):1 个线程 5000 万个 5.6 s;2 个线程各 5000 万个 11.0 s 有 TLAB:先领一块,再在自己那块里挪 Eden A 的 top B 的 top 下单线程 A 下单线程 B 只挪自己的 只有领新 TLAB 时才碰共享的 Eden,几十 KB 才一次 实测:1 个线程 5000 万个 0.29 s;2 个线程各 5000 万个 0.31 s

还有一种更老的办法:像 C 的 malloc 一样维护一张空闲块列表,每次找一块够大的。它不要求空闲内存连续,但每次分配都要查表,比挪指针慢得多(③)。

不懂这一层时的典型误用:以为“new 很贵”,给订单、DTO 手写对象池。可分配本来只要几纳秒,池子反而让本该马上死掉的对象一直活着,被 GC 一次次复制,最后晋升进老年代(全景第 6、7 站),还得自己处理并发和脏数据。

③相邻方案:内存从哪来

做法怎么分前提谁在用
空闲列表记下所有空闲块,找一块够大的切下来不要求空闲内存连续C 的 malloc;HotSpot 的 CMS 老年代(JDK 14 已移除)
共享指针碰撞一个 top,挪一下就是分配;多线程用 CAS空闲内存必须连续,所以 GC 要会整理或复制HotSpot 在 TLAB 外分配时(慢速路径)
TLAB + 指针碰撞每个线程先领一块,再在自己那块里挪指针,不用 CAS同上,另外要容忍每块尾巴上的一点浪费HotSpot 默认(-XX:+UseTLAB),所有 GC 都用
根本不分配JIT 发现对象不逃逸,把字段拆成局部变量(标量替换)代码已被 C2 编译,对象确实不逃逸全景第 2 站;待做页“JIT、逃逸分析与常见的性能误区”

选择规则:年轻代对象多、死得快,用复制算法,所以空闲内存天然连续,指针碰撞就成立;再给每个线程一块 TLAB,连 CAS 都省了。本页学这一条,“根本不分配”留给 JIT 那一页。

④原理:五步、两条路、一个头、一套排法

先知道这些

  • new Order(...) 编译成三条字节码:new(分内存、得到一个还没初始化的对象),dup,invokespecial <init>(跑构造方法)。分内存和跑构造方法本来就是两步。
  • G1 的 Eden 是一组 region(堆小时每个 1 MB),TLAB 从当前那个 Eden region 里切出来。见G1 与 ZGC。
  • 引用(比如 String status)默认只占 4 字节,这叫压缩指针,前提是堆小于 32 GB。上一圈的坑 5 讲过现象,这一圈的 ④-5 讲为什么。

④-1new 的五步(HotSpot 的实现,规范只规定结果)

1类检查

new 指令带着常量池里的类名。第一次执行时类还没加载,先走类加载(全景第 1 站);加载过了就直接拿到它的元数据,里面记着实例多大:48 字节。

第一次慢,之后零成本

2分配

在当前线程的 TLAB 里把 top 往后挪 48。解释器和 JIT 都把这几条指令直接生成成机器码,不进 JVM 的 C++ 代码。放不下才走慢速路径(④-2)。

快速路径:几条指令

3清零

把对象头以外的部分全部写成 0。所以字段不赋值也能读到 0、false、null。TLAB 领来时默认不清零(ZeroTLAB 默认关),在这一步按对象清。

字段的默认值从这来

4写对象头

mark word 写成“无锁、年龄 0、还没有哈希”(0x…01),再写类指针,指向第 1 步那份元数据。到这里,GC 已经能认出这是一个 Order。

④-3 讲头里有什么

5构造方法

invokespecial <init>:先跑父类的构造方法,再按代码给字段赋值。跑完,引用被存进栈帧里的局部变量 o。

这一步才是“你的代码”

第 3、4 步和第 5 步之间,对象已经存在、字段全是 0。如果引用在第 5 步跑完之前就被别的线程看到,它会读到“半初始化”的对象。这就是双重检查锁要加 volatile 的原因(⑥ 坑 3,JMM 页的双重检查锁)。

④-2TLAB:快速路径和三条慢速路径

一个 Eden region(1 MB)里切出几块 TLAB starttopend 线程 A 的 TLAB 线程 B 的 TLAB 假对象(丢掉的尾巴) Eden 的 top领 TLAB、TLAB 外分配都挪它(CAS) new Order:48 字节top + 48 ≤ end ? 放得下 快速路径:top += 48没有锁、没有 CAS,几条指令 放不下 TLAB 还剩多少?和 refill_waste_limit 比 剩得少:尾巴填成假对象,领新 TLAB新 TLAB 从 Eden 的 top 用 CAS 切出来 剩得多:这一个对象到 TLAB 外分配保留 TLAB,下次小对象还能用 Eden 也没空了:Young GC全景第 5 站,GC 完从头再试 ≥ 半个 region 的大对象直接占 Humongous region(全景第 4 站) refill_waste_limit 初值 = TLAB 期望大小 ÷ 64(TLABRefillWasteFraction),对象在 TLAB 外分配得越多,它涨得越高,越容易换新的。 慢速路径在 JVM 的 C++ 代码里(MemAllocator),要从机器码调进运行时,所以比快速路径慢一两个数量级。

TLAB 多大?不固定(ResizeTLAB 默认开)。JVM 的目标是:一个线程在两次 GC 之间大约领 50 次 TLAB(100 ÷ (2 × TLABWasteTargetPercent),后者默认 1),所以分配越多的线程,TLAB 越大。本机用 -Xmx64m 启动、加 -Xlog:gc+tlab=trace,main 线程第一块 TLAB 的 desired_size 是 122 KB。

HotSpot:每个线程一块 TLAB线程私有的指针碰撞

  • 快速路径只有比较和加法,可以内联进 JIT 编译出的代码
  • 每几十 KB 才碰一次共享的 Eden,线程越多越显出好处
  • 每块 TLAB 的尾巴可能浪费掉(目标是不超过 1%),要填成假对象,让堆能被顺序扫描
  • 前提是空闲内存连续,所以年轻代必须用复制或整理的 GC

被放弃的:所有线程共用一个 top或者空闲列表

  • 没有浪费,实现简单
  • 每次 new 都是一次 CAS,几十个线程打同一个缓存行
  • 空闲列表还要找块、切块、合并碎片,分配和回收都更贵

决定胜负的约束:分配是 Java 程序里最频繁的操作,而且一定是多线程的。所以要把共享的那一步摊薄到几十 KB 才发生一次,用一点尾巴浪费换掉每一次的 CAS。

mark word(64 位,JDK 17) 未用 25 位 identityHashCode 31 位第一次算 hashCode 时才写进来 1 GC 年龄 4 位所以最多 15 偏向锁 1 位15 起默认关 锁状态 2 位01 = 无锁 630 加锁以后,mark word 整个被换成“指向锁记录或 ObjectMonitor 的指针 + 锁状态”,原来的哈希和年龄挪到别处保存。 锁的那几种状态在 synchronized 页讲过,这里不重复。 类指针 4 字节压缩类指针,默认开 元空间里的 Klass:Order字段表、方法表、实例大小 48 数组还要再加 4 字节长度,所以数组头是 16 字节

锁状态怎么在 mark word 里切换,见 synchronized 页 ④-1。偏向锁 JDK 15 默认关(JEP 374),JDK 18 把代码删了。

④-4字段怎么排:先大后小,见缝插针,最后补齐到 8

JDK 15 起 HotSpot 用这套规则(JDK-8237767):

mark word类指针 / 数组长度基本类型字段引用字段空隙或补齐

左边是“按声明顺序硬排”的结果:第一个 long 只能从 16 开始,对象头后面空出 4 字节;status 放完停在 52,还要再补 4 字节,一共 56。右边是 HotSpot 的排法,也是 ⑤ 里 JOL 实测的结果:skuId 塞进对象头后面的空隙,正好 48 字节,一个字节都不浪费。

④-5为什么对齐到 8,又为什么卡在 32 GB

字段里存的不是地址,是“第几个 8 字节” status 字段:4 字节n = 0x0F3A_1C20 × 8(左移 3 位) 真实地址 = 堆基址 + n × 8JIT 生成的就是一条移位(再加基址) String 对象起点一定是 8 的倍数 对齐到 8 → 地址低 3 位永远是 0,不用存 4 字节能数到 2³² 个位置,每个位置 8 字节 2³² × 8 = 32 GB 堆 ≥ 32 GB(实测 -Xmx32g 就算):数不过来了 压缩指针自动关掉,每个引用变成 8 字节 Order 48 → 56,它的 String 24 → 32

压缩指针 + 8 字节对齐HotSpot 64 位默认

  • 引用占 4 字节,引用多的对象明显更小:⑤ 里 String 是 24 字节,关掉后是 32
  • 对象更小,同样的 CPU 缓存装得下更多对象
  • 每次读写引用多一次移位,成本很低
  • 补齐会浪费几个字节;堆上限 32 GB

被放弃的:引用就是 8 字节真实地址堆 ≥ 32 GB 时只能这样

  • 没有解码,堆想多大就多大
  • 所有引用翻倍,同样的数据要多占一截堆

决定胜负的约束:Java 堆里大部分是小对象,引用占了很大一块,而绝大多数服务的堆不到 32 GB。“对齐到 8”本来就有(CPU 读未对齐的 long 更慢),顺手把它变成 3 位免费的地址空间。真要更大,可以把 -XX:ObjectAlignmentInBytes 设成 16,上限变成 64 GB,代价是每个对象补齐更多。

API最少要会的几行
// 依赖 org.openjdk.jol:jol-core:0.17
System.out.println(ClassLayout.parseInstance(order).toPrintable());   // 一个对象:每个字段在第几字节
System.out.println(GraphLayout.parseInstance(order).toFootprint());   // 连同它引用的对象一共多大

# 和这页有关的 JVM 参数(都是默认值,一般不用动)
-XX:+UseTLAB  -XX:+ResizeTLAB  -XX:+UseCompressedOops  -XX:ObjectAlignmentInBytes=8
-Xlog:gc+tlab=trace                  # 看每个线程的 TLAB 大小、领了几次、浪费多少
jcmd <pid> GC.class_histogram        # 每个类有多少个实例、一共多少字节

TLAB 外的分配怎么在线上发现、紧凑对象头(JDK 25 可开,27 默认)、JDK 8 和 17 的差别,放到第⑥步。

⑤亲眼看到:用 JOL 拆开一个 Order,再关掉 TLAB 计时

交易靶场里的 Order,和全景页是同一个类。--release 8 编译,JDK 17.0.20 运行,JDK 21.0.12 上的布局完全一样。完整代码:study-labs/java/jvm/jvm-object-new-layout →

Order.java六个字段,声明顺序是 long long int int long String
public class Order {
    long id;  long userId;  int skuId;  int qty;  long amountCents;  String status;
}
Order o = new Order(1001L, 1024L, 88, 2, 17600L, "PAID");
System.out.println(ClassLayout.parseInstance(o).toPrintable());

实验 1:Order 的布局,算过哈希以后,再把压缩指针关掉

$ java -cp … Layout                                  # 默认参数
OFF  SZ   TYPE             DESCRIPTION             VALUE
  0   8                    (object header: mark)   0x0000000000000001 (non-biasable; age: 0)
  8   4                    (object header: class)  0x01085040
 12   4   int              Order.skuId             88
 16   8   long             Order.id                1001
 24   8   long             Order.userId            1024
 32   8   long             Order.amountCents       17600
 40   4   int              Order.qty               2
 44   4   java.lang.String Order.status            (object)
Instance size: 48 bytes

identityHashCode = 3b0143d3
  0   8                    (object header: mark)   0x0000003b0143d301 (hash: 0x3b0143d3; age: 0)

java.lang.Long   Instance size: 24 bytes     # 12 头 + 4 空隙 + 8 的 long
int[3]           Instance size: 32 bytes     # 16 头(含长度)+ 12 + 4 补齐
Order@… footprint:  [B 24 + String 24 + Order 48 = 96

$ java -XX:-UseCompressedOops -cp … Layout
  8   4                    (object header: class)  0x010c5868      # 类指针还是 4 字节
 40   4   int              Order.qty               2
 44   4                    (alignment/padding gap)
 48   8   java.lang.String Order.status            (object)
Instance size: 56 bytes
Order@… footprint:  [B 24 + String 32 + Order 56 = 112

实验 2:new 100 万个,线程分配了多少;关掉 TLAB 计时

$ java -Xms1g -Xmx1g -cp target/classes …AllocCost 1
new 1,000,000 个 Order:线程分配了 48,000,048 字节,平均每个 48.0 字节
第 1 轮:1 个线程各 new 50,000,000 个 Order,用时 706 ms   # 还没被 JIT 编译完
第 3 轮:1 个线程各 new 50,000,000 个 Order,用时 290 ms

$ java -Xms1g -Xmx1g -XX:-UseTLAB -cp target/classes …AllocCost 1
第 3 轮:1 个线程各 new 50,000,000 个 Order,用时 5,628 ms
第 3 轮(第 2 轮相差不到 5%,第 1 轮含 JIT 预热)1 个线程 × 5000 万2 个线程 × 5000 万两个线程的总吞吐
开着 TLAB(默认)290 ms314 ms接近翻倍
-XX:-UseTLAB5,628 ms11,038 ms没有增加
到这里已经能交差了:你能用 JOL 量出一个类、一张对象图的真实大小,能说清 new 为什么便宜,能解释 Order 为什么是 48 字节、什么时候会变成 56。
已知还没处理的细节:字符串和包装类型让一个订单远大于 48 字节(坑 1),堆跨过 32 GB(坑 2),半初始化对象(坑 3),手写填充字段防伪共享(坑 4),中等大小的对象总在 TLAB 外分配(坑 5)。

⑥细节·规范·优化

第一圈

注意事项:不知道就会出事故

坑 1只按字段算内存,缓存一上线堆就满了← ④-4 头 + 对齐
现象
按字段估算“100 万个订单 × 36 字节 ≈ 36 MB”,按下面的账一算,一个 HashMap<Long, Order> 大约要 160 MB。
根源
每个对象都有 12 字节头和补齐。Map<Long, Order> 里每一项还有 32 字节的 HashMap.Node、24 字节的 Long 键、数组里 4 字节的槽;status 如果是解析出来的字符串,每个订单再加 48 字节。
做法
上线前用 GraphLayout.parseInstance(cache).toFootprint() 量一次;键用 long 的原生集合,状态用 enum 或 byte,重复的字符串用 intern 或常量。
坑 2堆从 31 GB 调到 32 GB,能放的东西反而少了← ④-5 32 GB
现象
加了 1 GB 堆,GC 更频繁了。
根源
本机实测 -Xmx31g 时 UseCompressedOops=true,-Xmx32g 时变成 false(堆还要让出开头的空页,所以 32g 整就过线了)。所有引用从 4 字节变 8 字节。
做法
要么不超过 31 GB,要么直接上到 48 GB 以上,把翻倍的引用开销吃回来。用 java -Xmx… -XX:+PrintFlagsFinal -version | grep UseCompressedOops 确认。
坑 3别的线程拿到了“字段还是 0”的对象← ④-1 第 3 步和第 5 步
现象
双重检查锁的单例,偶尔读到 null 的字段;构造方法里把 this 注册进监听器,回调时字段还没赋值。
根源
分配和清零在构造方法之前,对象“存在”的时候字段全是 0;没有 volatile 或 final 的保证,“把引用写出去”可能被重排到“给字段赋值”之前。
做法
单例用 volatile 或静态内部类;字段尽量 final;构造方法里不要把 this 交给别人。详见 JMM 页。
坑 4手写的填充字段被重排,防伪共享失效← ④-4 不按声明顺序
现象
两个计数器之间写了 long p1…p7 想隔开缓存行,压测还是慢。
根源
JVM 先把所有 long 排在一起,填充字段不一定在你以为的位置;没被读过的字段也可能被优化掉。
做法
用 @jdk.internal.vm.annotation.Contended(应用代码要加 -XX:-RestrictContended),或者用 JDK 的 LongAdder。伪共享本身见 JMM 下一圈。
坑 5几 KB 到几十 KB 的数组,一直在 TLAB 外分配← ④-2 剩得多就走 TLAB 外
现象
导出接口每次 new 一个 16 KB 的缓冲区,CPU 火焰图里 MemAllocator 那一层可能很显眼。
根源
对象比 TLAB 剩下的空间大,而剩下的又多到舍不得丢,就会在 TLAB 外用 CAS 分配,每次都走慢速路径。
做法
用 JFR 的 jdk.ObjectAllocationOutsideTLAB 事件找出是谁;缓冲区按线程复用,或者改成流式处理。大到半个 region 的,就是全景第 4 站的大对象。
第二圈

使用规范

大缓存上线前量一次

用 JOL 的 GraphLayout 或 jcmd GC.class_histogram 拿真实字节数,不要按字段估算。

坑 1。

堆不要设在 32~40 GB 之间

31 GB 以下,或者明显更大。

坑 2。

不给普通对象写对象池

分配只要几纳秒,池子会把短命对象变成长命对象。数据库连接、线程这种创建贵的才池化。

② 的误用。

能 final 就 final,构造方法里不发布 this

让“对象建好了”和“别人能看到它”之间有保证。

坑 3。

第三圈

深处:版本、紧凑对象头、实验台

这一页讲的东西JDK 8JDK 17 / 21JDK 25 / 27
偏向锁默认开15 起默认关(JEP 374),18 删掉代码没有了
字段布局算法旧算法15 起换成 ④-4 的新算法(JDK-8237767),少数类在 8 和 17 上大小不同同 17
关掉压缩指针时的对象头16 字节(类指针一起变 8 字节)12 字节(15 起两者独立,JDK-8241825)同 17
紧凑对象头无无(24 起可以实验性开启,JEP 450)25:产品特性,-XX:+UseCompactObjectHeaders 开(JEP 519);27:默认开(JEP 534)

紧凑对象头把类指针压到 22 位,塞进 mark word 的高位,对象头从 12 字节变成 8 字节。它省不省得下来,要看补齐:Order 的 4 字节空隙原本就被 skuId 填上了,省下的 4 字节又被补齐吃掉,还是 48;Long 从 24 变 16。这两个数是按 ④-4 的规则推算的,本机没有 JDK 25,没有实测。下面的实验台可以自己试。

布局实验台先猜,再排

对象头
引用(堆大小)
字段(点 × 删掉,点下面的按钮加)
先选一个剧情,猜一下,再点“排一下”。之后改开关和字段,结果会马上重排。
还没排。

排法就是 ④-4 那四条:基本类型从大到小,再放引用,每个字段放进最小的能放下的空位,最后补齐到 8;大小相同的字段这里按声明顺序,真实 JVM 不保证。同一个函数也能排数组(对象头 + 4 字节长度,再放元素)。在 node 里核对过:Order 48 / 56、OrderLite 40、Long 24、Object 16、int[3] 32,和 JOL 实测一致;8 字节对象头那一档是推算。

下一圈

↺回到⑤:改造你的案例

1. 给 Order 瘦身

按坑 1,把 status 换成 byte、qty 换成 short,几个金额和 ID 换成 int(代码里的 OrderLite)。先按 ④-4 自己排一遍,再用 JOL 核对。做对了:40 字节,而不是你以为的 32。

2. 量缓存

把 100 万个订单放进 HashMap<Long, Order>,status 用 new String("PAID"),用 GraphLayout 量总大小,再和 "PAID" 字面量的版本比。做对了:差出大约 48 MB。

3. 看 TLAB 日志

给 AllocCost 加 -Xlog:gc+tlab=debug,在 GC 日志里找每个线程的 TLAB 大小、领了几次(refills)、浪费比例(waste)。看哪个线程的 TLAB 最大,和它分配得多不多对得上吗。

1. 用一句话说出这一页的精华。

new 快,是因为每个线程在 Eden 里有自己的 TLAB,分配只挪一个指针,不用和别人抢;对象多大,等于 12 字节对象头加上按“先大后小、见缝插针”排好的字段,再补齐到 8,引用在堆小于 32 GB 时只占 4 字节。

2. 没有 TLAB 时,疼在哪?

每次 new 都要在共享的分配指针上做 CAS、并且走 JVM 的慢速路径:⑤ 实测单线程慢 19 倍,两个线程的总吞吐和一个线程一样(②、④-2)。

3. 关掉压缩指针后,Order 为什么是 56 而不是 52?根源是哪条原理?

④-4 的对齐规则:8 字节的 status 必须从 8 的倍数开始,44 那个 4 字节空位放不下,只能接到 48,结尾是 56;而且对象本身也要补齐到 8 的倍数,52 不是一个合法的大小。

4. 预测:在 Order 里再加一个 int version,对象变成多大?再加一个 boolean deleted 呢?

加 int:12 的空隙已经被 skuId 占了。三个 long 在 16~40,另外两个 int 在 40、44,status 接到 48,结尾 52,补齐到 56。再加 boolean:基本类型先排,它放在 48;status 要从 4 的倍数开始,挪到 52,结尾 56,还是 56,49~52 成了空隙(④-4 规则 1~4)。可以在实验台里核对。

5. 预测:一个线程在循环里不停 new 8 KB 的 byte[],其余都是小对象。TLAB 剩 20 KB 时会怎样?剩 1 KB 时呢?

设 TLAB 是 128 KB,refill_waste_limit 初值是 128 KB ÷ 64 = 2 KB。剩 20 KB:放得下,挪指针。剩 1 KB:放不下,而且 1 KB 小于 2 KB,舍得丢,把这 1 KB 填成假对象,领一块新 TLAB,在新的里分配。最麻烦的是剩 5 KB 这种:放不下 8 KB,又比 2 KB 多,舍不得丢,这个数组就在 TLAB 外用 CAS 分配,走慢速路径(④-2、坑 5)。

6. 放回全景:第 3 站前后各发生了什么?如果第 3 站的 TLAB 用得特别快(比如每个请求都 new 一堆大缓冲),整条链路会怎样?

前面是第 1 站(类加载,类指针指向的元数据从这来)和第 2 站(JIT:编译以后快速路径才是几条机器指令,逃逸分析能让部分 new 根本不发生);后面是第 4 站(太大的直接进 Humongous)和第 5 站(Eden 被 TLAB 一块块领完就触发 Young GC)。分配得快,Eden 就满得快,Young GC 更频繁;对象来不及死就被复制、提早晋升(第 6 站),老年代涨得快,第 7 站的并发标记更忙,最坏退化成第 8 站的 Full GC。