Java中的"内部地址"是什么?

24
Object.hashCode()的Javadoc中,它声明:

尽可能合理地说,由类 Object 定义的hashCode方法确实为不同的对象返回不同的整数。(通常通过将对象内部地址转换为整数来实现此功能,但Java™编程语言不要求使用此实现技术。)

这是一个常见的误解,认为hashCode与内存地址有关,但它并不是这样,因为内存地址可以随时更改,而hashCode()不能且必须不能更改。

@Neet提供了一个好答案的链接https://dev59.com/Y3RB5IYBdhLWcg3wpoxm#565416,但我正在寻找更多细节。


以下是一个示例,以说明我的担忧

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(十进制数2989)是任何hashCode的正常值的两倍,因为当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.

1
此前已经有人问过了,请参见这里:https://dev59.com/Y3RB5IYBdhLWcg3wpoxm#565416 - Neet
1
@Neet 因为原帖的作者是回答另一个问题的人之一,所以我倾向于认为他在问不同的问题。 - assylias
1
@Peter 好的,那就算了 :-). @Michael 每个传递的对象都作为指针传递...否则 NullPointerException 将没有意义^^ - Neet
3
文档中提到“通常实现”。JVM的所有内部细节对开发人员来说并不需要有意义,只需告诉他们它可以正常工作。因此,当对象被实例化时,很可能会基于地址存储一个唯一标识符,该标识符将随着对象的位置而保持不变。 - Michael Ozeryansky
1
类似这样的编程问题已经被多次问到(通常涉及hashCode):https://dev59.com/mm445IYBdhLWcg3wTIjm 或者是这个问题:https://dev59.com/aGTWa4cB1Zd3GeqPFqtD - bestsss
显示剩余9条评论
4个回答

23

很明显,这是特定于实现的。

下面我包括在OpenJDK 7中使用的Object.hashCode()实现。

该函数支持六种不同的计算方法,其中只有两种方法考虑了对象的地址(“地址”是C++ oop转换为intptr_t)。其中一种方法直接使用地址,而另一种方法则进行一些位操作,然后将结果与不经常更新的随机数混合。

剩余的方法中,一个返回常量(可能用于测试),一个返回连续的数字,其余的基于伪随机序列。

看起来可以在运行时选择方法,默认似乎是方法0,即os::random()。后者是线性同余生成器,带有一个声称存在的竞争条件。 :-) 竞争条件是可接受的,因为最坏情况下会导致两个对象共享相同的哈希码;这不会破坏任何不变式。

第一次需要哈希码时执行计算。为保持一致性,结果随后存储在对象的头中,并在后续调用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;
}

1
我正在浏览源代码,但找不到它 - 它在哪个文件中? - assylias
3
hotspot/src/share/vm/runtime/synchronizer.hpp - NPE
1
谢谢 - 顺便提一下,打开jdk7的链接:http://hg.openjdk.java.net/jdk7/jdk7/hotspot/file/9b0ca45cd756/src/share/vm/runtime/synchronizer.cpp - 看起来没有太大变化... - assylias
2
@PeterLawrey:看起来hashCode是一个运行时标志(还不确定如何设置)。默认值似乎为0。 - NPE
注意:默认值看起来是5 - eis
显示剩余2条评论

5

通常情况下,是对象的内存地址。但是,第一次调用对象的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种不同的方法可以实现它,其中一种与我所说的等效。值通过调用get_next_hash(称为FastHashCode并从Object.hashCode()的本机版本调用)的方法存储在对象的头部。


1
对象首先在伊甸园空间中创建,每次运行小/完全GC时,伊甸园空间都会重复使用相同的地址。如果hashCode使用内存地址,它们将会重复很多次。 - Peter Lawrey
无论是哪种6个哈希算法被选择,它都取决于地址是否正确。实际上,需要的只是一种能够快速生成不重叠整数的方法... - Mathias Schwarz
我真的不知道为什么这么多人对我的回答投反对票。我现在会离开这个讨论。我有更好的事情要做,而不是为了争夺谁先发现一些随机源代码而打架。 - Mathias Schwarz
@MathiasSchwarz,他们可能决定给NPE的答案点赞并取消他们之前给你的投票。这不是关于谁先发现例子,而是它如何为其增加价值。你只是发布了代码,NPE指出了一些问题。 - Damian Leszczyński - Vash

2

在我看来,虚拟机中的对象引用虽然与具体实现有关,但它们从未是真正的内存地址,而是指向实际地址的内部虚拟机引用。这些内部引用最初基于内存地址生成,但它们会一直与对象相关联,直到对象被丢弃。

我这样说是因为Java HotSpot GC是某种复制式收集器,它们通过遍历活动对象并将其从一个堆复制到另一个堆,随后销毁旧堆。因此,当GC发生时,JVM不必更新所有对象中的引用,而只需更改映射到内部引用的实际内存地址即可。


0

Javadoc对Object.hashCode()的建议是从内存地址派生哈希码,这种实现方式已经过时。

可能没有人注意到或者关注到这种实现路径在使用复制垃圾收集器时不再可行(因为当对象被复制到另一个内存位置时,它会改变哈希码)。

在有复制垃圾收集器之前,以这种方式实现哈希码非常有意义,因为它可以节省堆空间。非复制GC(CMS)今天仍然可以以这种方式实现哈希码。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接