解答:ES为什么建议使用32或者26GB的堆?并且了解JVM中的指针压缩设计。

结论

Elasticsearch推荐使用的堆大小不超过32GB,主要是因为Java的垃圾收集器(Garbage Collector)和对象指针压缩(Compressed Object Pointers)的工作方式。

  • 对象指针压缩:在64位的JVM中,如果堆大小小于32GB,JVM可以使用一种叫做"Compressed Ordinary Object Pointers"(COOP)的技术,将64位的指针压缩为32位。这样可以节省大量的内存,因为在JVM中,大部分的内存都是用来存储对象指针的。但是,一旦堆大小超过32GB,JVM就不能使用COOP,这会导致内存使用率显著增加。
  • 而推荐26GB堆的原因则是JVM内部内存计算并不是严格的1024进位,即1GB==1000MB,因此使用26GB可以确保使用到COOP技术。
  • 垃圾收集器:Java的垃圾收集器在处理大堆时,可能会导致长时间的停顿。这是因为垃圾收集器需要遍历整个堆来查找和清理无用的对象。如果堆太大,这个过程可能会花费很长时间,导致Elasticsearch无法响应请求。

因此,Elasticsearch推荐的堆大小是物理内存的一半,最大不超过32GB。这样可以在保持对象指针压缩和避免长时间的垃圾收集停顿之间找到一个平衡。

本文下方记录研究此问题的过程。

前提

  1. 针对ES文档设置堆大小产生疑问。
  2. 我想了解下JVM中的指针压缩设计,也就是上述疑问的答案。
  3. JVM自动帮开发者管理内存(开发者无需自行操作内存指针)。
  4. 指针压缩是JVM针对内存管理所做的优化技巧之一。

本文提到的JVM默认均为HotSpot JVM

本文使用到的JVM参数:

1
2
3
4
5
6
7
8
# 打开指针压缩,jdk7之后只要 -Xmx 的值少于32GB在64位机器上都默认打开
-XX:-UseCompressedOops 
# 打印指针压缩日志
-XX:+PrintCompressedOopsMode
# 指定对齐填充字节,默认对齐8字节
-XX:ObjectAlignmentInBytes=16
# 打开诊断JVM选项
-XX:+UnlockDiagnosticVMOptions

JVM对象结构

我们先看看JVM中的对象在内存中是如何表示的:

  1. 对象头(object header)
    1. mark word 存放了:
      • 锁信息(biased locking pattern, locking information)
      • hashCode(identity hashcode)
      • GC信息(GC metadata)
    2. 指向类的指针(klass wordJava7之前指向的是永久代,从Java8开始指向元空间
      1. 类名(class name)
      2. 修饰符(modifiers)
      3. 父类信息(superclass info)
    3. 数组长度(仅针对数组对象,4字节)
  2. 实例数据(instance data 或者 array data) 对象实际信息,比如字段值
  3. 对齐填充(alignment paddings) 占用32位4字节,对齐的设计是为了硬件友好。

同时在JVM中,使用Ordinary Object Pointers (OOPS)数据结构表示指向对象的指针。而指向对象与数组的指针的数据结构叫oopDesc。这个oopDesc包含了上面我们提到的两个内容:

  1. mark word
  2. klass word 这部分是可以被压缩的。

如下源码:

1
2
3
4
5
6
7
8
9
class oopDesc {
  friend class VMStructs;
 private:
  volatile markOop  _mark;
  union _metadata {
    Klass*      _klass;
    narrowKlass _compressed_klass;
  } _metadata;
}

以上,普通对象对应instanceOop,而数组对象对应arrayOop

32位机器到64位机器的变化

32位机器的一个限制:堆内存最大4GB。原因是内存只有2^32bit==2^10(KB)*2^10(MB)*2^10(GB)*4即4GB大小,OS级别也受此限制。而64位机器内存则高达TB级别。

在64位机器上。 普通对象长度至少16字节,其中包括:

  • mark word 8字节
  • klass word 4字节
  • padding对齐填充 4字节

数组对象长度至少16字节,其中包括:

  • mark word 8字节
  • klass word 4字节
  • 数组长度 4字节(32位的word

当我们从32位机器迁移到64位机器上时,肯定是希望性能更好,但是这个问题没有这么简单。

如上面的描述,我们的对象有指针(klass word),而指针在64位机器上占用了大约是32位机器上1.5倍的空间。同时也带来了新的问题:

  • 更多的内存消耗
  • 更频繁的GC(导致我们的应用线程占用更少的CPU时间片)

所以这引出了JVM内存优化的方案:指针压缩。

指针压缩

指针压缩即Compressed Oops,简称COOP

JVM默认帮我们开启了指针压缩,作用是在64位的机器上也可以存储32位的指针(对象头中的类型指针以及引用类型的字段),从而节省内存、提高性能。

在实现上,压缩的是oopDesc中的_metadata

64位转32位:encode;32位转64位:decode

encode过程的原理我们可以这么理解:

  1. 时间换空间(利用CPU计算少许逻辑,达到节省内存的作用),因为需要计算,所以叫做encode
  2. 由于JVM默认对齐填充8字节,所以我们的oops永远是8的倍数,而8的倍数使用二进制表示时,最后三位永远是0,所以我们多出来三位,decode时可以补三位零进行还原。

综上,64位机器上我们使用32位的空间,实际可以寻址到2^35bit==2^10(KB)*2^10(MB)*2^10(GB)*2^5==32GB。

所以使用更少的空间,我们可以寻址更多地址。

对应decode时,我们需要将其左移 3 位,再加上一个固定偏移量,便可以得到能够寻址 32GB 地址空间的伪 64 位指针了。

基于0虚拟地址的指针压缩

Zero-Based Compressed Ordinary Object Pointers (oops) Zero-Based指针压缩的意思就是从32位decode64位地址时无需加一个Java堆的基础地址。

当堆小于4GB时,JVM内可以使用一个字节的偏移量,避免使用一个对象的偏移量,这也就节省了一个8位的偏移量空间。同时将64位地址encode为32位地址时也非常高效。

Solaris, Linux, Windows这些系统上,堆空间小于26GB时,一般可以用到Zero-Based指针压缩(相比普通指针压缩更加高效)。

小结

通过本文,我们从理论上研究了JVM指针压缩以及基于0虚拟地址的指针压缩设计原理。

也明白了为什么ES建议JVM堆调优设置为32GB:使用指针压缩性能更好,而使用26GB时可以应用到基于0虚拟地址的指针压缩,性能更佳。

Ref