JDK核心JAVA源码解析(9)

本文基于 OpenJDK 11, HotSpot 虚拟机

在开发过程中我们可能会经常接触到hashcode这个方法来生成哈希码,那么底层是如何实现的?使用时有何注意点呢?

hashcode() 方法底层实现

hashcode()Object的方法:

1@HotSpotIntrinsicCandidate 2public native int hashCode();

它是一个native的方法,并且被@HotSpotIntrinsicCandidate注解修饰,证明它是一个在HotSpot中有一套高效的实现,该高效实现基于CPU指令。

具体的实现参考源码synchronizer.cpp

1static inline intptr_t get_next_hash(Thread* self, oop obj) { 2 intptr_t value = 0; 3 if (hashCode == 0) { 4 value = os::random(); 5 } else if (hashCode == 1) { 6 intptr_t addr_bits = cast_from_oop<intptr_t>(obj) >> 3; 7 value = addr_bits ^ (addr_bits >> 5) ^ GVars.stw_random; 8 } else if (hashCode == 2) { 9 value = 1; 10 } else if (hashCode == 3) { 11 value = ++GVars.hc_sequence; 12 } else if (hashCode == 4) { 13 value = cast_from_oop<intptr_t>(obj); 14 } else { 15 unsigned t = self->_hashStateX; 16 t ^= (t << 11); 17 self->_hashStateX = self->_hashStateY; 18 self->_hashStateY = self->_hashStateZ; 19 self->_hashStateZ = self->_hashStateW; 20 unsigned v = self->_hashStateW; 21 v = (v ^ (v >> 19)) ^ (t ^ (t >> 8)); 22 self->_hashStateW = v; 23 value = v; 24 } 25 26 value &= markWord::hash_mask; 27 if (value == 0) value = 0xBAD; 28 assert(value != markWord::no_hash, "invariant"); 29 return value; 30}

可以看出,根据hashcode这个全局变量的取值,决定用何种策略生成哈希值,查看globals.hpp来看是哪一种变量:

 experimental(intx, hashCode, 5, "(Unstable) select hashCode generation algorithm")  

发现是一个experimental的 JVM 变量,这样的话,想要修改,必须添加额外的参数,如下所示:

-XX:+UnlockExperimentalVMOptions -XX:hashCode=2

并且,这个hashCode默认为5。

哈希值是每次hashcode()方法调用重计算么?

对于没有覆盖hashcode()方法的类,实例每次调用hashcode()方法,只有第一次计算哈希值,之后哈希值会存储在对象头的 标记字(MarkWord) 中。

