java默认的hashcode方法到底得到的是什么?

hashcode方法会影响jvm性能?听上去天方夜谭,实际上蕴藏着一些微小的原理,接下来让我们走进hashcode方法,一探native方法源头。

默认实现是什么?
调用hashCode方法默认返回的值被称为identity hash code(标识哈希码),接下来我们会用标识哈希码来区分重写hashCode方法。如果一个类重写了hashCode方法,那么通过调用System.identityHashCode(Object o)方法获得标识哈希码。

在hashCode方法注释中,说hashCode一般是通过对象内存地址映射过来的。

As much as is reasonably practical, the hashCode method defined by
 class {@code Object} does return distinct integers for distinct
 objects. (This is typically implemented by converting the internal
 address of the object into an integer, but this implementation
 technique is not required by the
 Java<font size="-2"><sup>TM</sup></font> programming language.)

但是了解jvm的同学肯定知道,不管是标记复制算法还是标记整理算法,都会改变对象的内存地址。鉴于jvm重定位对象地址,但该hashCode又不能变化,那么该值一定是被保存在对象的某个地方了。

我们推测,很有可能是在第一次调用hashCode方法时获取当前内存地址,并将其保存在对象的某个地方,当下次调用时,只用从对象的某个地方获取值即可。但这样实际是有问题的,你想想,如果对象被归集到别的内存上了,那在对象以前的内存上创建的新对象其hashCode方法返回的值岂不是和旧对象的一样了?这倒没关系,java规范允许这样做。

以上都是我们的猜测,并没有实锤。我们来看一下源码吧,可恶,hashCode方法是一个本地方法。

public native int hashCode();

真正的hashCode方法
hashCode方法的实现依赖于jvm,不同的jvm有不同的实现,我们目前能看到jvm源码就是OpenJDK的源码,OpenJDK的源码大部分和Oracle的JVM源码一致。

OpenJDK定义hashCode的方法在 src/share/vm/prims/jvm.hsrc/share/vm/prims/jvm.cpp

jvm.cpp:

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

ObjectSynchronizer :: FastHashCode() 也是通过调用identity_hash_value_for方法返回值的,System.identityHashCode()调用的也是这个方法。

708 intptr_t ObjectSynchronizer::identity_hash_value_for(Handle obj) {
709   return FastHashCode (Thread::current(), obj()) ;
710 }

我们可能会认为 ObjectSynchronizer :: FastHashCode() 会判断当前的hash值是否为0,如果是0则生成一个新的hash值。实际上没那么简单,来看看其中的代码。

685   mark = monitor->header();
...
687   hash = mark->hash();
688   if (hash == 0) {
689     hash = get_next_hash(Self, obj);
...
701   }
...
703   return hash;

上边的片段展示了hash值是如何生成的,可以看到hash值是存放在对象头中的,如果hash值不存在,则使用get_next_hash方法生成。

真正的 identity hash code 生成
在第二节中,我们终于找到了生成hash的最终函数 get_next_hash,这个函数提供了6种生成hash值的方法。

