歡迎光臨
每天分享高質量文章

TLAB 與堆可解析性

(給ImportNew加星標,提高Java技能)

 

編譯:唐尤華

鏈接:shipilev.net/jvm/anatomy-quarks/5-tlabs-and-heap-parsability/

 

1. 寫在前面

 

“[JVM 解剖公園][1]”是一個持續更新的系列迷你博客,閱讀每篇文章一般需要5到10分鐘。限於篇幅,僅對某個主題按照問題、測試、基準程式、觀察結果深入講解。因此,這裡的資料和討論可以當軼事看,不做寫作風格、句法和語意錯誤、重覆或一致性檢查。如果選擇採信文中內容,風險自負。

 

Aleksey Shipilёv,JVM 性能極客   

推特 [@shipilev][2]   

問題、評論、建議發送到 [[email protected]][3]

 

[1]:https://shipilev.net/jvm-anatomy-park

[2]:http://twitter.com/shipilev

[3]:[email protected]

 

 2. 問題

 

你是否遇到過無法申請大陣列 `int[]` 的情況?看起來沒有分配到任何地方,但仍然占據堆空間,儲存的內容像是垃圾資料?

 

3. 理論

 

按照 GC 理論,好的回收器具有一種非常重要的特性——堆可解析性,即無需複雜的元資料就可以解析物件、欄位等。例如在 OpenJDK 中,許多內部任務採取下麵這樣的簡單迴圈進行堆遍歷:

 

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

 

就像這樣!如果堆具備可解析性,可以從頭到尾分配一個連讀的物件流。雖然不是必備特性,但是可解析性能夠使 GC 實現、測試與除錯變得更容易。

 

從 [TLAB 機制][4]中可以知道,每個執行緒都有自己的當前 TLAB 可分配物件。從 GC 的角度看,這意味著宣告了整個 TLAB。GC 無法快速知道有哪些執行緒在那裡,它們是否正在操作 TLAB 游標?當前 TLAB 游標的值是什麼?執行緒可能把這些信息儲存在暫存器中不向外部展示( OpenJDK 並沒有這麼做)。因此,這裡的問題在於外部無法瞭解 TLAB 中到底發生了什麼。

 

[4]:https://shipilev.net/jvm/anatomy-quarks/4-tlab-allocation/

 

為了驗證當前是否正在遍歷 TLAB 中的一部分,希望最好能夠停止執行緒以避免 TLAB 發生變化,從而可以實現精確的堆遍歷。但這裡還有一個更便捷的技巧:為什麼不向堆中插入填充物件?這樣就可以讓堆變得可解析。也就是說,如果 TLAB 像下麵這樣:

 

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

 

我們可以停止執行緒,讓它們在 TLAB 剩餘空間分配一個 dummy 物件,這樣就可以使它們的堆變得可解析:

 

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

 

有什麼比 dummy 物件更好的選擇?當然,可以用 `int[]` 陣列。請註意,這種“放置”方法只分配了 array essay-header,堆處理機制會跳過陣列內容繼續完成接下來的工作。一旦執行緒恢覆在 TLAB 中分配物件,會像什麼都沒有發生一樣改寫之分配的填充的內容。

 

順便說一下,在移除物件的時候,堆遍歷程式也可以很好地處理填充物件,簡化堆清掃工作。

 

4. 實驗

 

能看到上面方案的執行效果嗎?當然可以。我們可以啟動很多執行緒,宣告各自的 TLAB。然後啟動單獨的執行緒耗盡 Java 堆,丟擲 `OutOfMemoryException` 並觸發 heap dump。

 

例如下麵這樣的代碼:

 

```java
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 l = new ArrayList<>();
    new Thread(() -> allocateAndDie(l)).start();
  }


  public static void allocateAndWait(CountDownLatch cdl) {
    Object o = new Object();  // 請求一個 TLAB 物件
    cdl.countDown();
    while (true) {
      try {
        Thread.sleep(1000);
      } catch (Exception e) {
        break;
      }
    }
    System.out.println(o); // 使用物件
  }


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

 

為了精確得到 TLAB 大小,可以使用 Epsilon GC 設置 `-Xmx1G -Xms1G -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC -XX:+HeapDumpOnOutOfMemoryError` 引數運行。這樣可以迅速失敗並生成 heap dump 檔案。

 

用 [Eclipse Memory Analyzer (MAT)][5] 打開 heap dump 檔案,可以看到下圖:

 

[5]:http://www.eclipse.org/mat/

 

```shell
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 展示無法訪問的物件。根據定義,這些填充物件是無法訪問的。它們出現在 heap dump 檔案中是因為在轉儲過程利用堆的可解析性進行了遍歷。這些物件實際上並不存在,好的分析器會把它們過濾出來。這樣就可以解釋為什麼1G heap dump 實際上只儲存了900MB物件。

 

5. 觀察

 

TLAB 很有趣,堆的可解析性一樣有趣。把二者結合有助瞭解一些內部工作機制,這是極好的。如果在運行中發現一些奇怪的結果,那麼你很可能正在探索更有趣的技巧!

    已同步到看一看
    赞(0)

    分享創造快樂