【譯】JVM Anatomy Quark #24: 對(duì)象對(duì)齊

原文地址:JVM Anatomy Quark #24: Object Alignment

問題

  • Java 對(duì)象有對(duì)齊的限制么?
  • 我聽說 Java 對(duì)象是 8 字節(jié)對(duì)齊的,是這樣么?
  • 我可以通過調(diào)整對(duì)齊來改善壓縮引用的性能么?

理論

許多硬件實(shí)現(xiàn)要求對(duì)數(shù)據(jù)的訪問是對(duì)齊的,這使得 N 字節(jié)寬度數(shù)據(jù)的訪問地址總是 N 的倍數(shù)。即使對(duì)數(shù)據(jù)的普通訪問沒用特殊要求,特殊操作(特別是原子操作)通常也有對(duì)齊約束。

舉例來說,x86 平臺(tái)通常接受未對(duì)齊的讀寫操作,跨越兩個(gè)緩存行的未對(duì)齊 CAS 操作也可以工作,但是對(duì)齊可以提高吞吐量。其它平臺(tái)可能會(huì)直接拒絕這樣的原子操作,產(chǎn)生一個(gè) SIGBUS 信號(hào)或者另外一種硬件異常。對(duì)于跨越多個(gè)緩存行的數(shù)據(jù),x86 平臺(tái)也不會(huì)保證訪問的原子性,這可能發(fā)生在未對(duì)齊的情況下。另外對(duì)于大部分類型和明確聲明 volatile 的訪問,Java 規(guī)范要求保證訪問的原子性。

所以對(duì)于 Java 對(duì)象中的 long 字段,這占用 8 字節(jié)內(nèi)存,基于性能因素,我們需要保證 8 字節(jié)對(duì)齊。如果該字段是 volatile 的,那么基于正確性的因素,我們也需要 8 字節(jié)對(duì)齊。在一個(gè)簡單的方法中[1],為了實(shí)現(xiàn)這一點(diǎn),需要做兩件事:對(duì)象內(nèi)部字段的偏移應(yīng)該 8 字節(jié)對(duì)齊,以及對(duì)象本身應(yīng)該 8 字節(jié)對(duì)齊。java.lang.Long 實(shí)例就是這樣的:[2]

$ java -jar jol-cli.jar internals java.lang.Long
# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
# Objects are 8 bytes aligned.
# Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
# Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]

java.lang.Long object internals:
 OFFSET  SIZE   TYPE DESCRIPTION              VALUE
      0     4        (object header)          01 00 00 00
      4     4        (object header)          00 00 00 00
      8     4        (object header)          ce 21 00 f8
     12     4        (alignment/padding gap)
     16     8   long Long.value               0
Instance size: 24 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total

value 字段位于偏移 16 字節(jié)(這是 8 的倍數(shù))的位置,并且對(duì)象 8 字節(jié)對(duì)齊。

即使沒有字段需要被特殊對(duì)待,但是對(duì)象頭部仍然需要原子性訪問。從技術(shù)上講,大部分 Java 對(duì)象可以 4 字節(jié)對(duì)齊,而不是 8 字節(jié),然而實(shí)現(xiàn)這一目標(biāo)所需的運(yùn)行時(shí)工作非常巨大。

所以在 Hotspot 中最小的對(duì)象對(duì)齊是 8 字節(jié)。不過,對(duì)齊可以更大嗎?當(dāng)然可以,有一個(gè)對(duì)應(yīng)的 VM 參數(shù):-XX:ObjectAlignmentInBytes。這帶來兩個(gè)后果,一個(gè)負(fù)面的,一個(gè)正面的。

實(shí)例大小變大

當(dāng)然一旦對(duì)齊變大,就意味著每個(gè)對(duì)象平均的內(nèi)存空間浪費(fèi)將會(huì)增加。例如將對(duì)象對(duì)齊從 16 字節(jié)增加至 128 字節(jié):

$ java -XX:ObjectAlignmentInBytes=16 -jar jol-cli.jar internals java.lang.Long

