当前位置: 首页 > 后端技术 > Java

HashCode技术内幕

时间:2023-04-02 00:17:33 Java

3HashCode内幕技巧:面试常问/常用/常犯的错误什么是hashCode?是对象的内存地址吗?1)直接使用内存地址?目标:通过一个demo验证hasCode是否为内存地址publicnativeinthashCode();com.hashcode.HashCodeTestpackagecom.hashcode;importorg.openjdk.jol.vm.VM;importjava.util.ArrayList;importjava.util.List;publicclassHashCodeTest{//目的:只要出现重复,hashcode就不是内存地址,但需要证明(JVM代码证明)publicstaticvoidmain(String[]args){List<整数>integerList=newArrayList();整数=0;for(inti=0;i<150000;i++){//创建一个新对象Objectobject=newObject();if(integerList.contains(object.hashCode())){num++;//出现重复(内存地址肯定不会重复)}else{integerList.add(object.hashCode());//不重复}}System.out.println(num+"重复哈希码");System.out.println("列表总大小"+integerList.size()+"单位");}}15万次循环,重复出现,表示hashCode不是内存地址(严格来说肯定不是直接取的内存地址)。想一想,为什么不能直接使用内存地址呢?提示:jvm垃圾回收算法、对象迁移……那到底是什么?它是如何产生的?2)如果不是地址,它在哪里?既然不是内存地址,肯定是存放在某处的,那么存放在哪里呢?答:在对象头!(绘画类在jvm内存中的布局)对象头分为两部分,一部分是上面指向类描述的地址Klass,另一部分是Markword,我们这里要找的hashcode在Markword中!(不记得mark位的意思了!)32位:64位:3)什么时候生成的?新的时候有哈希码吗??给我看代码!我们使用代码校验包com.hashcode;importorg.openjdk.jol.info.ClassLayout;importorg.openjdk.jol.vm.VM;publicclassShowHashCode{publicstaticvoidmain(String[]args){ShowHashCodea=new显示哈希码();//jvm信息System.out.println(VM.current().details());System.out.println("--------------------------");//调用前打印a对象的头部信息//以表格的形式打印对象布局System.out.println(ClassLayout.parseInstance(a).toPrintable());系统。out.println("------------------------");//调用System.out.println(Integer.toHexString(a.hashCode()));打印对象的hashcode值System.out.println(ClassLayout.parseInstance(a).toPrintable());System.out.println("------------------------");//有线程和重量级锁时,看对象头newThread(()->{try{synchronized(a){Thread.sleep(5000);}}catch(InterruptedExceptione){e.printStackTrace();}})。开始();System.out.println(Integer.toHexString(a.hashCode()));System.out.println(ClassLayout.parseInstance(a).toPrintable());}}结果分析结论:不调用时,这个值为空。当你第一次调用hashCode方法时,它会被生成。加锁后不知道跑到哪里去了……4)怎么生成的?接上上文,我们来考察一下它的详细生成和运动过程我们都知道这个积是一个本地方法publicnativeinthashCode();那么就需要使用上面提到的方法通过JVM虚拟机的源码查看hashcode的生成1)首先从Object.c\native\java\lang\Object.cJNIEXPORTvoid中找到hashCode映射src\shareJNICALL//jni调用//全路径:java_lang_Object_registerNatives是java对应包下的方法java_java_lang_Object_registerNatives(JNIEnv*env,jclasscls){//jni环境调用;以下参数methods对应javamethod(*env)->RegisterNatives(env,cls,methods,sizeof(methods)/sizeof(methods[0]));}JAVA-------------------->C++函数对应//JAVA方法(返回值)----->C++函数对象staticJNINativeMethodmethods[]={//JAVA方法返回值(参数)c++函数{"hashCode","()I",(void*)&JVM_IHashCode},{"wait","(J)V",(void*)&JVM_MonitorWait},{"notify","()V",(void*)&JVM_MonitorNotify},{"notifyAll","()V",(void*)&;JVM_MonitorNotifyAll},{"clone","()Ljava/lang/Object;",(void*)&JVM_Clone},};JVM_IHashCod在哪里?2)全局搜索JVM_IHashCode根本找不到这个方法名,只有这个有点像,那是什么?src\share\vm\prims\jvm.cpp/*JVM_ENTRY是一个预处理器宏,它添加了一些样板代码,这些代码对于HotSpotJVMAPI的所有功能都是通用的。这个API是JDK类库的本机代码和JVM.JVM_ENTRY是一个预加载的宏,它向jvm的所有函数添加了一些样板代码。这个api是native方法和jdk之间的一个连接层。那么,这里就是生成hashCode的逻辑了!*/JVM_ENTRY(jint,JVM_IHashCode(JNIEnv*env,jobjecthandle))JVMWrapper("JVM_IHashCode");//调用了ObjectSynchronizer对象的FastHashCodereturnhandle==NULL?0:ObjectSynchronizer::FastHashCode(THREAD,JNIHandles::resolve_non_null(handle));JVM_END3)继续,ObjectSynchronizer::FastHashCode先说说生成过程,留个印象:intptr_tObjectSynchronizer::FastHashCode(Thread*Self,oopobj){//是否开启偏向锁(Biased:bias,tendency)if(UseBiasedLocking){//如果当前对象处于偏向锁状态if(obj->mark()->has_bias_pattern()){Handlehobj(自我,对象);assert(Universe::verify_in_progress()||!SafepointSynchronize:is_at_safepoint(),“此处的VM线程不应看到偏差”);//然后撤销偏向锁(达到无锁状态,revoke:abolish)BiasedLocking::revoke_and_rebias(hobj,false,JavaThread::current());obj=hobj();//Assert,看是否取消成功(取消后状态为解锁)assert(!obj->mark()->has_bias_pattern(),"biasesshouldberevokedbynow");}}//...ObjectMonitor*monitor=NULL;markOoptemp,test;intptr_thash;//读出稳定标记;防止对象obj膨胀;//如果膨胀,就等它展开再读出来markOopmark=ReadStableMark(obj);//偏向锁(即无锁状态)是否被撤销(中性:中性,无偏无偏)if(mark->is_neutral()){//从mark头中获取hash值hash=mark->hash();//如果有,直接返回hashcode(xor)if(hash){//如果有hash,就返回它returnhash;}//如果没有,生成一个新的(get_next_hash)hash=get_next_hash(Self,obj);//分配一个新的哈希码//生成后,原子性设置,将哈希放在对象中header,这样下次就可以直接取了:cmpxchg_ptr(temp,obj->mark_addr(),标记);if(test==mark){返回散列;}//如果原子操作失败,我们必须将标头膨胀//到重量级监视器中。我们可以添加更多此处的代码//用于快速路径,但不值得如此复杂。//如果已经升级为重量级锁,则找到它的监视器//也就是我们所说的内置锁(objectMonitor),这是c中的数据类型//因为锁升级后,位inmark不再存储hashcode,而是指向monitor的地址//升级后的mark呢?移到了c的monitorelseif(mark->has_monitor()){//沿着monitor寻找header,即对象headertemp=monitor->header();assert(temp->is_neutral(),"invariant");//找到header后得到hash,返回hash=temp->hash();如果(哈希){返回哈希;}//跳到下面的代码,减少代码量}elseif(Self->is_lock_owned((address)mark->locker())){//轻量锁也从java对象头移到c,调用helpertemp=mark->displaced_mark_helper();//这是一个轻量级监视器拥有assert(temp->is_neutral(),"invariant");hash=temp->hash();//通过当前线程,检查是否找到被置换的//找到,返回if(hash){//headercontainshash代码返回散列;}}......一个小问题:为什么需要先撤销偏向锁到无锁状态,再生成hashcode?这和锁有什么关系?答:在markword中,存放hashcode的字节位置被偏向锁占用了!偏向锁存储锁持有者的线程id(参考上面markword图)扩展:关于hashCode生成算法(了解)//hashCode()generation://涉及到c++算法领域,有兴趣的同学自己动手research//Possibilities://*{obj,stwRandom}的MD5Digest//*{obj,stwRandom}的CRC32或任何线性反馈移位寄存器函数。//*DES或AES样式的SBox[]机制//*基于Phi的方案之一,例如://2654435761=2^32*Phi(黄金比例)//HashCodeValue=((uintptr_t(obj)>>3)*2654435761)^GVars.stwRandom;//*Marsaglia的shift-xorRNG方案的一种变体。//*(obj^stwRandom)很吸引人,但可能会导致//相邻对象的hashCode值具有不良的规律性//(背靠背分配的对象,在特别的)。这可能//导致哈希表冲突并降低哈希表效率。//有一些简单的方法可以“扩散”中间地址位s在//生成的hashCode值上//staticinlineintptr_tget_next_hash(Thread*Self,oopobj){intptr_tvalue=0;if(hashCode==0){//这种形式使用了一个不受保护的全局Park-MillerRNG,//所以两个线程有??可能竞争并生成相同的RNG。//在MP系统上,我们将对全局进行大量RW访问,因此//机制会导致大量一致性流量。value=os::random();//返回随机数}elseif(hashCode==1){//这种变体具有在STW操作之间稳定(幂等)的特性。这在某些1-0//同步方案中很有用。//与地址有关,但与地址无关;右移+异或算法intptr_taddrBits=cast_from_oop(obj)>>3;value=addrBits^(addrBits>>5)^GVars.stwRandom;//随机数位移异或计算}elseif(hashCode==2){value=1;//return1}elseif(hashCode==3){value=++GVars.hcSequence;//返回一个序列号}elseif(hashCode==4){value=cast_from_oop(obj);//不是地址}else{//常用//Marsaglia的具有线程特定状态的异或移位方案//这可能是最好的总体实施——我们//可能会在未来的版本中将其设为默认值。//Massalia教授写的Xor-shift随机数算法(异或随机算法)unsignedt=Self->_hashStateX;t^=(t<<11);自我->_hashStateX=自我->_hashStateY;Self->_hashStateY=Self->_hashStateZ;自我->_hashStateZ=自我->_hashStateW;unsignedv=Self->_hashStateW;v=(v^(v>>19))^(t^(t>>8));self->_hashStateW=v;价值=v;}5)总结通过分析虚拟机的源码,我们证明hashCode并不是直接使用的内存地址,而是采用一定的算法生成hashcode值存储在markword中,共享一个bit与锁,这导致与锁状态相关。如果是偏向锁:一旦调用了hashcode,就会撤销偏向锁,保存hashcode。占用了markword,对象又回到了解锁状态,那么为什么会有线程强行使用对象的锁呢?对象不能再回到偏向锁状态而是升级为重量级锁哈希码被移动到markword后面的c的对象监视器,并从那里取走如果本文对您有帮助,请关注并点赞;有什么建议也可以留言或私信。您的支持是我坚持创作的动力