当前位置: 首页 > 科技观察

一个关于HashCode的问题差点让我掉进无底洞

时间:2023-03-14 13:51:13 科技观察

你一个想法,我一个想法,我们交流之后,一个人有两个想法如果你不能简单地解释它,你还不够了解它现在我们陆续将Demo代码和技术文章整理到了一起。Github实践精选,方便大家阅读查看。本文也收录于此。我觉得很好。还请Star🌟:如果重写equals不重写hashcode,会有什么影响?这个问题从上午10:45到下午15:39一直在讨论(忽略这个假马赛克)。基础面试题,我也专门写了一篇关于Javaequals和hashCode的文章,这些题能不能解释清楚,主要解释下面的内容随着讨论的深入,题目逐渐聚焦到内存溢出和内存泄漏这两个词内存溢出VS内存leak在中文解释上有些相似,至少我的第一感觉,他们的区别是这样的(有没有人和我一样?)Memoryoverflow:OutofMemory(OOM),这个大家都很熟悉,也很容易理解。只是内存不够(啤酒【对象】太多,杯子【内存】装不下)。那么什么是内存泄漏呢?内存泄漏:MemoryLeak专门查了Leak'sDictionary的意思,Explanation1直白的翻译就是【通常是由于errors或者失误,通过一个开口进入或者逃逸】所以我对程序内存泄漏的理解更多的是:由于编程暴露了一些开口程序中的错误,导致某些对象进入这个写口,最终导致相关的问题。说白了就是程序有漏洞,调用不当就会出问题。所以接下来,我们主要看一下Java内存泄漏,以及问题产生的原因。hashCode和内存泄露有什么关系?我们也是内存泄露的身份证人,总不能说白话。官方对内存泄漏的相对解释是这样的:内存泄漏描述的是堆中有一些未使用的对象,但是垃圾收集器无法将它们从内存中移除(垃圾收集器会定期移除未被引用的对象,但从不收集被引用的对象)arestillreferenced),sotheyareunnecessarilymaintained这句话有点抽象,一张图你就可以理解,如果有更多有用但不能被垃圾收集器删除的对象,如下图,那么它会逐渐导致内存溢出(OOM),所以也可以总结为,OOM的原因之一可以是内存泄漏会导致哪些问题?内存泄漏将导致实际可用内存减少。在没有达到OOM的过程中,会出现奇怪的问题。当应用程序长时间连续运行时,性能会严重下降,毕竟可用内存变小自发且奇怪的应用程序崩溃应用程序偶尔会用完连接对象(这经常听到)最后的结果是OOM所以也可以反过来推理,如果出现上述问题,有可能是程序的某些部分发生了内存泄漏。有哪些常见的情况可能会导致内存泄漏?有哪些解决方案?导致内存泄露的常见情况及相应的解决方案我们直接来看一下静态成员变量的误用示例@Slf4jpublicclassStaticTest{publicstaticListlist=newArrayList<>();publicvoidpopulateList(){for(inti=0;i<10000000;i++){list.add(Math.random());}}publicstaticvoidmain(String[]args){newStaticTest().populateList();}}populateList()是一种公共方法,可能会被各种方法调用,从而导致列表无限增长。解决方法很简单。针对这种情况(也称为长周期对象指短周期对象),即把list放在方法内部,执行完方法栈帧会自动回收publicvoidpopulateList(){Listlist=newArrayList<>();for(inti=0;i<10000000;i++){list.add(Math.random());}}有的童鞋可能会有疑惑:在看Spring源码的时候,静态修饰的成员变量很多。它们也会导致内存泄漏吗?不是的,仔细看逻辑,都是在容器初始化过程中一次性加载的,所以不会像populateList那样随着调用次数的增加,List未关闭的流会无限扩大。在学习stream的时候,老师会在耳边反复说:一定要关闭stream...关闭stream...stream...?...child...因为每当我们建立新的connection或者打开一个流(例如数据库连接、输入流和Session对象),JVM会为这些资源分配内存,如果不关闭,这就是占用空间的“有用”对象,GC不会回收它们,当请求很大时,一个请求会创建一个新的流,最终没有Close,结果可想而知。流的解决方案很简单。事实上,您可以通过遵循相应的范式来避免此类问题。通过try/catch/finally范式关闭finally中的流。如果你使用Java7+版本,你还可以使用try-with-resources,这样代码会在编译后自动为你关闭流。您还可以使用Lombok的@Cleanup注释,如以下@CleanupInputStreamjobJarInputStream=newURL(jobJarUrl).openStream();@CleanupOutputStreamjobJarOutputStream=newFileOutputStream(jobJarFileStream);IOUtils.copy(jobJarInputStream,jobJarOutputStream);equals和hashCode的错误实现返回到这两个函数,很大一部分程序员不会主动重写equals和hashCode,尤其是Lombok@Data注解(这个注解会默认帮助重写这两个函数),会忽略这两个方法的实现。如果不小心使用它,可能会导致内存泄漏。让我们看一个非常简单的例子:publicclassMemLeakTest{publicstaticvoidmain(String[]args)throwsInterruptedException{Mapma??p=newHashMap<>();Personp1=newPerson("zhangsan",1);Personp2=newPerson("zhangsan",1);Personp3=newPerson("张三",1);map.put(p1,"张三");map.put(p2,"张三");map.put(p3,"张三")");System.out.println(map.entrySet().size());//运行结果:3}}@Getter@SetterclassPerson{privateStringname;privateIntegerid;publicPerson(Stringname,Integerid){this.name=name;this.id=id;}}Person类没有重写hashCode方法,那么Map的put方法会调用Object默认的hashCode方法publicVput(Kkey,Vvalue){returnputVal(hash(key),key,value,false,true);}staticfinalinthash(Objectkey){inth;return(key==null)?0:(h=key.hashCode())^(h>>>16);}p1,p2,p3在[业务]attribute它们是完全相同的三个对象,由于“对象地址”不同,生成的hashCode不同,最后都放到了Map中,会造成业务对象重复占用空间,所以这也是内存泄露的一种解决方法,解决方法很简单,给Person加一个Lombok@Data注解,自动帮你重写hashCode方法,或者在IDE中手动生成,再运行,结果为1。如果满足业务需求,重写hashCode确实可以避免。重复对象的加入,游戏就这样结束了吗?看一个例子publicstaticvoidmain(String[]args)throwsInterruptedException{//注:HashSet底层也是一个Map结构Setset=newHashSet();Personp1=newPerson("zhangsan",1);Personp2=newPerson("lisi",2);Personp3=newPerson("wanger",3);set.add(p1);set.add(p2);set.add(p3);系统.out.println(set.size());//运行结果:3p3.setName("wangermao");set.remove(p3);System.out.println(set.size());//运行结果:3set.add(p3);System.out.println(set.size());//运行结果:4}从运行结果可以看出,set.remove(p3)并没有删除成功,因为p3.setName("wangermao"),重新计算p3的hashCode会发生变化,所以remove的时候会找不到对应的Node,给添加相同对象的“机会”,导致业务中无用的对象被引用,所以可以说这也是一种内存泄漏看运行结果:所以对于这样的操作,最好先去掉,再改属性,最后再添加。看到这里,你应该已经发现,要解决hashCode相关的问题,必须要充分了解集合的特点,注意类是否重写了这个方法和它们的实现方法,以避免内存泄漏。在ThreadLocal群消息的最后,小姐姐留下了【ThreadLocal】四个字,带着深深的功德离开了。一看就是高手ThreadLocal是一个多线程面试的高频考点。它的优点是可以快速方便的实现线程隔离,但是大家也都知道它是一把双刃剑,因为如果使用不当可能会造成内存泄漏。在实际工作中我们都是使用线程池来管理线程“具体可以参考我会手动创建线程,为什么要使用线程池”,这种方式可以让线程被复用(故意不允许被GC回收),现在,如果任何类创建了一个ThreadLocal变量,但不显式删除它,那么即使在Web应用程序停止后,该对象的副本仍将保留在工作线程中,防止该对象被垃圾回收,因此滥用也会造成内存泄漏解决方法解决方法还是很简单的。还是按照标准调用ThreadLocal的remove()方法来移除当前线程变量值。它也可以被视为一种资源,使用try/finally范式。万一运行时出现异常,也可以在finally中removetry{threadLocal.set(System.nanoTime());//businesscode}finally{threadLocal.remove();}我想小姐姐一定是高手了总的来说,内存泄漏的原因有很多,比如内部类引用外部类等,这里不做解释,只解释几种很常见的可能导致内存泄漏的场景。内存泄漏不易检测,因此有时需要借助工具。帮助JVisualVMJVisualvm[可视化JVM],可以分析JDK1.6及以上版本运行JVM时的JVM参数、系统参数、堆栈、CPU使用率等信息。它可以分析本地应用程序和远程应用程序。它带有JDK1.6及更高版本。该工具的使用将不会被解释。要快速使用这个工具,只需要在IDE中安装一个VisualVMLauncher插件,然后进行基本配置,然后在IDE的右上角或者当前类的鼠标右键,点击即可运行并查看它。运行可视化虚拟机。就是这个。生成?不知道为什么脑海里的印象是这样的。我深深地接受了ObjecthashCode是根据对象的内存地址生成的。这次只是想探究一下hashCode的本质,真的打破了我的固有印象(以JDK1.8为例)OpenJDK在下面两个文件src/share/vm/prims/jvm.hsrc/share中定义了hashCode方法/vm/prims/jvm.cpp一步步看下去,最后来到get_next_hash方法,方便大家在这里查看方法截图:总的来说,有6种方式生成hashCode:0:A随机生成的数字1:对象内存地址的函数2:硬编码的1(用于敏感性测试)。3:序列。4:对象的内存地址,转为int5(else):线程状态结合xor-shift[1]JDK1.8用的是哪一个?![](https://rgyb.sunluomeng.top/ScreenShot2020-08-01at1.35.29PM.png)可以看出JDK1.8中生成hashCode的方式是5,也就是走程序的else路径,也就是使用Xorshift,和不是之前想的对象内存地址“1”。以为老版本用的是对象内存地址的方法,所以继续看其他版本。从图中可以看出JDK1.6[2]和JDK1.7[3]版本生成hashCode随机数“1”的形式和我们原来想的不一样。其他版本没有继续查询。至于对象的内存地址生成的“传世”hashCode,我没有进一步研究。请同学们留言赐教。那么问题来了:假设使用的是JDK1.6或者JDK1.7,他们生成hashCode的方式是随机生成的。一个对象多次调用hashCode会不会有不同的hashCode?(排除服务重启的情况)显然不应该,因为如果每次都变,集合中存储的对象很容易丢失,那么问题又来了:它们存在于哪里?hash值存储在对象头中,我们也知道线程ID也可能存储在对象头中,所以它们在某些情况下仍然可能与对象头中的hashCode和偏向锁有冲突。jvm启动时,可以使用-XX:+UseBiasedLocking=true开启偏向锁,(关于偏向锁、轻量级锁、重量级锁,可以参考synchronized的相关文档),这里是OpenJDK中的图片Wiki[4]解释了整个冲突过程,所以调用Object的hashCode()方法或System.identityHashCode()方法会使对象无法使用偏向锁。你应该知道这里。如果还想使用偏向锁,最好重写hashCode()方法,避免偏向锁失效。为了解决群的这个问题,发现同时新世界差点让我掉进【问无底洞】,不过通过这篇文章,你应该明白内存溢出和内存泄漏的区别,以及他们的解决方案。另外,hashCode[5]的生成方式实在是让人大跌眼镜。如果你知道“hashCode是根据对象内存地址的来源生成的”,请留言赐教。此外,较小的hashCode可能会使偏向锁失效。所有这些细节都可能导致程序崩溃,所以不要因为“恶”的渺小而去做,也不要因为“善”的渺小而去做。不,良好的编程习惯可以避免很多问题。当然,要想更好的理解内存泄漏,就需要对GC机制有更好的理解。想要更好的理解GC,当然需要更好的理解JVM。后续我们慢慢分析。为了清除ThreadLocal线程变量值,用ThreadLocal.set(null)代替ThreadLocal.remove()是否达到同样的效果?你遇到过无法检测到的内存泄漏吗?参考[1]xor-shift算法:https://en.wikipedia.org/wiki/Xorshift[2]JDK1.6代码:http://hg.openjdk.java.net/jdk6/jdk6/hotspot/file/5cec449cc409/src/share/vm/runtime/globals.hpp#l1128[3]JDK1.7代码:http://hg.openjdk.java.net/jdk7u/jdk7u/hotspot/file/5b9a416a5632/src/share/vm/runtime/globals.hpp#l1100[4]OpenJDKWiki:https://wiki.openjdk.java.net/display/HotSpot/Synchronization[5]默认hashCode生成方式:https://srvaroa.github.io/jvm/java/openjdk/biased-locking/2017/01/30/hashCode.html本文转载自微信公众号“日工一冰”,可通过以下二维码关注。转载本文请联系日工一兵公众号。Java中hashCode和equals方法的正确使用关于equals和hashCode,看完这篇文章真的够了!