java.lang.Long object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00
      4     4        (object header)                           00 00 00 00
      8     4        (object header)                           c8 10 01 00
     12     4        (alignment/padding gap)
     16     8   long Long.value                                0
     24     8        (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 4 bytes internal + 8 bytes external = 12 bytes total
$ java -XX:ObjectAlignmentInBytes=128 -jar jol-cli.jar internals java.lang.Long

java.lang.Long object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00
      4     4        (object header)                           00 00 00 00
      8     4        (object header)                           a8 24 01 00
     12     4        (alignment/padding gap)
     16     8   long Long.value                                0
     24   104        (loss due to the next object alignment)
Instance size: 128 bytes
Space losses: 4 bytes internal + 104 bytes external = 108 bytes total

見鬼,每個(gè)實(shí)例 128 字節(jié),而只有 8 字節(jié)有用數(shù)據(jù),這太浪費(fèi)了。為什么有人會(huì)這么做?

壓縮引用閾值被移動(dòng)

(一語雙關(guān))

還記得《壓縮引用》中的這張圖嗎?

我們可以通過引用移位對(duì)大于 4GB 的堆內(nèi)存啟用壓縮引用。移位的大小依賴引用中有多少低位比特是零。也就是,對(duì)象是如何對(duì)齊的!默認(rèn) 8 字節(jié)對(duì)齊,那么最低 3 位是零,所以移動(dòng) 3 位,那么就有 2(32+3)字節(jié) = 32 GB 壓縮引用空間。如果 16 字節(jié)對(duì)齊,那么就有 2(32+4)字節(jié) = 64 GB 壓縮引用堆空間!

實(shí)驗(yàn)

所以對(duì)象對(duì)齊增加了實(shí)例大小,這增加了堆內(nèi)存占用;但是可以在大堆內(nèi)存上啟用引用壓縮,這減少了堆內(nèi)存占用!依賴堆內(nèi)存的結(jié)構(gòu)。我們可以使用之前的測(cè)試用例,但是稍微自動(dòng)化一下。

這個(gè)測(cè)試用例嘗試識(shí)別容納給定數(shù)量對(duì)象的最小堆內(nèi)存:

import java.io.*;
import java.util.*;

public class CompressedOopsAllocate {

  static final int MIN_HEAP = 0 * 1024;
  static final int MAX_HEAP = 100 * 1024;
  static final int HEAP_INCREMENT = 128;

  static Object[] arr;

  public static void main(String... args) throws Exception {
    if (args.length >= 1) {
      int size = Integer.parseInt(args[0]);
      arr = new Object[size];
      IntStream.range(0, size).parallel().forEach(x -> arr[x] = new byte[(x % 20) + 1]);
      return;
    }

    String[] opts = new String[]{
      "",
      "-XX:-UseCompressedOops",
      "-XX:ObjectAlignmentInBytes=16",
      "-XX:ObjectAlignmentInBytes=32",
      "-XX:ObjectAlignmentInBytes=64",
    };

    int[] lastPasses = new int[opts.length];
    int[] passes = new int[opts.length];
    Arrays.fill(lastPasses, MIN_HEAP);

    for (int size = 0; size < 3000; size += 30) {
      for (int o = 0; o < opts.length; o++) {
        passes[o] = 0;
        for (int heap = lastPasses[o]; heap < MAX_HEAP; heap += HEAP_INCREMENT) {
          if (tryWith(size * 1000 * 1000, heap, opts[o])) {
            passes[o] = heap;
            lastPasses[o] = heap;
            break;
          }
        }
      }

      System.out.println(size + ", " + Arrays.toString(passes).replaceAll("[\\[\\]]",""));
    }
  }

  private static boolean tryWith(int size, int heap, String... opts) throws Exception {
    List<String> command = new ArrayList<>();
    command.add("java");
    command.add("-XX:+UnlockExperimentalVMOptions");
    command.add("-XX:+UseEpsilonGC");
    command.add("-XX:+UseTransparentHugePages"); // faster this way
    command.add("-XX:+AlwaysPreTouch");          // even faster this way
    command.add("-Xmx" + heap + "m");
    Arrays.stream(opts).filter(x -> !x.isEmpty()).forEach(command::add);
    command.add(CompressedOopsAllocate.class.getName());
    command.add(Integer.toString(size));

    Process p = new ProcessBuilder().command(command).start();
    return p.waitFor() == 0;
  }
}

在可以分配 100+ GB 堆的大內(nèi)存機(jī)器上執(zhí)行這個(gè)測(cè)試用例,這將會(huì)得出預(yù)期的結(jié)果。讓我們從平均對(duì)象大小開始討論。注意這些是特定測(cè)試中的平均對(duì)象大小,也就是分配的一些小 byte[] 數(shù)組。結(jié)果如下:

意料之中,增大對(duì)齊增加了平均對(duì)象大?。?6 字節(jié)和 32 字節(jié)對(duì)齊“稍微”增加了對(duì)象大小,但是 64 字節(jié)大幅增加了平均對(duì)象大小。注意對(duì)象對(duì)齊基本上表示對(duì)象的最小大小,一旦最小值增加了,平均值也會(huì)增加。

正如我們?cè)?a target="_blank" rel="nofollow">《壓縮引用》中看到的,堆內(nèi)存達(dá)到 32 GB 壓縮引用將會(huì)失效。但是注意更大的對(duì)齊將會(huì)延遲這一限制,對(duì)齊越大,內(nèi)存空間越大。例如,16 字節(jié)對(duì)齊,將會(huì)在壓縮引用中移動(dòng) 4 位,也就是 64GB 空間。32 字節(jié)對(duì)齊移動(dòng)5 位,也就是 128GB 空間。[3]在這個(gè)測(cè)試中,某些情況下對(duì)齊變大導(dǎo)致的對(duì)象變大將會(huì)與壓縮引用減少的空間占用相抵消。當(dāng)然,當(dāng)壓縮引用最后被關(guān)閉時(shí),對(duì)齊的成本就表現(xiàn)出來了。

