字长这里我们指一个指针的 bit 数,在 32 位系统上是 32,64 位系统是 64(而不是 x86 汇编的那个 word,x86 的 word 是 16bit)
查看 jdk17 markWord
的注释,可以发现这样一段说明:
// jdk17u-dev/src/hotspot/share/oops/markWord.hpp
// The markWord describes the header of an object.
//
// Bit-format of an object header (most significant first, big endian layout below):
//
// 32 bits:
// --------
// hash:25 ------------>| age:4 biased_lock:1 lock:2 (normal object)
// JavaThread*:23 epoch:2 age:4 biased_lock:1 lock:2 (biased object)
//
// 64 bits:
// --------
// unused:25 hash:31 -->| unused_gap:1 age:4 biased_lock:1 lock:2 (normal object)
// JavaThread*:54 epoch:2 unused_gap:1 age:4 biased_lock:1 lock:2 (biased object)
...
// The runtime system aligns all JavaThread* pointers to a very large
// value (**currently 128 bytes (32bVM) or 256 bytes (64bVM)**) to make room
// for the age bits & the epoch bits (used in support of biased locking).
//
// [JavaThread* | epoch | age | 1 | 01] lock is biased toward given thread
// [0 | epoch | age | 1 | 01] lock is anonymously biased
这可不太对劲,比方说,32位的情况下,留给 JavaThread*
的字段是 23 位,而注释里说分配对象的时候,会 128(2^7) 字节对齐,这可远做不到让指针的后 9 位为 0。难道这里面还有什么别的魔法存在?
为了一探究竟,只能看看 JavaThread
是怎么分配内存的了:
// jdk17u-dev/src/hotspot/share/runtime/thread.hpp
class JavaThread: public Thread { ... }
class Thread: public ThreadShadow {
...
public:
void* operator new(size_t size) throw() { return allocate(size, true); }
...
}
void* Thread::allocate(size_t size, bool throw_excpt, MEMFLAGS flags) {
if (UseBiasedLocking) {
const size_t alignment = markWord::biased_lock_alignment;
size_t aligned_size = size + (alignment - sizeof(intptr_t));
void* real_malloc_addr = throw_excpt? AllocateHeap(aligned_size, flags, CURRENT_PC)
: AllocateHeap(aligned_size, flags, CURRENT_PC,
AllocFailStrategy::RETURN_NULL);
void* aligned_addr = align_up(real_malloc_addr, alignment);
assert(((uintptr_t) aligned_addr + (uintptr_t) size) <=
((uintptr_t) real_malloc_addr + (uintptr_t) aligned_size),
"JavaThread alignment code overflowed allocated storage");
if (aligned_addr != real_malloc_addr) {
log_info(biasedlocking)("Aligned thread " INTPTR_FORMAT " to " INTPTR_FORMAT,
p2i(real_malloc_addr),
p2i(aligned_addr));
}
((Thread*) aligned_addr)->_real_malloc_address = real_malloc_addr;
return aligned_addr;
}
可以看到,JavaThread
在分配的时候是按 alignment
对齐的。我们可以按照定义,一层层替换来找出他的真实值:
const size_t alignment = markWord::biased_lock_alignment;
= 2 << (epoch_shift + epoch_bits)
= 2 << (epoch_shift + 2)
= 2 << (hash_shift + 2)
= 2 << (unused_gap_shift + unused_gap_bits + 2)
= 2 << (unused_gap_shift + LP64_ONLY(1) NOT_LP64(0) + 2)
// 考虑 32 位的情况,此时 LP64_ONLY(1) NOT_LP64(0) 展开为 0
= 2 << (unused_gap_shift + 2)
= 2 << (age_shift + age_bits + 2)
= 2 << (age_shift + 6)
= 2 << (lock_bits + biased_lock_bits + 6)
= 2 << (2 + 1 + 6)
= 2 << 9
这里层级比较多,但归根结底就是 shift = epoch_bits + age_bits + biased_lock_bits + lock_bits = 2 + 4 + 1 + 2 = 9
。也就是说,实际上 JavaThread 对象是按 2^9=512
字节对齐的,而不是注释里说的 128 字节。
到这里可以说问题已经解决,但我们可不能止步于此。剩下还有个问题是,这对齐到底是怎么做到的?
我们分配完内存以后,把他向上对齐到 aligment 字节后,相关的变量的相对关系如下:
// - 低地址方向 -
//
// real_malloc_addr --> +-----+
// ∧ | |
// | | |
// aligned_size | | <-- aligned_addr
// | | | |
// | | | size
// | | | ┴
// ∨ | |
// - +-----+
//
// - 高地址方向 -
align_up(real_malloc_addr, 512)
= align_down(real_malloc_addr + (512-1), 512)
= (real_malloc_addr + (512-1)) & ~(512-1)
通俗一点说,aligned_addr
是从 real_malloc_addr
算起的第一个跟 alignment 对齐的地址。看起来不错,但是, aligned_addr + size <= real_malloc_addr + aligned_size
是如何保证的?
假设 real_malloc_addr
可以是任意值,则 align_up 以后的值最多的情况下会比原先大 (alignment - 1) 字节(比方说,align_up(513, 512) = 1024
)。在这种情况下,由于我们才多申请了 alignment - sizeof(intptr_t)
个字节,显然是不能满足要求的。唯一可能满足的可能是,AllocateHeap
返回的地址本身已经按 sizeof(intptr_t)
对齐;这种情况下,align up 以后最多就只会比原先多 alignment - sizeof(intptr_t)
个字节。那么,真实情况是否真的是如此?
查看代码可以发现,AllocateHeap
最终调用的其实是 libc 的 malloc
。翻阅 malloc
的文档,第一句是这么说的(在 Mac 上通过 manpage 查看):
The
malloc()
,calloc()
,valloc()
,realloc()
, andreallocf()
functions allocate memory. The allocated memory is aligned such that it can be used for any data type.
重点看最后的“it can be used for any data type”,这意味着返回的地址是 sizeof(intptr_t)
对齐的。也就是说,前面我们假设“AllocateHeap
返回的地址本身已经按 sizeof(intptr_t)
对齐”成立。
所以有
align_up(real_malloc_addr, alignment) <= real_malloc_addr + alignment - sizeof(intptr_t)
即
aligned_addr <= real_malloc_addr + alignment - sizeof(intptr_t)
两边加上 size,则有
aligned_addr + size <= real_malloc_addr + (size + alignment - sizeof(intptr_t)
aligned_addr + size <= real_malloc_addr + aligned_size
结论成立。
也可以换一种论证方式。由于我们 align up 以后的地址最多会比原先增大
alignment - sizeof(intptr_t)
,而我们在分配内存的时候又多分配了这么多,所以实际使用的内存肯定不会越界。
真是应了那句话,“细节是魔鬼”。还好偏向锁在 jdk19 移除了,下一代程序员不需要受它折磨了。