【译】JVM Anatomy Quark #24: 对象对齐

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

问题

  • Java 对象有对齐的限制么?
  • 我听说 Java 对象是 8 字节对齐的,是这样么?
  • 我可以通过调整对齐来改善压缩引用的性能么?

理论

许多硬件实现要求对数据的访问是对齐的,这使得 N 字节宽度数据的访问地址总是 N 的倍数。即使对数据的普通访问没用特殊要求,特殊操作(特别是原子操作)通常也有对齐约束。

举例来说,x86 平台通常接受未对齐的读写操作,跨越两个缓存行的未对齐 CAS 操作也可以工作,但是对齐可以提高吞吐量。其它平台可能会直接拒绝这样的原子操作,产生一个 SIGBUS 信号或者另外一种硬件异常。对于跨越多个缓存行的数据,x86 平台也不会保证访问的原子性,这可能发生在未对齐的情况下。另外对于大部分类型和明确声明 volatile 的访问,Java 规范要求保证访问的原子性。

所以对于 Java 对象中的 long 字段,这占用 8 字节内存,基于性能因素,我们需要保证 8 字节对齐。如果该字段是 volatile 的,那么基于正确性的因素,我们也需要 8 字节对齐。在一个简单的方法中[1],为了实现这一点,需要做两件事:对象内部字段的偏移应该 8 字节对齐,以及对象本身应该 8 字节对齐。java.lang.Long 实例就是这样的:[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 字节(这是 8 的倍数)的位置,并且对象 8 字节对齐。

即使没有字段需要被特殊对待,但是对象头部仍然需要原子性访问。从技术上讲,大部分 Java 对象可以 4 字节对齐,而不是 8 字节,然而实现这一目标所需的运行时工作非常巨大

所以在 Hotspot 中最小的对象对齐是 8 字节。不过,对齐可以更大吗?当然可以,有一个对应的 VM 参数:-XX:ObjectAlignmentInBytes。这带来两个后果,一个负面的,一个正面的。

实例大小变大

当然一旦对齐变大,就意味着每个对象平均的内存空间浪费将会增加。例如将对象对齐从 16 字节增加至 128 字节:

$ 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

见鬼,每个实例 128 字节,而只有 8 字节有用数据,这太浪费了。为什么有人会这么做?

压缩引用阈值被移动

(一语双关)

还记得《压缩引用》中的这张图吗?

我们可以通过引用移位对大于 4GB 的堆内存启用压缩引用。移位的大小依赖引用中有多少低位比特是零。也就是,对象是如何对齐的!默认 8 字节对齐,那么最低 3 位是零,所以移动 3 位,那么就有 2(32+3)字节 = 32 GB 压缩引用空间。如果 16 字节对齐,那么就有 2(32+4)字节 = 64 GB 压缩引用堆空间!

实验

所以对象对齐增加了实例大小,这增加了堆内存占用;但是可以在大堆内存上启用引用压缩,这减少了堆内存占用!依赖堆内存的结构。我们可以使用之前的测试用例,但是稍微自动化一下。

这个测试用例尝试识别容纳给定数量对象的最小堆内存:

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 堆的大内存机器上执行这个测试用例,这将会得出预期的结果。让我们从平均对象大小开始讨论。注意这些是特定测试中的平均对象大小,也就是分配的一些小 byte[] 数组。结果如下:

意料之中,增大对齐增加了平均对象大小:16 字节和 32 字节对齐“稍微”增加了对象大小,但是 64 字节大幅增加了平均对象大小。注意对象对齐基本上表示对象的最小大小,一旦最小值增加了,平均值也会增加。

正如我们在《压缩引用》中看到的,堆内存达到 32 GB 压缩引用将会失效。但是注意更大的对齐将会延迟这一限制,对齐越大,内存空间越大。例如,16 字节对齐,将会在压缩引用中移动 4 位,也就是 64GB 空间。32 字节对齐移动5 位,也就是 128GB 空间。[3]在这个测试中,某些情况下对齐变大导致的对象变大将会与压缩引用减少的空间占用相抵消。当然,当压缩引用最后被关闭时,对齐的成本就表现出来了。

在“最小堆大小”图中可以很清楚看到这种现象:

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 这两个阈值。注意 16 字节和 32 字节对齐在某些情况下如何占用更少堆内存,也就是借助更有效的引用编码。这个改善并不是普遍的:当 8 字节对齐足够或者压缩引用失败的情况下,更大的对齐将会浪费内存。

结论

对象对齐是一件很有趣的事情。虽然它会显著增加对象大小,但是一旦启用压缩引用,这又会降低整体的内存占用。有时候为了减少内存占用,增加一点儿对齐是有意义的 [原来如此!]。然而在许多情况下,这将会增加整体内存占用。需要对给定应用程序和给定数据集进行仔细研究,以确定调整对齐是否有好处。利器当慎用。


  1. 比较麻烦的方法是让内存管理器在不同的起始地址分配对象。这需要检查分配路径上的对象,从性能角度来看这很有趣
  2. 这个以及之后的内存表示都是用 JOL 生成的。
  3. 粗略计算:因为在 Hotspot 中 -XX:ObjectAlignmentInBytes 最大接受 256,这意味着最多移动 8 位,也就是 1024 GB 内存。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,688评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,559评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,749评论 0 226
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,581评论 0 191
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,741评论 3 271
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,684评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,122评论 2 292
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,847评论 0 182
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,441评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,939评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,333评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,783评论 2 236
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,275评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,830评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,444评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,553评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,618评论 2 249

推荐阅读更多精彩内容

  • 原文地址:JVM Anatomy Quark #23: Compressed References 问题 总之 J...
    袁世超阅读 998评论 0 1
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,028评论 1 32
  • 这篇文章是我之前翻阅了不少的书籍以及从网络上收集的一些资料的整理,因此不免有一些不准确的地方,同时不同JDK版本的...
    高广超阅读 15,398评论 3 83
  • 前言:依旧是可以说很良心了,今天了解了一下加密,才发现其中内容真的多,涉及具体的需求一般常用MD5,再进一步就是加...
    iOS_July阅读 625评论 0 0
  • 知道你不想再失败 知道你不想再失去一次机会 知道你看重它 还是睡吧 睡吧 不管出于哪方面的压力 放下它 眉头不要...
    世俗之外阅读 144评论 0 0