在上一篇文章 [Java JVM] Hotspot GC研究- 开篇&对象内存布局 中介绍对象内存布局时, 曾提到过, 由于在64位CPU下, 指针的宽度是64位的, 而实际的heap区域远远用不到这么大的内存, 使用64bit来存对象引用会造成浪费, 所以应该做点事情来节省资源.
基于以下事实:
细心的你从上面一定可以看出一点线索, 由于存一个对象引用和取一个对象引用必须经过虚拟机, 所以完全可以在虚拟机这一层做些手脚. 对于外部来说, 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(offset)) :
load_decode_heap_oop(obj_field_addr(offset));
}
void oopDesc::obj_field_put(int offset, oop value) {
UseCompressedOops ? oop_store(obj_field_addr(offset), value) :
oop_store(obj_field_addr(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字节, 翻了一倍.