【译】JVM Anatomy Park #5: TLABs 与 堆可解析性

原文地址:JVM Anatomy Park #5: TLABs and Heap Parsability

问题

你曾经遇到过无法解释的大int[]数组么?这些并没有分配的对象仍然会消耗堆内存么?这里面有需要回收的对象么?

理论

在 GC 理论中,可靠的收集器需要维护一个重要的属性——堆的可解析性,也就是将堆塑造成可以解析对象、字段等的形式,而且不需要复杂的元数据的支持。举个例子,在 OpenJDK 中许多遍历堆的分析任务就像这样,仅仅是一个简单的循环:

HeapWord* cur = heap_start;
while (cur < heap_used) {
  object o = (object)cur;
  do_object(o);
  cur = cur + o->size();
}

就是这个样子!如果堆是可解析的,那么我们可以假设已经分配的堆从头到尾是一个连续的对象流。严格来说这并不是一个必要属性,但是这个属性使得 GC 实现、测试和调试更简单。

考虑线程本地分配缓冲区 (TLAB) 机制:现在每个线程有用于本地分配的 TLAB。从 GC 的角度理解,这意味着整个 TLAB 都被分配了。GC 很难知道本地线程在做什么操作:它们正在移动 TLAB 指针?总之 TLAB 指针当前在哪里?可能线程只是在本地寄存器维护指针(虽然在 OpenJDK 中并不是这样),而外部系统是看不到这个数据的。所以这里就有个问题:外部系统无法精确得知 TLAB 内部的情况。

我们可以通过停止线程来中止 TLAB 分配,然后精确的遍历堆内存。但是有一个更方便的技巧:为什么我们不通过插入填充对象的办法使得堆内存可解析呢?也就是,如果我们有下述 TLAB:

 ...........|===================           ]............
            ^                  ^           ^
        TLAB start        TLAB used   TLAB end

我们可以停止线程,然后请求线程在 TLAB 的空闲部分中分配填充对象,使得这部分堆内存可解析:

 ...........|===================!!!!!!!!!!!]............
            ^                  ^           ^
        TLAB start        TLAB used   TLAB end

选什么作为填充对象呢?当然是长度可变的对象。为什么不选int[]数组呢?注意,填充这种对象仅仅是写一个数组的头部,使得遍历堆内存的机制可以工作,跳过原来空闲的部分。一旦线程恢复 TLAB 分配,那么仅仅复写填充部分即可,就像什么都没发生过。

顺便说一下,这也简化了清理堆内存的过程。如果我们需要清理对象,那么只需要在原处复写填充对象即可,这样就能保持堆内存可以被正常的遍历。

实验

我们可以在实际的例子中观察到这个机制么?当然可以。我们起很多线程分配自己的 TLAB,然后主线程持续分配对象耗尽 Java 堆内存,最终程序将会以 OutOfMemoryException 崩溃,这会触发堆转储(heap dump)。

工作负载就像这样:

import java.util.*;
import java.util.concurrent.*;

public class Fillers {
  public static void main(String... args) throws Exception {
    final int TRAKTORISTOV = 300;
    CountDownLatch cdl = new CountDownLatch(TRAKTORISTOV);
    for (int t = 0 ; t < TRAKTORISTOV; t++) {
      new Thread(() -> allocateAndWait(cdl)).start();
    }
    cdl.await();
    List<Object> l = new ArrayList<>();
    new Thread(() -> allocateAndDie(l)).start();
  }

  public static void allocateAndWait(CountDownLatch cdl) {
    Object o = new Object();  // Request a TLAB
    cdl.countDown();
    while (true) {
      try {
        Thread.sleep(1000);
      } catch (Exception e) {
        break;
      }
    }
    System.out.println(o); // Use the object
  }

  public static void allocateAndDie(Collection<Object> c) {
    while (true) {
      c.add(new Object());
    }
  }
}

为了控制 TLAB 的大小,我们继续使用 Epsilon GC。启动参数为 -Xmx1G -Xms1G -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC -XX:+HeapDumpOnOutOfMemoryError,这样程序将会很快崩溃,并且输出堆转储。

Eclipse Memory Analyzer (MAT) 中打开这个堆转储——我非常喜欢这个工具——我们可以看到下述类直方图:

Class Name                                 |   Objects | Shallow Heap |
-----------------------------------------------------------------------
                                           |           |              |
int[]                                      |     1,099 |  814,643,272 |
java.lang.Object                           | 9,181,912 |  146,910,592 |
java.lang.Object[]                         |     1,521 |  110,855,376 |
byte[]                                     |     6,928 |      348,896 |
java.lang.String                           |     5,840 |      140,160 |
java.util.HashMap$Node                     |     1,696 |       54,272 |
java.util.concurrent.ConcurrentHashMap$Node|     1,331 |       42,592 |
java.util.HashMap$Node[]                   |       413 |       42,032 |
char[]                                     |        50 |       37,432 |
-----------------------------------------------------------------------

int[] 消耗了最多堆内存!这是我们的填充对象。当然,这个实验有一些注意事项。

首先,我们配置 Epsilon 具有静态的 TLAB 大小。高性能的收集器将会自适应 TLAB 的大小,当线程已经分配了一些对象,但是仍然占用很多 TLAB 内存的时候,这种自适应的机制将会最小化无效内存的占用。这也是不要设置太大 TLAB 的一个原因。如果设置了较大的 TLAB ,那么在一个持续分配对象的线程中仍然可能观察到填充对象,但是这并不是真正的对象。

然后,我们通过配置 MAT 来展示不可达的对象。从定义上来说,填充对象是不可达的。它们出现在堆转储中仅仅是堆可解析属性的副作用。这些对象并不是真的存在,一个成熟的堆转储分析器将会为你过滤掉这些对象——这也就是 900 MB 对象就能耗尽 1G 堆内存的一个原因。

观察

TLAB 很有趣,堆可解析性也很有趣。将两者组合在一起就更有趣了,有时候会泄漏一些内部的机制。如果你遇到一些令人惊讶的现象,那么你可能是看到一些聪明的小技巧了!

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

推荐阅读更多精彩内容

  • rljs by sennchi Timeline of History Part One The Cognitiv...
    sennchi阅读 7,120评论 0 10
  • 你好,我叫宦宇栋。 每一年春节,我总是会说一些不一样的祝福,添加上对方的名字,逐一发送。这不是特立独行或者标榜立异...
    与栋不同阅读 611评论 0 1
  • 佛有无量德号,其中一个叫“正遍知”,即正知且遍知。阿罗汉只有正知,而不能遍知。只有佛圆满通达一切,穷尽法性,故称“...
    简佛系阅读 600评论 5 10
  • 厨师到…… 看她认真的样子,我心里美美的,孩子长大了!
    春暖花开_63c8阅读 332评论 1 0