java 获取内存地址_对Java中HashCode方法的深入思考

最近在学习 Go 语言,Go 语言中有指针对象,一个指针变量指向了一个值的内存地址。学习过 C 语言的猿友应该都知道指针的概念。Go 语言语法与 C 相近,可以说是类 C 的编程语言,所以 Go 语言中有指针也是很正常的。我们可以通过将取地址符&放在一个变量前使用就会得到相应变量的内存地址。

fc9b882c009205f21f6851dbd6651688.png

因为本人主要开发语言是 Java,所以我就联想到 Java 中没有指针,那么 Java 中如何获取变量的内存地址呢?

如果能获取变量的内存地址那么就可以清晰的知道两个对象是否是同一个对象,如果两个对象的内存地址相等那么无疑是同一个对象反之则是不同的对象。

很多人说对象的 HashCode 方法返回的就是对象的内存地址,包括我在《Java核心编程·卷I》的第5章内容中也发现说是 HashCode 其值就是对象的内存地址。

460a6a9219388a019fca5cde91c5d0a8.png

但是 HashCode 方法真的是内存地址吗?回答这个问题前我们先回顾下一些基础知识。

==和equals

在 Java 中比较两个对象是否相等主要是通过 ==号,比较的是他们在内存中的存放地址。Object 类是 Java 中的超类,是所有类默认继承的,如果一个类没有重写 Object 的 equals方法,那么通过equals方法也可以判断两个对象是否相同,因为它内部就是通过==来实现的。

//Indicates whether some other object is "equal to" this one.public boolean equals(Object obj) { return (this == obj);}
Tips:这里额外解释个疑惑
我们学习 Java 的时候知道,Java 的继承是单继承,如果所有的类都继承了 Object 类,那么为何创建一个类的时候还可以extend其他的类?这里涉及到直接继承和间接继承的问题,当创建的类没有通过关键字 extend 显示继承指定的类时,类默认的直接继承了Object,A –> Object。当创建的类通过关键字 extend 显示继承指定的类时,则它间接的继承了Object类,A –> B –> Object。

这里的相同,是说比较的两个对象是否是同一个对象,即在内存中的地址是否相等。而我们有时候需要比较两个对象的内容是否相同,即类具有自己特有的“逻辑相等”概念,而不是想了解它们是否指向同一个对象。

例如比较如下两个字符串是否相同String a = "Hello" 和 String b = new String("Hello"),这里的相同有两种情形,是要比较 a 和 b 是否是同一个对象(内存地址是否相同),还是比较它们的内容是否相等?这个具体需要怎么区分呢?

如果使用 == 那么就是比较它们在内存中是否是同一个对象,但是 String 对象的默认父类也是 Object,所以默认的equals方法比较的也是内存地址,所以我们要重写 equals方法,正如 String 源码中所写的那样。

3329de2b17d2fb9805d971b147d093e5.png

这样当我们 a == b时是判断 a 和 b 是否是同一个对象,a.equals(b)则是比较 a 和 b 的内容是否相同,这应该很好理解。

JDK 中不止 String 类重写了equals 方法,还有数据类型 Integer,Long,Double,Float等基本也都重写了 equals 方法。所以我们在代码中用 Long 或者 Integer 做业务参数的时候,如果要比较它们是否相等,记得需要使用 equals 方法,而不要使用 ==。

因为使用 ==号会有意想不到的坑出现,像这种数据类型很多都会在内部封装一个常量池,例如 IntegerCache,LongCache 等等。当数据值在某个范围内时会直接从常量池中获取而不会去新建对象。

如果要使用==,可以将这些数据包装类型转换为基本类型之后,再通过==来比较,因为基本类型通过==比较的是数值,但是在转换的过程中需要注意 NPE(NullPointException)的发生。

Object中的HashCode

equals 方法能比较两个对象的内容是否相等,因此可以用来查找某个对象是否在集合容器中,通常大致就是逐一去取集合中的每个对象元素与需要查询的对象进行equals比较,当发现某个元素与要查找的对象进行equals方法比较的结果相等时,则停止继续查找并返回肯定的信息,否则,返回否定的信息。

