Java中的“内部地址”是啥?
Posted
技术标签:
【中文标题】Java中的“内部地址”是啥?【英文标题】:What is an "internal address" in Java?Java中的“内部地址”是什么? 【发布时间】:2012-12-01 08:43:17 【问题描述】:在 Object.hashCode() 的 Javadoc 中声明
在合理可行的情况下,
Object
类定义的 hashCode 方法确实为不同的对象返回不同的整数。 (这通常通过将对象的内部地址转换为整数来实现,但 Java™ 编程语言不需要这种实现技术。)
这是一个常见的误解,这与内存地址有关,但它不会在没有通知的情况下更改,并且 hashCode() 不会也不能更改对象。
@Neet 提供了一个很好的答案https://***.com/a/565416/57695 的链接,但我正在寻找更多详细信息。
这里有一个例子来说明我的担心
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafe.get(null);
for (int t = 0; t < 10; t++)
System.gc();
Object[] objects = new Object[10];
for (int i = 0; i < objects.length; i++)
objects[i] = new Object();
for (int i = 0; i < objects.length; i++)
if (i > 0) System.out.print(", ");
int location = unsafe.getInt(objects, Unsafe.ARRAY_OBJECT_BASE_OFFSET + Unsafe.ARRAY_OBJECT_INDEX_SCALE * i);
System.out.printf("%08x: hc= %08x", location, objects[i].hashCode());
System.out.println();
打印
eac00038: hc= 4f47e0ba, eac00048: hc= 2342d884, eac00058: hc= 7994d431, eac00068: hc= 19f71b53, eac00078: hc= 2e22f376, eac00088: hc= 789ddfa3, eac00098: hc= 44c58432, eac000a8: hc= 036a11e4, eac000b8: hc= 28bc917c, eac000c8: hc= 73f378c8
eac00038: hc= 30813486, eac00048: hc= 729f624a, eac00058: hc= 3dee2310, eac00068: hc= 5d400f33, eac00078: hc= 18a60d19, eac00088: hc= 3da5f0f3, eac00098: hc= 596e0123, eac000a8: hc= 450cceb3, eac000b8: hc= 4bd66d2f, eac000c8: hc= 6a9a4f8e
eac00038: hc= 711dc088, eac00048: hc= 584b5abc, eac00058: hc= 3b3219ed, eac00068: hc= 564434f7, eac00078: hc= 17f17060, eac00088: hc= 6c08bae7, eac00098: hc= 3126cb1a, eac000a8: hc= 69e0312b, eac000b8: hc= 7dbc345a, eac000c8: hc= 4f114133
eac00038: hc= 50c8c3b8, eac00048: hc= 2ca98e77, eac00058: hc= 2fc83d89, eac00068: hc= 034005e1, eac00078: hc= 6041f871, eac00088: hc= 0b1df416, eac00098: hc= 5b83d60d, eac000a8: hc= 2c5a1e6b, eac000b8: hc= 5083198c, eac000c8: hc= 4f025f9f
eac00038: hc= 00c5eb8a, eac00048: hc= 41eab16b, eac00058: hc= 1726099c, eac00068: hc= 4240eca3, eac00078: hc= 346fe350, eac00088: hc= 1db4b415, eac00098: hc= 429addef, eac000a8: hc= 45609812, eac000b8: hc= 489fe953, eac000c8: hc= 7a8f6d64
eac00038: hc= 7e628e42, eac00048: hc= 7869cfe0, eac00058: hc= 6aceb8e2, eac00068: hc= 29cc3436, eac00078: hc= 1d77daaa, eac00088: hc= 27b4de03, eac00098: hc= 535bab52, eac000a8: hc= 274cbf3f, eac000b8: hc= 1f9fd541, eac000c8: hc= 3669ae9f
eac00038: hc= 772a3766, eac00048: hc= 749b46a8, eac00058: hc= 7e3bfb66, eac00068: hc= 13f62649, eac00078: hc= 054b8cdc, eac00088: hc= 230cc23b, eac00098: hc= 1aa3c177, eac000a8: hc= 74f2794a, eac000b8: hc= 5af92541, eac000c8: hc= 1afcfd10
eac00038: hc= 396e1dd8, eac00048: hc= 6c696d5c, eac00058: hc= 7d8aea9e, eac00068: hc= 2b316b76, eac00078: hc= 39862621, eac00088: hc= 16315e08, eac00098: hc= 03146a9a, eac000a8: hc= 3162a60a, eac000b8: hc= 4382f3da, eac000c8: hc= 4a578fd6
eac00038: hc= 225765b0, eac00048: hc= 17d5176d, eac00058: hc= 26f50154, eac00068: hc= 1f2a45c7, eac00078: hc= 104b1bcd, eac00088: hc= 330e3816, eac00098: hc= 6a844689, eac000a8: hc= 12330301, eac000b8: hc= 530a3ffc, eac000c8: hc= 45eee3fb
eac00038: hc= 3f9432e0, eac00048: hc= 1a9830bc, eac00058: hc= 7da79447, eac00068: hc= 04f801c4, eac00078: hc= 363bed68, eac00088: hc= 185f62a9, eac00098: hc= 1e4651bf, eac000a8: hc= 1aa0e220, eac000b8: hc= 385db088, eac000c8: hc= 0ef0cda1
作为旁注;如果你看这段代码
if (value == 0) value = 0xBAD ;
0xBAD 似乎是任何 hashCode 的两倍,因为 0 映射到该值。如果你运行足够长的时间,你会看到
long count = 0, countBAD = 0;
while (true)
for (int i = 0; i < 200000000; i++)
int hc = new Object().hashCode();
if (hc == 0xBAD)
countBAD++;
count++;
System.out.println("0xBAD ratio is " + (double) (countBAD << 32) / count + " times expected.");
打印
0xBAD ratio is 2.0183116992481205 times expected.
【问题讨论】:
之前有人问过,看这里:***.com/questions/557574/… @Neet 由于 OP 是回答其他问题的人之一,我倾向于认为他在问一些不同的问题。 @Peter 好吧,那没关系:-)。 @Michael 您传递的每个对象都会作为pointer
传递...否则NullPointerException
将没有意义^^
文档说“通常实现”。 JVM 的所有内部结构都不需要对开发人员有意义。他们告诉你的一切都会奏效。所以最有可能的是,当对象被实例化时,它会根据地址存储一个唯一标识符,该标识符在任何地方都与对象保持一致。
这个问题的变体已被多次询问(通常与 hashCode 有关):***.com/questions/4930781/… 或此:***.com/questions/10917731/…
【参考方案1】:
这显然是特定于实现的。
下面我包括在 OpenJDK 7 中使用的 Object.hashCode()
实现。
该函数支持六种不同的计算方法,其中只有两种会注意到对象的地址(“地址”是 C++ 的 oop
转换为 intptr_t
)。这两种方法中的一种按原样使用地址,而另一种则进行一些操作,然后将结果与不经常更新的随机数混合。
其余方法中,一种返回常量(可能用于测试),一种返回序列号,其余的基于伪随机序列。
貌似可以在运行时选择方法,默认好像是方法0,也就是os::random()
。后者是linear congruential generator,带有所谓的竞争条件。:-) 竞争条件是可以接受的,因为在最坏的情况下它会导致两个对象共享相同的哈希码;这不会破坏任何不变量。
第一次需要哈希码时执行计算。为了保持一致性,结果随后存储在对象的标头中,并在后续调用hashCode()
时返回。缓存是在这个函数之外完成的。
总而言之,Object.hashCode()
基于对象地址的概念在很大程度上是一个历史产物,已被现代垃圾收集器的属性所淘汰。
// hotspot/src/share/vm/runtime/synchronizer.hpp
// hashCode() generation :
//
// Possibilities:
// * MD5Digest of obj,stwRandom
// * CRC32 of obj,stwRandom or any linear-feedback shift register function.
// * A DES- or AES-style SBox[] mechanism
// * One of the Phi-based schemes, such as:
// 2654435761 = 2^32 * Phi (golden ratio)
// HashCodeValue = ((uintptr_t(obj) >> 3) * 2654435761) ^ GVars.stwRandom ;
// * A variation of Marsaglia's shift-xor RNG scheme.
// * (obj ^ stwRandom) is appealing, but can result
// in undesirable regularity in the hashCode values of adjacent objects
// (objects allocated back-to-back, in particular). This could potentially
// result in hashtable collisions and reduced hashtable efficiency.
// There are simple ways to "diffuse" the middle address bits over the
// generated hashCode values:
//
static inline intptr_t get_next_hash(Thread * Self, oop obj)
intptr_t value = 0 ;
if (hashCode == 0)
// This form uses an unguarded global Park-Miller RNG,
// so it's possible for two threads to race and generate the same RNG.
// On MP system we'll have lots of RW access to a global, so the
// mechanism induces lots of coherency traffic.
value = os::random() ;
else
if (hashCode == 1)
// This variation has the property of being stable (idempotent)
// between STW operations. This can be useful in some of the 1-0
// synchronization schemes.
intptr_t addrBits = intptr_t(obj) >> 3 ;
value = addrBits ^ (addrBits >> 5) ^ GVars.stwRandom ;
else
if (hashCode == 2)
value = 1 ; // for sensitivity testing
else
if (hashCode == 3)
value = ++GVars.hcSequence ;
else
if (hashCode == 4)
value = intptr_t(obj) ;
else
// Marsaglia's xor-shift scheme with thread-specific state
// This is probably the best overall implementation -- we'll
// likely make this the default in future releases.
unsigned t = Self->_hashStateX ;
t ^= (t << 11) ;
Self->_hashStateX = Self->_hashStateY ;
Self->_hashStateY = Self->_hashStateZ ;
Self->_hashStateZ = Self->_hashStateW ;
unsigned v = Self->_hashStateW ;
v = (v ^ (v >> 19)) ^ (t ^ (t >> 8)) ;
Self->_hashStateW = v ;
value = v ;
value &= markOopDesc::hash_mask;
if (value == 0) value = 0xBAD ;
assert (value != markOopDesc::no_hash, "invariant") ;
TEVENT (hashCode: GENERATE) ;
return value;
【讨论】:
我正在浏览源代码,但找不到它 - 这是什么文件?hotspot/src/share/vm/runtime/synchronizer.hpp
感谢 - 记录,链接到 openjdk7:hg.openjdk.java.net/jdk7/jdk7/hotspot/file/9b0ca45cd756/src/… - 它似乎没有太大变化......
知道它使用的是哪一个,或者你可以如何改变它?
@PeterLawrey:看起来hashCode
是一个运行时标志(尚不确定如何设置它)。默认的似乎是0。【参考方案2】:
它通常是对象的内存地址。但是,第一次在对象上调用 hashcode
方法时,整数存储在该对象的标头中,以便下一次调用将返回相同的值(正如您所说,压缩垃圾收集可以更改地址)。据我所知,这就是它在 Oracle JVM 中的实现方式。
编辑:深入研究 JVM 源代码,这就是显示的内容(synchronizer.cpp):
// hashCode() generation :
//
// Possibilities:
// * MD5Digest of obj,stwRandom
// * CRC32 of obj,stwRandom or any linear-feedback shift register function.
// * A DES- or AES-style SBox[] mechanism
// * One of the Phi-based schemes, such as:
// 2654435761 = 2^32 * Phi (golden ratio)
// HashCodeValue = ((uintptr_t(obj) >> 3) * 2654435761) ^ GVars.stwRandom ;
// * A variation of Marsaglia's shift-xor RNG scheme.
// * (obj ^ stwRandom) is appealing, but can result
// in undesirable regularity in the hashCode values of adjacent objects
// (objects allocated back-to-back, in particular). This could potentially
// result in hashtable collisions and reduced hashtable efficiency.
// There are simple ways to "diffuse" the middle address bits over the
// generated hashCode values:
//
static inline intptr_t get_next_hash(Thread * Self, oop obj)
intptr_t value = 0 ;
if (hashCode == 0)
// This form uses an unguarded global Park-Miller RNG,
// so it's possible for two threads to race and generate the same RNG.
// On MP system we'll have lots of RW access to a global, so the
// mechanism induces lots of coherency traffic.
value = os::random() ;
else
if (hashCode == 1)
// This variation has the property of being stable (idempotent)
// between STW operations. This can be useful in some of the 1-0
// synchronization schemes.
intptr_t addrBits = intptr_t(obj) >> 3 ;
value = addrBits ^ (addrBits >> 5) ^ GVars.stwRandom ;
else
if (hashCode == 2)
value = 1 ; // for sensitivity testing
else
if (hashCode == 3)
value = ++GVars.hcSequence ;
else
if (hashCode == 4)
value = intptr_t(obj) ;
else
// Marsaglia's xor-shift scheme with thread-specific state
// This is probably the best overall implementation -- we'll
// likely make this the default in future releases.
unsigned t = Self->_hashStateX ;
t ^= (t << 11) ;
Self->_hashStateX = Self->_hashStateY ;
Self->_hashStateY = Self->_hashStateZ ;
Self->_hashStateZ = Self->_hashStateW ;
unsigned v = Self->_hashStateW ;
v = (v ^ (v >> 19)) ^ (t ^ (t >> 8)) ;
Self->_hashStateW = v ;
value = v ;
value &= markOopDesc::hash_mask;
if (value == 0) value = 0xBAD ;
assert (value != markOopDesc::no_hash, "invariant") ;
TEVENT (hashCode: GENERATE) ;
return value;
所以在Oracle JVM中有6种不同的实现方式,其中一种和我说的等价... 987654324@ 并从 Object.hashCode()
的本机版本调用。
【讨论】:
对象首先在 eden 空间中创建,并且每次运行一次次要/完整 GC 时,eden 空间都会使用相同的地址重用。如果 hashCode 使用内存地址,它们会重复很多次。 是否是地址取决于选择了6个hashcode算法中的哪一个。真的,所需要的只是一种生成不会重叠太多且快速完成的整数的方法...... 我真的不知道为什么这么多人投票反对我的答案。我现在将离开这个讨论。比起为谁先找到一些随机源代码而争吵,我还有更好的事情要做…… @MathiasSchwarz,他们可能决定支持 NPE 的回答并删除他们给你的投票。这与谁首先找到示例无关,而是它如何为其增加价值。您刚刚发布到代码 NPE 指出了一些事情。【参考方案3】:恕我直言,虽然依赖于实现,但 JVM 中的对象引用绝不是实际的内存地址,而是指向实际地址的内部 JVM 引用。这些内部引用最初是根据内存地址生成的,但在对象被丢弃之前,它们仍然与对象相关联。
我这样说是因为 Java HotSpot GC 正在复制某种形式的收集器,它们通过遍历 live objects
并将它们从一个堆复制到另一个堆来工作,随后销毁 old heap
。所以当GC发生时,JVM不必更新所有对象中的引用,而只是改变映射到内部引用的实际内存地址。
【讨论】:
【参考方案4】:Javadoc 对 Object.hashCode() 的建议是通过从内存地址派生哈希码来实现它,这已经过时了。
可能没有人打扰(或注意到)复制垃圾收集器不再可能实现此实现路径(因为当对象被复制到另一个内存位置时,它会更改哈希码)。
在复制垃圾收集器之前以这种方式实现哈希码非常有意义,因为它可以节省堆空间。今天,非复制 GC (CMS) 可能仍以这种方式实现哈希码。
【讨论】:
以上是关于Java中的“内部地址”是啥?的主要内容,如果未能解决你的问题,请参考以下文章