十年后,情况发生了变化。老实说,我不敢相信这一点(但我内心的极客非常快乐)。
正如您已经注意到的那样,某些字符串可能存在String::hashCode
某些字符串zero
并且未缓存(将得到缓存)。很多人争论(包括在这个问答中)为什么没有在java.lang.String
: 中添加一个字段,hashAlreadyComputed
然后简单地使用它。问题很明显:每个 String 实例都有额外的空间。顺便说一句,引入s 是有原因 的,因为许多基准测试表明,在大多数应用程序中,这是一个相当(过度)使用的类。增加更多空间?决定是:不。特别是因为最小可能的加法本来是,而不是(对于s,额外的空间本来是java-9
compact String
1 byte
1 bit
32 bit JMV
8 bytes
: 1 表示标志,7 表示对齐)。
因此,Compact String
s 出现了java-9
,如果您仔细(或关心)他们确实java.lang.String
在:中添加了一个字段coder
。我刚才不是反对吗?那并没那么简单。似乎紧凑字符串的重要性超过了“额外空间”的论点。同样重要的是要说额外的空间32 bits VM
仅对(因为对齐没有间隙)。相比之下,在jdk-8
布局中java.lang.String
是:
java.lang.String object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 12 (object header) N/A
12 4 char[] String.value N/A
16 4 int String.hash N/A
20 4 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
请注意那里的一件重要事情:
Space losses : ... 4 bytes total.
因为每个 java 对象都是对齐的(多少取决于 JVM 和一些启动标志UseCompressedOops
,例如),所以有一个未使用String
的间隙。4 bytes
因此,在添加 时coder
,1 byte
无需添加额外空间即可。因此,在 Compact String
添加 s 之后,布局发生了变化:
java.lang.String object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 12 (object header) N/A
12 4 byte[] String.value N/A
16 4 int String.hash N/A
20 1 byte String.coder N/A
21 3 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 3 bytes external = 3 bytes total
coder
吃1 byte
了,差距缩小到了3 bytes
。所以“伤害”已经造成了jdk-9
。因为32 bits JVM
有一个增加8 bytes : 1 coder + 7 gap
和为64 bit JVM
- 没有增加,coder
从间隙中占据了一些空间。
而现在,jdk-13
他们决定利用它gap
,因为它无论如何都存在。让我提醒您,具有零 hashCode 的字符串的概率是 40 亿分之一;还是有人说:那又怎样?让我们解决这个问题!瞧:jdk-13
布局java.lang.String
:
java.lang.String object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 12 (object header) N/A
12 4 byte[] String.value N/A
16 4 int String.hash N/A
20 1 byte String.coder N/A
21 1 boolean String.hashIsZero N/A
22 2 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 2 bytes external = 2 bytes total
这里是:boolean String.hashIsZero
。它在代码库中:
public int hashCode() {
int h = hash;
if (h == 0 && !hashIsZero) {
h = isLatin1() ? StringLatin1.hashCode(value)
: StringUTF16.hashCode(value);
if (h == 0) {
hashIsZero = true;
} else {
hash = h;
}
}
return h;
}
等待!h == 0
和 hashIsZero
领域?不应该将其命名为 :hashAlreadyComputed
吗?为什么实现不符合以下内容:
@Override
public int hashCode(){
if(!hashCodeComputed){
// or any other sane computation
hash = 42;
hashCodeComputed = true;
}
return hash;
}
即使我阅读了源代码下的评论:
// The hash or hashIsZero fields are subject to a benign data race,
// making it crucial to ensure that any observable result of the
// calculation in this method stays correct under any possible read of
// these fields. Necessary restrictions to allow this to be correct
// without explicit memory fences or similar concurrency primitives is
// that we can ever only write to one of these two fields for a given
// String instance, and that the computation is idempotent and derived
// from immutable state
只有在我读完这个之后才有意义。相当棘手,但这一次只写一篇,更多细节在上面的讨论中。