但是通过这种比较的方式效率很低,时间复杂度比较高。那么我们是否可以通过某种编码方式,将每一个对象都具有某个特定的码值,根据码值将对象分组然后划分到不同的区域,这样当我们需要在集合中查询某个对象时,我们先根据该对象的码值就能确定该对象存储在哪一个区域,然后再到该区域中通过equals方式比较内容是否相等,就能知道该对象是否存在集合中。

通过这种方式我们减少了查询比较的次数,优化了查询的效率同时也就减少了查询的时间。

这种编码方式在 Java 中就是 hashCode 方法,Object 类中默认定义了该方法, 它是一个 native 修饰的本地方法,返回值是一个 int 类型。

76d33b50af6fbc08ed69b2c3bc538a54.png

从注释的描述可以知道,hashCode 方法返回该对象的哈希码值。它可以为像 HashMap 这样的哈希表有益。Object 类中定义的 hashCode 方法为不同的对象返回不同的整形值。具有迷惑异议的地方就是This is typically implemented by converting the internal address of the object into an integer这一句,意为通常情况下实现的方式是将对象的内部地址转换为整形值。

如果你不深究就会认为它返回的就是对象的内存地址,我们可以继续看看它的实现,但是因为这里是 native 方法所以我们没办法直接在这里看到内部是如何实现的。native 方法本身非 java 实现,如果想要看源码,只有下载完整的 jdk 源码,Oracle 的 JDK 是看不到的,OpenJDK 或其他开源 JRE 是可以找到对应的 C/C++ 代码。我们在 OpenJDK 中找到 Object.c 文件,可以看到hashCode 方法指向 JVM_IHashCode 方法来处理。

6d1727f19e4de9cb6769eaa80f259f34.png

而JVM_IHashCode方法实现在 jvm.cpp中的定义为:

JVM_ENTRY(jint, JVM_IHashCode(JNIEnv* env, jobject handle))  JVMWrapper("JVM_IHashCode");  // as implemented in the classic virtual machine; return 0 if object is NULL  return handle == NULL ? 0 : ObjectSynchronizer::FastHashCode (THREAD, JNIHandles::resolve_non_null(handle)) ; JVM_END 

这里是一个三目表达式,真正计算获得 hashCode 值的是ObjectSynchronizer::FastHashCode,它具体的实现在synchronizer.cpp中,截取部分关键代码片段。

a249bc5b79d88c5f9e9706749da8794c.png

从以上代码片段中可以发现,实际计算hashCode的是 get_next_hash,还在这份文件中我们搜索get_next_hash,得到他的关键代码。

4d1c2cf517ec63ea614d4d1dd9f10002.png
7b37444e3cf9bc3a5d4708fa8968256b.png

从get_next_hash的方法中我们可以看到,如果从0开始算的话,这里提供了6种计算 hash 值的方案,有自增序列,随机数,关联内存地址等多种方式,其中官方默认的是最后一种,即随机数生成。可以看出 hashCode 也许和内存地址有关系,但不是直接代表内存地址的,具体需要看虚拟机版本和设置。

equals和hashCode

equals 和 hashCode 都是 Object 类拥有的方法,包括 Object 类中的 toString 方法打印的内容也包含 hashCode 的无符号十六进制值。

public String toString() { return getClass().getName() + "@" + Integer.toHexString(hashCode());}

由于需要比较对象内容,所以我们通常会重写 equals 方法,但是重写 equals 方法的同时也需要重写 hashCode 方法,有没有想过为什么?

因为如果不这样做的话,就会违反 hashCode 的通用约定,从而导致该类无法结合所有基于散列的集合一起正常工作,这类集合包括 HashMap 和 HashSet。

这里的通用约定,从 Object 类的 hashCode 方法的注释可以了解,主要包括以下几个方面,

  • 在应用程序的执行期间,只要对象的 equals 方法的比较操作所用到的信息没有被修改,那么对同一个对象的多次调用,hashCode 方法都必须始终返回同一个值。
  • 如果两个对象根据 equals 方法比较是相等的,那么调用这两个对象中的 hashCode 方法都必须产生同样的整数结果。
  • 如果两个对象根据 equals 方法比较是不相等的,那么调用者两个对象中的 hashCode 方法,则不一定要求 hashCode 方法必须产生不同的结果。但是给不相等的对象产生不同的整数散列值,是有可能提高散列表(hash table)的性能。

