【译】JVM Anatomy Park #4: TLAB 分配

原文地址:JVM Anatomy Park #4: TLAB allocation

问题

TLAB 分配是什么?指针碰撞(Pointer-bump)分配又是什么?总之谁负责分配对象?

理论

当我们执行 new MyClass() 的时候,运行时环境将会为新建的对象分配内存。用于分配的 GC (内存管理器)示例接口非常简单:

 ref Allocate(T type);
 ref AllocateArray(T type, int size);

当然,由于编写内存管理器的语言通常与运行的语言不同(比如 JVM 运行 Java 语言,但是 HotSpot JVM 是用 C++ 编写的),所以代码中真实的接口比较晦涩。新建对象的 Java 代码需要转化为本地 VM 代码。这个过程的成本高么?可能会。内存管理器需要处理多线程并发申请内存的情况么?当然。

为了优化多线程并发申请的场景,我们一次性为线程分配整内存,用完了之后再向 VM 申请新的一块。在 Hotspot 中这些内存块被称为线程本地分配缓冲区(TLABs),为了支持基于内存块的分配又构建了一套复杂的机制。从空间上看,TLABs 是在线程本地的,这意味着 TLAB 就是接受本地线程分配的缓冲区。TLAB 仍然是 Java 堆的一部分,线程可以将本地新建对象的引用传递给 TLAB 外部的字段,等等。

所有知名的 OpenJDK GCs 都支持 TLAB 分配。这部分 VM 代码抽象的相当好。所有的 Hotspot 编译器都支持 TLAB 分配,对象分配逻辑生成的机器码通常是这样的:

0x00007f3e6bb617cc: mov    0x60(%r15),%rax        ; TLAB "current"
0x00007f3e6bb617d0: mov    %rax,%r10              ; tmp = current
0x00007f3e6bb617d3: add    $0x10,%r10             ; tmp += 16 (object size)
0x00007f3e6bb617d7: cmp    0x70(%r15),%r10        ; tmp > tlab_size?
0x00007f3e6bb617db: jae    0x00007f3e6bb61807     ; TLAB is done, jump and request another one
0x00007f3e6bb617dd: mov    %r10,0x60(%r15)        ; current = tmp (TLAB is fine, alloc!)
0x00007f3e6bb617e1: prefetchnta 0xc0(%r10)        ; ...
0x00007f3e6bb617e9: movq   $0x1,(%rax)            ; store header to (obj+0)
0x00007f3e6bb617f0: movl   $0xf80001dd,0x8(%rax)  ; store klass to (obj+8)
0x00007f3e6bb617f7: mov    %r12d,0xc(%rax)        ; zero out the rest of the object

分配逻辑内联在生成代码中,所以并不需要调用 GC 来分配对象。如果申请分配的对象耗尽了 TLAB,或者对象比 TLAB 还大,那么我们采用一条“慢路径”,要么在此处直接分配,要么返回一个新的 TLAB。绝大多数正常的分配逻辑仅仅是将 TLAB 当前的指针增加对象的大小,然后继续执行。

这就是这种分配机制有时被称为“指针碰撞分配(pointer bump allocation)”的原因。指针碰撞分配需要一块连续的内存用于分配,但是这又引入了内存压缩的需要。留意一下 CMS 在老年代是如何从空闲列表分配内存的,受益于指针碰撞分配的方式,使得并发清除成为可能。新生代更少的存活对象将会支付空闲列表分配的成本。

在实验中我们可以通过 -XX:-UseTLAB 关闭 TLAB 机制。所有的对象分配将会执行下述本地代码:

-   17.12%     0.00%  org.openjdk.All  perf-31615.map
   - 0x7faaa3b2d125
      - 16.59% OptoRuntime::new_instance_C
         - 11.49% InstanceKlass::allocate_instance
              2.33% BlahBlahBlahCollectedHeap::mem_allocate  <---- entry point to GC
              0.35% AllocTracer::send_allocation_outside_tlab_event

...但是通常来说这不是一个好主意。

实验

像往常一样,让我们构建一个实验来观察 TLAB 分配。因为所有 GC 实现都有这个特性,所以出于最小化运行环境影响的目的,我们采用实验性的 Epsilon GC。事实上它实现了分配,所为为实验提供了很好的研究平台。

迅速设计一个工作负载:分配 50M 个对象(为什么不呢?),运行在 JMH 的 SingleShot 模式,这样就不会做性能统计和分析。你也可以单独执行这样的测试,只是使用 SingleShot 实在太方便了。