在“最小堆大小”圖中可以很清楚看到這種現(xiàn)象:

Here, we clearly see the 32 GB and 64 GB failure thresholds. Notice how 16-byte and 32-byte alignment took less heap in some configurations, piggybacking on more efficient reference encoding. That improvement is not universal: when 8-byte alignment is enough or when compressed references fail, higher alignments waste memory.

在這里我們可以清楚看到 32 GB 和 64 GB 這兩個(gè)閾值。注意 16 字節(jié)和 32 字節(jié)對(duì)齊在某些情況下如何占用更少堆內(nèi)存,也就是借助更有效的引用編碼。這個(gè)改善并不是普遍的:當(dāng) 8 字節(jié)對(duì)齊足夠或者壓縮引用失敗的情況下,更大的對(duì)齊將會(huì)浪費(fèi)內(nèi)存。

結(jié)論

對(duì)象對(duì)齊是一件很有趣的事情。雖然它會(huì)顯著增加對(duì)象大小,但是一旦啟用壓縮引用,這又會(huì)降低整體的內(nèi)存占用。有時(shí)候?yàn)榱藴p少內(nèi)存占用,增加一點(diǎn)兒對(duì)齊是有意義的 [原來如此!]。然而在許多情況下,這將會(huì)增加整體內(nèi)存占用。需要對(duì)給定應(yīng)用程序和給定數(shù)據(jù)集進(jìn)行仔細(xì)研究,以確定調(diào)整對(duì)齊是否有好處。利器當(dāng)慎用。


  1. 比較麻煩的方法是讓內(nèi)存管理器在不同的起始地址分配對(duì)象。這需要檢查分配路徑上的對(duì)象,從性能角度來看這很有趣。
  2. 這個(gè)以及之后的內(nèi)存表示都是用 JOL 生成的。
  3. 粗略計(jì)算:因?yàn)樵?Hotspot 中 -XX:ObjectAlignmentInBytes 最大接受 256,這意味著最多移動(dòng) 8 位,也就是 1024 GB 內(nèi)存。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 原文地址:JVM Anatomy Quark #23: Compressed References 問題 總之 J...
    袁世超閱讀 1,210評(píng)論 0 1
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對(duì)...
    cosWriter閱讀 11,680評(píng)論 1 32
  • 這篇文章是我之前翻閱了不少的書籍以及從網(wǎng)絡(luò)上收集的一些資料的整理,因此不免有一些不準(zhǔn)確的地方,同時(shí)不同JDK版本的...
    高廣超閱讀 16,061評(píng)論 3 83
  • 前言:依舊是可以說很良心了,今天了解了一下加密,才發(fā)現(xiàn)其中內(nèi)容真的多,涉及具體的需求一般常用MD5,再進(jìn)一步就是加...
    iOS_July閱讀 717評(píng)論 0 0
  • 知道你不想再失敗 知道你不想再失去一次機(jī)會(huì) 知道你看重它 還是睡吧 睡吧 不管出于哪方面的壓力 放下它 眉頭不要...
    世俗之外閱讀 221評(píng)論 0 0

友情鏈接更多精彩內(nèi)容