从理论上来说如果重写了 equals 方法而没有重写 hashCode 方法则违背了上述约定的第二条,相等的对象必须拥有相等的散列值。

但是规则是大家默契的约定,如果我们就喜欢不走寻常路,在重写了 equals 方法后没有覆盖 hashCode 方法,会产生什么后果吗?

我们自定义一个 Student 类,并且重写了 equals 方法,但是我们没有重写 hashCode 方法,那么当调用 Student 类的 hashCode 方法的时候,默认就是调用超类 Object 的 hashCode 方法,根据随机数返回的一个整型值。

970742082654824dcf5d0f2c46f5f4da.png

我们创建两个对象并且设置属性值一样,测试下结果:

public static void main(String[] args) { Student student1 = new Student("小明

https://stackoverflow.com/questions/16418713/does-hashcode-number-represent-the-memory-address
Hashcode 是 JVM 用于散列以存储和检索对象的数字。例如,当我们在 hashmap 中添加一个对象时,JVM 会查找 hashcode 实现来决定将对象放在内存中的哪个位置。当我们再次检索对象时,使用哈希码来获取对象的位置。请注意,哈希码不是实际的内存地址,而是 JVM 从指定位置获取对象的链接,复杂度为 O(1)。

A hashcode is an integer value that represents the state of the object upon which it was called. That is why an Integer that is set to 1 will return a hashcode of "1" because an Integer's hashcode and its value are the same thing. A character's hashcode is equal to it's ASCII character code. If you write a custom type you are responsible for creating a good hashCode implementation that will best represent the state of the current instance.

Java中的hashCode 真的是地址吗?

问题

1.在java中hashCode获取是如何实现的?

2.hashCode的值是否是可预测的?

(注:hashCode(散列值)——将对象映射为一个整型值,不同的对象返回不同的数值)

 

正文

在Object.java#hashCode 的注解中找到怎么一句话:

  1. (This is typically implemented by converting the internal
  2. address of the object into an integer, but this implementation
  3. technique is not required by the
  4. Java™ programming language.)

意思是:hash值来源于这个对象的内部地址转换成的整型值。

我就很好奇了,这里的内部地址到底指的是什么地址?莫非类似下面这样

  1. int main()
  2. {
  3. char var = 1;
  4. printf("%p\n", &var);
  5. }

console:

0028FF3F

在C当中上述代码输出的是var变量的内存地址。

为了解决这个谜团,还是得看看#Object.java#hashCode的具体实现方法了。native方法本身非java实现,如果想要看源码,只有下载完整的jdk呗(openJdk1.8)。找到Object.c文件,查看上面的方法映射表发现,hashCode被映射到了一个叫JVM_IHashCode上去了。

  1. static JNINativeMethod methods[] = {
  2. {"hashCode", "()I", (void *)&JVM_IHashCode},
  3. {"wait", "(J)V", (void *)&JVM_MonitorWait},
  4. {"notify", "()V", (void *)&JVM_MonitorNotify},
  5. {"notifyAll", "()V", (void *)&JVM_MonitorNotifyAll},
  6. {"clone", "()Ljava/lang/Object;", (void *)&JVM_Clone},
  7. };

顺藤摸瓜去看看JVM_IHashCode到底干了什么?熟悉的味道,我猜在jvm.h里面有方法声明,那实现一定在jvm.cpp里面。

果然处处有惊喜,和猜想的没错,不过jvm.cpp对于JVM_IHashCode的实现调用的是ObjectSynchronizer::FastHashCode的方法。看来革命尚未成功啊!

  1. JVM_ENTRY(jint, JVM_IHashCode(JNIEnv* env, jobject handle))
  2. JVMWrapper("JVM_IHashCode");
  3. // as implemented in the classic virtual machine; return 0 if object is NULL
  4. return handle == NULL ? 0 : ObjectSynchronizer::FastHashCode (THREAD, JNIHandles::resolve_non_null(handle)) ;
  5. JVM_END

找了一会儿,没找到,这就尴尬了。后面百度了一下,发现声明在synchronizer.hpp 实现在这里synchronizer.cpp。感谢前辈们走出的路啊!

  1. // hashCode() generation :
  2. //
  3. // Possibilities:
  4. // * MD5Digest of {obj,stwRandom}
  5. // * CRC32 of {obj,stwRandom} or any linear-feedback shift register function.
  6. // * A DES- or AES-style SBox[] mechanism
  7. // * One of the Phi-based schemes, such as:
  8. // 2654435761 = 2^32 * Phi (golden ratio)
  9. // HashCodeValue = ((uintptr_t(obj) >> 3) * 2654435761) ^ GVars.stwRandom ;
  10. // * A variation of Marsaglia's shift-xor RNG scheme.
  11. // * (obj ^ stwRandom) is appealing, but can result
  12. // in undesirable regularity in the hashCode values of adjacent objects
  13. // (objects allocated back-to-back, in particular). This could potentially
  14. // result in hashtable collisions and reduced hashtable efficiency.
  15. // There are simple ways to "diffuse" the middle address bits over the
  16. // generated hashCode values:
  17.  
  18. static inline intptr_t get_next_hash(Thread * Self, oop obj) {
  19. intptr_t value = 0;
  20. if (hashCode == 0) {
  21. // This form uses global Park-Miller RNG.
  22. // On MP system we'll have lots of RW access to a global, so the
  23. // mechanism induces lots of coherency traffic.
  24. value = os::random();
  25. } else if (hashCode == 1) {
  26. // This variation has the property of being stable (idempotent)
  27. // between STW operations. This can be useful in some of the 1-0
  28. // synchronization schemes.
  29. intptr_t addrBits = cast_from_oop<intptr_t>(obj) >> 3;
  30. value = addrBits ^ (addrBits >> 5) ^ GVars.stwRandom;
  31. } else if (hashCode == 2) {
  32. value = 1; // for sensitivity testing
  33. } else if (hashCode == 3) {
  34. value = ++GVars.hcSequence;
  35. } else if (hashCode == 4) {
  36. value = cast_from_oop<intptr_t>(obj);
  37. } else {
  38. // Marsaglia's xor-shift scheme with thread-specific state
  39. // This is probably the best overall implementation -- we'll
  40. // likely make this the default in future releases.
  41. unsigned t = Self->_hashStateX;
  42. t ^= (t << 11);
  43. Self->_hashStateX = Self->_hashStateY;
  44. Self->_hashStateY = Self->_hashStateZ;
  45. Self->_hashStateZ = Self->_hashStateW;
  46. unsigned v = Self->_hashStateW;
  47. v = (v ^ (v >> 19)) ^ (t ^ (t >> 8));
  48. Self->_hashStateW = v;
  49. value = v;
  50. }
  51.  
  52. value &= markOopDesc::hash_mask;
  53. if (value == 0) value = 0xBAD;
  54. assert(value != markOopDesc::no_hash, "invariant");
  55. TEVENT(hashCode: GENERATE);
  56. return value;
  57. }
  58.  
  59.  
  60. intptr_t ObjectSynchronizer::FastHashCode(Thread * Self, oop obj) {
  61. if (UseBiasedLocking) {
  62. // NOTE: many places throughout the JVM do not expect a safepoint
  63. // to be taken here, in particular most operations on perm gen
  64. // objects. However, we only ever bias Java instances and all of
  65. // the call sites of identity_hash that might revoke biases have
  66. // been checked to make sure they can handle a safepoint. The
  67. // added check of the bias pattern is to avoid useless calls to
  68. // thread-local storage.
  69. if (obj->mark()->has_bias_pattern()) {
  70. // Handle for oop obj in case of STW safepoint
  71. Handle hobj(Self, obj);
  72. // Relaxing assertion for bug 6320749.
  73. assert(Universe::verify_in_progress() ||
  74. !SafepointSynchronize::is_at_safepoint(),
  75. "biases should not be seen by VM thread here");
  76. BiasedLocking::revoke_and_rebias(hobj, false, JavaThread::current());
  77. obj = hobj();
  78. assert(!obj->mark()->has_bias_pattern(), "biases should be revoked by now");
  79. }
  80. }
  81.  
  82. // hashCode() is a heap mutator ...
  83. // Relaxing assertion for bug 6320749.
  84. assert(Universe::verify_in_progress() || DumpSharedSpaces ||
  85. !SafepointSynchronize::is_at_safepoint(), "invariant");
  86. assert(Universe::verify_in_progress() || DumpSharedSpaces ||
  87. Self->is_Java_thread() , "invariant");
  88. assert(Universe::verify_in_progress() || DumpSharedSpaces ||
  89. ((JavaThread *)Self)->thread_state() != _thread_blocked, "invariant");
  90.  
  91. ObjectMonitor* monitor = NULL;
  92. markOop temp, test;
  93. intptr_t hash;
  94. markOop mark = ReadStableMark(obj);
  95.  
  96. // object should remain ineligible for biased locking
  97. assert(!mark->has_bias_pattern(), "invariant");
  98.  
  99. if (mark->is_neutral()) {
  100. hash = mark->hash(); // this is a normal header
  101. if (hash) { // if it has hash, just return it
  102. return hash;
  103. }
  104. hash = get_next_hash(Self, obj); // allocate a new hash code
  105. temp = mark->copy_set_hash(hash); // merge the hash code into header
  106. // use (machine word version) atomic operation to install the hash
  107. test = obj->cas_set_mark(temp, mark);
  108. if (test == mark) {
  109. return hash;
  110. }
  111. // If atomic operation failed, we must inflate the header
  112. // into heavy weight monitor. We could add more code here
  113. // for fast path, but it does not worth the complexity.
  114. } else if (mark->has_monitor()) {
  115. monitor = mark->monitor();
  116. temp = monitor->header();
  117. assert(temp->is_neutral(), "invariant");
  118. hash = temp->hash();
  119. if (hash) {
  120. return hash;
  121. }
  122. // Skip to the following code to reduce code size
  123. } else if (Self->is_lock_owned((address)mark->locker())) {
  124. temp = mark->displaced_mark_helper(); // this is a lightweight monitor owned
  125. assert(temp->is_neutral(), "invariant");
  126. hash = temp->hash(); // by current thread, check if the displaced
  127. if (hash) { // header contains hash code
  128. return hash;
  129. }
  130. // WARNING:
  131. // The displaced header is strictly immutable.
  132. // It can NOT be changed in ANY cases. So we have
  133. // to inflate the header into heavyweight monitor
  134. // even the current thread owns the lock. The reason
  135. // is the BasicLock (stack slot) will be asynchronously
  136. // read by other threads during the inflate() function.
  137. // Any change to stack may not propagate to other threads
  138. // correctly.
  139. }
  140.  
  141. // Inflate the monitor to set hash code
  142. monitor = ObjectSynchronizer::inflate(Self, obj, inflate_cause_hash_code);
  143. // Load displaced header and check it has hash code
  144. mark = monitor->header();
  145. assert(mark->is_neutral(), "invariant");
  146. hash = mark->hash();
  147. if (hash == 0) {
  148. hash = get_next_hash(Self, obj);
  149. temp = mark->copy_set_hash(hash); // merge hash code into header
  150. assert(temp->is_neutral(), "invariant");
  151. test = Atomic::cmpxchg(temp, monitor->header_addr(), mark);
  152. if (test != mark) {
  153. // The only update to the header in the monitor (outside GC)
  154. // is install the hash code. If someone add new usage of
  155. // displaced header, please update this code
  156. hash = test->hash();
  157. assert(test->is_neutral(), "invariant");
  158. assert(hash != 0, "Trivial unexpected object/monitor header usage.");
  159. }
  160. }
  161. // We finally get the hash
  162. return hash;
  163. }

没想到代码这么长,确实比

  1. int var;
  2. return &var;

长太多了。接下来看看这段代码到底干了些什么?

可以看到在get_next_hash函数中,有五种不同的hashCode生成策略。

第一种:是使用全局的os::random()随机数生成策略。os::random()的实现方式在os.cpp中,代码如下

  1. void os::init_random(unsigned int initval) {
  2. _rand_seed = initval;
  3. }
  4.  
  5.  
  6. static int random_helper(unsigned int rand_seed) {
  7. /* standard, well-known linear congruential random generator with
  8. * next_rand = (16807*seed) mod (2**31-1)
  9. * see
  10. * (1) "Random Number Generators: Good Ones Are Hard to Find",
  11. * S.K. Park and K.W. Miller, Communications of the ACM 31:10 (Oct 1988),
  12. * (2) "Two Fast Implementations of the 'Minimal Standard' Random
  13. * Number Generator", David G. Carta, Comm. ACM 33, 1 (Jan 1990), pp. 87-88.
  14. */
  15. const unsigned int a = 16807;
  16. const unsigned int m = 2147483647;
  17. const int q = m / a; assert(q == 127773, "weird math");
  18. const int r = m % a; assert(r == 2836, "weird math");
  19.  
  20. // compute az=2^31p+q
  21. unsigned int lo = a * (rand_seed & 0xFFFF);
  22. unsigned int hi = a * (rand_seed >> 16);
  23. lo += (hi & 0x7FFF) << 16;
  24.  
  25. // if q overflowed, ignore the overflow and increment q
  26. if (lo > m) {
  27. lo &= m;
  28. ++lo;
  29. }
  30. lo += hi >> 15;
  31.  
  32. // if (p+q) overflowed, ignore the overflow and increment (p+q)
  33. if (lo > m) {
  34. lo &= m;
  35. ++lo;
  36. }
  37. return lo;
  38. }
  39.  
  40. int os::random() {
  41. // Make updating the random seed thread safe.
  42. while (true) {
  43. unsigned int seed = _rand_seed;
  44. unsigned int rand = random_helper(seed);
  45. if (Atomic::cmpxchg(rand, &_rand_seed, seed) == seed) {
  46. return static_cast<int>(rand);
  47. }
  48. }
  49. }

根据代码注解的提示,随机数的生成策略是一种线性取余方式生成的。具体原理,看wiki吧(以后更新,或者大佬们不嫌弃分享一下呗)。

第二种:addrBits ^ (addrBits >> 5) ^ GVars.stwRandom。这里是第一次 看到和地址相关的变量,addrBits通过调用cast_from_oop方法得到。cast_from_oop实现在oopsHierarchy.cpp。具体代码如下

  1. template <class T> inline oop cast_to_oop(T value) {
  2. return (oop)(CHECK_UNHANDLED_OOPS_ONLY((void *))(value));
  3. }
  4. //以下部分内容来源于 oopsHierachy.hpp
  5. template <class T> inline T cast_from_oop(oop o) {
  6. return (T)(CHECK_UNHANDLED_OOPS_ONLY((void*))o);
  7. }

很遗憾的是我还是没有看到 cast_to_oop具体是怎么实现的,后面会更新的

第三种:敏感测试

value = 1;    

第四种:自增序列

 value = ++GVars.hcSequence;

第五种:官方将会默认。利用位移生成随机数

  1. // Marsaglia's xor-shift scheme with thread-specific state
  2. // This is probably the best overall implementation -- we'll
  3. // likely make this the default in future releases.
  4. unsigned t = Self->_hashStateX;
  5. t ^= (t << 11);
  6. Self->_hashStateX = Self->_hashStateY;
  7. Self->_hashStateY = Self->_hashStateZ;
  8. Self->_hashStateZ = Self->_hashStateW;
  9. unsigned v = Self->_hashStateW;
  10. v = (v ^ (v >> 19)) ^ (t ^ (t >> 8));
  11. Self->_hashStateW = v;
  12. value = v;

最后来回答 一开始的问题。


1.hashCode 是怎么来的?——原来有很多,自增序列,随机数,内存地址。这里又有个新问题产生了,为什么不用时间戳了?

2.可以预测值?——这很难说啊!

 

 

 





posted @ 2021-07-07 14:55  CharyGao  阅读(61)  评论(0)    收藏  举报