@Warmup(iterations = 3)
@Measurement(iterations = 3)
@Fork(3)
@BenchmarkMode(Mode.SingleShotTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public class AllocArray {
    @Benchmark
    public Object test() {
        final int size = 50_000_000;
        Object[] objects = new Object[size];
        for (int c = 0; c < size; c++) {
            objects[c] = new Object();
        }
        return objects;
    }
}

这个测试用例在单线程中分配了 50M 个对象。基于经验,我们配置了 20GB 的堆内存,并且至少迭代执行6次。实验性的 -XX:EpsilonTLABSize 配置项用于精确控制 TLAB 的大小。其它 OpenJDK GC 采用自适应的 TLAB 大小调整策略,这种方式将会基于分配压力和其它因素调整 TLAB 的大小。

闲话少说,让我们看一下结果:

Benchmark                     Mode  Cnt     Score    Error   Units

# Times, lower is better                                            # TLAB size
AllocArray.test                 ss    9   548.462 ±  6.989   ms/op  #      1 KB
AllocArray.test                 ss    9   268.037 ± 10.966   ms/op  #      4 KB
AllocArray.test                 ss    9   230.726 ±  4.119   ms/op  #     16 KB
AllocArray.test                 ss    9   223.075 ±  2.267   ms/op  #    256 KB
AllocArray.test                 ss    9   225.404 ± 17.080   ms/op  #   1024 KB

# Allocation rates, higher is better
AllocArray.test:·gc.alloc.rate  ss    9  1816.094 ± 13.681  MB/sec  #      1 KB
AllocArray.test:·gc.alloc.rate  ss    9  2481.909 ± 35.566  MB/sec  #      4 KB
AllocArray.test:·gc.alloc.rate  ss    9  2608.336 ± 14.693  MB/sec  #     16 KB
AllocArray.test:·gc.alloc.rate  ss    9  2635.857 ±  8.229  MB/sec  #    256 KB
AllocArray.test:·gc.alloc.rate  ss    9  2627.845 ± 60.514  MB/sec  #   1024 KB

我们在单线程中实现 2.5 GB/sec 的分配速度。由于一个对象16字节,所以这意味着每秒 160 百万个对象。在多线程的工作负载下,分配速率可能达到每秒几十 GB。当然一旦 TLAB 变小,分配成本将会升高,分配速率将会下降。很不幸我们不能将 TLAB 设置为 1KB 以下,这是因为 Hotspot 的实现需要浪费一些空间,但是我们可以完全关闭 TLAB 机制,看一下对性能的影响:

Benchmark                      Mode  Cnt     Score   Error    Units

# -XX:-UseTLAB
AllocArray.test                  ss    9  2784.988 ± 18.925   ms/op
AllocArray.test:·gc.alloc.rate   ss    9   580.533 ±  3.342  MB/sec

我咧个去!分配速率下降了5倍,执行时间变大了10倍!这还是没有多线程并发申请内存的情况(可能会发生原子性竞争),或者是需要查找可用内存位置的场景(比如说尝试从空闲列表中分配)。对于 Epsilon 来说,分配的逻辑仅仅是一个 compare-and-set —— 因为它通过指针移动分配内存。如果你再增加一个线程 —— 总共两个执行线程 —— 而且关闭 TLAB,性能就更糟糕了:

Benchmark                            Mode  Cnt           Score       Error   Units

# TLAB = 4M (default for Epsilon)
AllocArray.test                        ss    9         407.729 ±     7.672   ms/op
AllocArray.test:·gc.alloc.rate         ss    9        4190.670 ±    45.909  MB/sec

# -XX:-UseTLAB
AllocArray.test                        ss    9        8490.585 ±   410.518   ms/op
AllocArray.test:·gc.alloc.rate         ss    9         422.960 ±    19.320  MB/sec

现在性能下降了20倍。随着线程数增加,性能将进一步下降!

观察

TLAB 是内存分配机制的骨干:它消除了分配器的并发瓶颈,提供了更快的分配方式,改善了整体的性能。有一种想法很有趣:TLAB将会造成更频繁的 GC,原因仅仅是分配的太快了!相反,如果内存管理器没有快速分配内存的方式,那么将会隐藏内存回收的性能问题。当我们对比内存管理器的时候,需要充分理解分配和回收两部分,以及两种之间的关系。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345

推荐阅读更多精彩内容

  • http://www.cnblogs.com/angeldevil/p/3801189.html值得一看 Clas...
    snail_knight阅读 1,409评论 1 0
  • 第二部分 自动内存管理机制 第二章 java内存异常与内存溢出异常 运行数据区域 程序计数器:当前线程所执行的字节...
    小明oh阅读 1,130评论 0 2
  • JVM架构 当一个程序启动之前,它的class会被类装载器装入方法区(Permanent区),执行引擎读取方法区的...
    cocohaifang阅读 1,646评论 0 7
  • 无意中,想起我还买过梁实秋的文集。散文、杂文、情书。转角处的旧书店,一块一本,买了三本。回头想想真的是值。 71岁...
    挂瓜阅读 310评论 1 3
  • 晚上和好友聊天,突然就聊到了人际关系这件事。他讲的很多让我感觉到很诧异,因为这些都是一些以前所不知道的。 有时候,...
    关宝宝阅读 886评论 0 1