0. A randomly generated number.
1. A function of memory address of the object.
2. A hardcoded 1 (used for sensitivity testing.)
3. A sequence.
4. The memory address of the object, cast to int.
5. Thread state combined with xorshift (https://en.wikipedia.org/wiki/Xorshift)

那么默认用哪一个呢?根据globals.hpp,OpenJDK8默认采用第五种方法。而 OpenJDK7 和 OpenJDK6 都是使用第一种方法,即 随机数生成器。

大家也看到了,JDK的注释算是欺骗了我们,明明在678版本上都是随机生成的值,为什么要引导说是内存地址映射呢?我理解可能以前就是通过第4种方法实现的。

对象头格式
在上一节,我们知道了hash值是放在对象头里的,那就来了解一下对象头的结构吧。

markOop.hpp

30 // The markOop describes the header of an object.
31 //
32 // Note that the mark is not a real oop but just a word.
33 // It is placed in the oop hierarchy for historical reasons.
34 //
35 // Bit-format of an object header (most significant first, big endian layout below):
36 //
37 //  32 bits:
38 //  --------
39 //             hash:25 ------------>| age:4    biased_lock:1 lock:2 (normal object)
40 //             JavaThread*:23 epoch:2 age:4    biased_lock:1 lock:2 (biased object)
41 //             size:32 ------------------------------------------>| (CMS free block)
42 //             PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)
43 //
44 //  64 bits:
45 //  --------
46 //  unused:25 hash:31 -->| unused:1   age:4    biased_lock:1 lock:2 (normal object)
47 //  JavaThread*:54 epoch:2 unused:1   age:4    biased_lock:1 lock:2 (biased object)
48 //  PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
49 //  size:64 ----------------------------------------------------->| (CMS free block)
50 //
51 //  unused:25 hash:31 -->| cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && normal object)
52 //  JavaThread*:54 epoch:2 cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && biased object)
53 //  narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object)
54 //  unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block)

它的格式在32位和64位上略有不同,64位有两种变体,具体取决于是否启用了压缩对象指针。

对象头中偏向锁和hashcode的冲突
在上一节我们看到,normal object和biased object分别存放的是hashcode和java的线程id。因此也就是说如果调用了本地方法hashCode,就会占用偏向锁对象使用的位置,偏向锁将会失效,晋升为轻量级锁。

这个过程我们可以看看这个图:
在这里插入图片描述

这里我来简单解读一下,首先在jvm启动时,可以使用-XX:+UseBiasedLocking=true参数开启偏向锁。

接下来,如果偏向锁可用,那分配的对象中标记字格式为可包含线程ID,当未锁定时,线程ID为0,第一次获取锁时,线程会把自己的线程ID写到ThreadID字段内,这样,下一次获取锁时直接检查标记字中的线程ID和自身ID是否一致,如果一致就认为获取了锁,因此不需要再次获取锁。

假设这时有别的线程需要竞争锁了,此时该线程会通知持有偏向锁的线程释放锁,假设持有偏向锁的线程已经销毁,则将对象头设置为无锁状态,如果线程活着,则尝试切换,如果不成功,那么锁就会升级为轻量级锁。

这时有个问题来了,如果需要获取对象的identity hash code,偏向锁就会被禁用,然后给原先设置线程ID的位置写入hash值。

如果hash有值,或者偏向锁无法撤销,则会进入轻量级锁。轻量级锁竞争时,每个线程会先将hashCode值保存到自己的栈内存中,然后通过CAS尝试将自己新建的记录空间地址写入到对象头中,谁先写成功谁就拥有了该对象。

轻量级锁竞争失败的线程会自旋尝试获取锁一段时间,一段时间过后还没获取到锁,则升级为重量级锁,没获取锁的线程会被真正阻塞。

总结

  • OpenJDK默认的hashCode方法实现和对象内存地址无关,在版本6和7中,它是随机生成的数字,在版本8中,它是基于线程状态的数字。(AZUL-ZING的hashcode是基于地址的)
  • 在Hotspot中,hash值会存在标记字中。
  • hashCode方法和System.identityHashCode()会让对象不能使用偏向锁,所以如果想使用偏向锁,那就最好重写hashCode方法。
  • 如果大量对象跨线程使用,可以禁用偏向锁。
  • 使用-XX:hashCode=4来修改默认的hash方法实现。

参考

  • https://srvaroa.github.io/jvm/java/openjdk/biased-locking/2017/01/30/hashCode.html
  • https://www.cnblogs.com/javaminer/p/3892288.html
  • https://www.cnblogs.com/charlesblc/p/5994162.html
  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: HashCode方法是Object类中的一个方法,用于返回对象的哈希码值,它是一个int类型的整数。哈希码值是根据对象的内部信息计算出来的,可以用于快速地比较两个对象是否相等。在Java中,哈希码值经常被用于哈希表、集合等数据结构中。 ### 回答2: HashCode方法Java中Object类中的一个方法,用于返回对象的哈希码。哈希码是由对象的内部状态通过哈希算法转换而来的一个唯一标识。在Java中,哈希码经常被用于数据结构中,如HashMap、HashSet等。 HashCode方法的定义在Object类中,因此所有的Java对象都可以调用该方法。一般情况下,Object类的HashCode方法返回对象的内存地址的一个整数表示。但是,根据对象的不同,也可以自己重写HashCode方法以实现特定的哈希码生成逻辑。 HashCode方法具有以下特点: 1. 对于同一个对象,在程序的多次执行中,其HashCode值保持不变。 2. 如果两个对象通过equals方法相等,那么它们的HashCode值必须相同。但是,HashCode相等的对象不一定通过equals方法相等。 3. HashCode方法的性能要求比较高,因为在使用哈希表等数据结构时,需要频繁地计算对象的HashCode值。 重写HashCode方法时,需要遵循以下规范: 1. 如果equals方法返回true,那么HashCode方法必须返回相同的哈希码。 2. 对于不相等的对象,HashCode方法应返回尽可能不同的哈希码,以提高哈希表等数据结构的性能。 为了方便使用哈希表等数据结构,许多Java类如String、Integer等都已经重写了HashCode方法。对于自定义的类,如果需要使用哈希表等数据结构,可以根据对象的内部状态来生成哈希码,以满足不同对象的区分和快速访问的需求。 ### 回答3: HashCode方法Java中的一种方法,用于将一个对象的内存地址经过算法计算后转化为一个整型数值。HashCode方法通常在数据结构中用于快速查找和比较对象。 在Java中,每个对象都有一个默认HashCode方法默认HashCode方法会根据对象的内存地址生成一个唯一的整型数值。这意味着,不同的对象肯定会有不同的HashCodeHashCode方法在集合类中被广泛使用,例如在HashTable、HashMap、HashSet等类中。这些类根据对象的HashCode值来进行快速的查找和比较操作。在这些集合类中,对象的HashCode值被用于确定对象在集合中的位置或者用于判断两个对象是否相等。 为了正确使用HashCode方法,需要满足一些规则: 1. 如果两个对象通过equals方法相等,那么它们的HashCode值必须相等。 2. 如果两个对象的HashCode值相等,它们不一定通过equals方法相等,这是因为HashCode方法的计算过程有可能发生冲突。 3. 对象的HashCode值在同一程序的多次执行中可能会发生变化,所以HashCode值不能被作为对象的唯一标识。 通常情况下,当我们需要在自定义类中使用HashCode方法时,需要重写该方法。重写HashCode方法的目的是为了保证对象的相等性和性能。在重写HashCode方法时,一般会根据对象的属性进行计算,以尽量保证不同的对象拥有不同的HashCode值。同时,需要保证如果两个对象的属性相等,它们的HashCode值也相等。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值