image (上图来自于:https://www.cnblogs.com/helloworldcode/p/11914053.html)

如果进入各种锁状态,那么会缓存在其他地方,一般是获取锁的线程里面存储,恢复无锁(即释放锁)会改回原有的哈希值

关于对象头结构,以及对象存储结构,感兴趣的话,可以参考:Java GC详解 - 1. 理解Java对象结构

-XX:hashCode=0 利用 Park-Miller 伪随机数生成器生成哈希值

1if (hashCode == 0) { 2 value = os::random(); 3}

调用 os 的 random 方法生成随机数。这个方法的实现方式是: os.cpp

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

其中,random_helper 就是随机数的生成公式的实现,公式是: image 这里,a=16807, c=0, m=2^31-1

由于这些随机数都是采用的同一个生成器,会 CAS 更新同一个 seed,如果有大量的生成的新对象并且都调用hashcode()方法的话,可能会有性能问题。重复调用同一个对象的hashcode()方法不会有问题,因为之前提到了是有缓存的。

-XX:hashCode=1或者4 基于对象指针 OOPs

OOPs(Ordinary Object Pointers)对象指针是对象头的一部分。关于对象头结构,以及对象存储结构,感兴趣的话,可以参考:Java GC详解 - 1. 理解Java对象结构。可以简单理解为对象在内存中的地址的描述。

1else if (hashCode == 1) { 2 // This variation has the property of being stable (idempotent) 3 // between STW operations. This can be useful in some of the 1-0 4 // synchronization schemes. 5 intptr_t addr_bits = cast_from_oop<intptr_t>(obj) >> 3; 6 value = addr_bits ^ (addr_bits >> 5) ^ GVars.stw_random; 7} 8else if (hashCode == 4) { 9 value = cast_from_oop<intptr_t>(obj); 10}

cast_from_oop很简单,就是获取oop的实现基类oopDesc的指向地址(oopDesc描述了OOP的基本组成,感兴趣可以参考:Java GC详解 - 1. 理解Java对象结构):

1template <class T> inline T cast_from_oop(oop o) { 2 return (T)(CHECK_UNHANDLED_OOPS_ONLY((oopDesc*))o); 3}

-XX:hashCode=4,直接用oop的地址作为哈希值。-XX:hashCode=1则是经过变换的,每次发生 Stop The World (STW)stw_random会发生改变,通过这个addr_bits ^ (addr_bits >> 5) ^ GVars.stw_random变换减少哈希碰撞,让哈希值更散列化。

想更深入了解 Stop the world,可以参考:JVM相关 - SafePoint 与 Stop The World 全解(基于OpenJDK 11版本)

-XX:hashCode=2 敏感测试,恒定为1

1else if (hashCode == 2) { 2 value = 1; // for sensitivity testing 3}

主要用于测试某些集合是否对于哈希值敏感。

-XX:hashCode=3 自增序列

1else if (hashCode == 3) { 2 value = ++GVars.hc_sequence; 3} 4 5struct SharedGlobals { 6 // omitted 7 DEFINE_PAD_MINUS_SIZE(1, DEFAULT_CACHE_LINE_SIZE, sizeof(volatile int) * 2); 8 // Hot RW variable -- Sequester to avoid false-sharing 9 volatile int hc_sequence; 10 DEFINE_PAD_MINUS_SIZE(2, DEFAULT_CACHE_LINE_SIZE, sizeof(volatile int)); 11}; 12static SharedGlobals GVars;

每创建一个新对象,调用哈希值,这个自增数+1,可以看出,散列性极差,很容易哈希碰撞。

-XX:hashCode=5 默认实现

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

采用的算法是 Marsaglia's xor-shift 随机数生成法。主要是这篇论文提出的一种快速并且散列性好的哈希算法。

特殊的哈希值导致某些场景的问题

我们经常使用某个对象或者某个字段的哈希值,通过对于某个数组长度取模,获取到下标,取出数组对应下标的对象,进行进一步处理。这在负载均衡,任务调度,线程分配很常见。那下面这段代码是否有问题呢?

1//获取userId这个字符串的哈希值的绝对值 2int index = Math.abs(userId.hashCode()); 3//返回哈希值取模之后的下标的对象 4return userAvatarList.get(index % userAvatarList.size()).getUrl();

通常大多数情况下,是没有问题的,但是假设userId是这几个哈希值为Integer.MIN_VALUE的字符串:

1System.out.println("polygenelubricants".hashCode()); 2System.out.println("GydZG_".hashCode()); 3System.out.println("DESIGNING WORKHOUSES".hashCode());

输出:

1-2147483648 2-2147483648 3-2147483648

对于这些值,如果你用Math.abs()取绝对值的话,我们知道Math.abs(Integer.MIN_VALUE)还是等于Integer.MIN_VALUE,这是因为底层实现:

1public static int abs(int a) { 2 return (a < 0) ? -a : a; 3}

-Integer.MIN_VALUEInteger.MIN_VALUE是相等的。Integer.MIN_VALUE取模还是负数,这样取下标对应的对象的时候,就会报异常

所以,需要修改为:

1int index = Math.abs(userId.hashCode() % userAvatarList.size()); 2return userAvatarList.get(index).getUrl();
点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

2020年前端实用代码段,为你的工作保驾护航

有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )