[Java JVM] Hotspot GC研究- 64位引用指针压缩技术

为什么需要指针压缩

在上一篇文章 [Java JVM] Hotspot GC研究- 开篇&对象内存布局 中介绍对象内存布局时, 曾提到过, 由于在64位CPU下, 指针的宽度是64位的, 而实际的heap区域远远用不到这么大的内存, 使用64bit来存对象引用会造成浪费, 所以应该做点事情来节省资源.


如何做

基于以下事实:

  • CPU 使用的虚拟地址是64位的, 访问内存时, 必须使用64位的指针访问内存对象
  • java对象是分配于具体的某个内存位置的, 对其访问必须使用64位地址
  • 对java对象内的引用字段进行访问时, 必须经过虚拟机这一层, 操作某个对象引用不管是getfield还是putfield, 都是由虚拟机来执行. 或者简单来说, 要改变java对象某个引用字段, 必须经过虚拟机的参与.

细心的你从上面一定可以看出一点线索, 由于存一个对象引用和取一个对象引用必须经过虚拟机, 所以完全可以在虚拟机这一层做些手脚. 对于外部来说, putfield提供的对象地址是64位的, 经过虚拟机的转换, 映射到32位, 然后存入对象; getfield指定目标对象的64位地址和其内部引用字段的偏移, 取32位的数据, 然后反映射到64位内存地址. 对于外部来说, 只看见64位的对象放进去, 拿出来, 内部的转换是透明的.


详细实现

请看代码:
hotspot/src/share/vm/oops/oop.hpp

// In order to put or get a field out of an instance, must first check
// if the field has been compressed and uncompress it.
oop oopDesc::obj_field(int offset) const {
  return UseCompressedOops ?
    load_decode_heap_oop(obj_field_addr<narrowOop>(offset)) :
    load_decode_heap_oop(obj_field_addr<oop>(offset));
}

void oopDesc::obj_field_put(int offset, oop value) {
  UseCompressedOops ? oop_store(obj_field_addr<narrowOop>(offset), value) :
                      oop_store(obj_field_addr<oop>(offset),       value);
}

//补充oop和narrowOop的定义
typedef juint                   narrowKlass;
....
typedef class oopDesc*          oop;

当存取对象引用时, 首先会检查是否开启了指针压缩(UseCompressedOops), 然后调用不同的函数来处理. 我们来看:

//模板函数, 如果T是oop, 则访问的是8字节; 如果是narrowKlass, 则访问的是4字节
template <class T> T* oopDesc::obj_field_addr(int offset) const { return (T*)  field_base(offset); }

//模板函数, 这里有两个分支, 核心的转换函数是oopDesc::encode_store_heap_oop(p, v);
template <class T> void oop_store(T* p, oop v) {
  if (always_do_update_barrier) {
    oop_store((volatile T*)p, v);
  } else {
    update_barrier_set_pre(p, v);
    oopDesc::encode_store_heap_oop(p, v);
    // always_do_update_barrier == false =>
    // Either we are at a safepoint (in GC) or CMS is not used. In both
    // cases it's unnecessary to mark the card as dirty with release sematics.
    update_barrier_set((void*)p, v, false /* release */);  // cast away type
  }
}

//压缩指针版本, 调用了压缩函数
// Encode and store a heap oop allowing for null.
void oopDesc::encode_store_heap_oop(narrowOop* p, oop v) {
  *p = encode_heap_oop(v);
}

//判断null, 否则压缩
narrowOop oopDesc::encode_heap_oop(oop v) {
  return (is_null(v)) ? (narrowOop)0 : encode_heap_oop_not_null(v);
}

//核心压缩函数, 对象地址与base地址的差值, 再做移位
narrowOop oopDesc::encode_heap_oop_not_null(oop v) {
  assert(!is_null(v), "oop value can never be zero");
  assert(check_obj_alignment(v), "Address not aligned");
  assert(Universe::heap()->is_in_reserved(v), "Address not in heap");
  address base = Universe::narrow_oop_base();
  int    shift = Universe::narrow_oop_shift();
  uint64_t  pd = (uint64_t)(pointer_delta((void*)v, (void*)base, 1));
  assert(OopEncodingHeapMax > pd, "change encoding max if new encoding");
  uint64_t result = pd >> shift;
  assert((result & CONST64(0xffffffff00000000)) == 0, "narrow oop overflow");
  assert(decode_heap_oop(result) == v, "reversibility");
  return (narrowOop)result;
}

//核心解压缩函数, 压缩函数反过来, base地址加上对象起始地址的偏移
oop oopDesc::decode_heap_oop_not_null(narrowOop v) {
  assert(!is_null(v), "narrow oop value can never be zero");
  address base = Universe::narrow_oop_base();
  int    shift = Universe::narrow_oop_shift();
  oop result = (oop)(void*)((uintptr_t)base + ((uintptr_t)v << shift));
  assert(check_obj_alignment(result), "address not aligned: " INTPTR_FORMAT, p2i((void*) result));
  return result;
}

//普通指针encode版本, 直接解引用进行赋值
static inline void encode_store_heap_oop(oop* p, oop v) { *p = v; }

//普通指针decode版本, 直接返回值
static inline oop decode_heap_oop(oop v) { return v; }

从上面的代码我们看到了指针压缩的代码, 体会下来, 做一些总结: 虽然64位的地址空间很大, 但是往往我们使用的内存范围并不需要这么多, 我们只需要能表达实际使用的内存范围即可, 哪怕地址是128位的, 我们只使用了其中1G, 这种情况仍然可以使用指针压缩; 我们需要表达的是范围, 而不是具体值, 从上面代码可以看到, 实际压缩指针存储的是基于base地址的差值, 而这个差值的最大值, 大部分情况不会超过32bit的表示能力.

那既然压缩后的指针是32bit, 使用指针压缩的最大堆是4G吗? 并非如此, 由于对象是8字节对齐的, 因此对象起始地址最低三位总是0, 因此可以存储时可以右移3bit, 高位空出来的3bit可以表示更高的数值, 实际上, 可以使用指针压缩的maxHeapSize是4G * 8 = 32G.


空说乏味, 我们来实际测一下

测试java代码:

public class JavaTest {
    /**
     * @param args
     */
    public static void main(String[] args) throws Exception {
      // 512M个引用槽位
      final int count = 512 * 1024 * 1024;

      Object[] array = new Object[count];

      Thread.sleep(1000000);
    }
}

运行结果:


默认指针压缩版本:
~/projects/JavaTest$ java -cp bin/ com.lqp.test.JavaTest

PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
10034 lqp 20 0 6835136 2.036g 15876 S 0.0 13.2 0:01.57 java

可以看到, 大概使用了2.036g(约等于512M * 4)的内存, 其中每个引用slot占4字节


再看关闭指针压缩的版本:
这里默认heapsize已经不够用了, 必须指定, 不然报OutOfMemoryError
~/projects/JavaTest$ java -Xms8G -XX:-UseCompressedOops -cp bin/ com.lqp.test.JavaTest

PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
10114 lqp 20 0 9.827g 4.055g 15624 S 0.0 26.4 0:03.52 java

可以看到, 大概使用了4.055g(约等于512M * 8), 其中每个引用slot占8字节, 翻了一倍.

没有更多推荐了,返回首页