一个HashCode问题的追问,差点让我陷入无底洞

一个HashCode问题的追问,差点让我陷入无底洞
内存溢出 VS 内存泄漏这两个词在中文解释上有些类似,至少给个人第一感受,他们的差异是这样的(有人和我同样吗?)
做者:an日拱一兵 来源:日拱一兵|2020-08-04 08:44
收藏
分享html

你有一个思想,我有一个思想,咱们交换后,一我的就有两个思想
If you can NOT explain it simply, you do NOT understand it well enough
现陆续将Demo代码和技术文章整理在一块儿 Github实践精选 ,方便你们阅读查看,本文一样收录在此,以为不错,还请Star
原由
原由是群里的一位童鞋忽然问了这么问题:
若是重写 equals 不重写 hashcode 会有什么影响?
这个问题从上午10:45 开始陆续讨论,到下午15:39 接近尾声 (忽略这形同虚设的马赛克)
一个HashCode问题的追问,差点让我陷入无底洞
这是一个好问题,更是一个高频基础面试题,我还曾经专门写过一篇文章 Java equals 和 hashCode 的这几个问题能够说明白吗, 主要说明了如下内容
一个HashCode问题的追问,差点让我陷入无底洞
随着讨论的进行,问题慢慢集中在内存溢出和内存泄漏的问题上
内存溢出 VS 内存泄漏
这两个词在中文解释上有些类似,至少给个人第一感受,他们的差异是这样的(有人和我同样吗?)
一个HashCode问题的追问,差点让我陷入无底洞
内存溢出:Out of Memory (OOM) ,这个你们都很熟悉了,理解起来也很简单,就是内存不够用了(啤酒【对象】太多,杯子【内存】装不下了)
那啥是内存泄漏呢?
内存泄漏:Memory
Leak特地查了一下 Leak 的字典含义,解释1的直白翻译是【一般是因为错误或失误,从一个开口 进入或逃脱】
一个HashCode问题的追问,差点让我陷入无底洞
因此程序中的内存泄漏个人理解更可能是:因为程序的编写错误暴漏出一些 开口,致使一些对象进入这写开口,最终致使相关问题,进一步说白了,程序有漏洞,不当的调用就会出问题
因此接下来咱们主要来看看 Java 内存泄漏,以及问题的原由 hashCode 和内存泄漏到底有哪些关系
内存泄漏
咱也是一个有身份证的人,不能总讲大白话,相对官方的内存泄漏解释是这样滴:
内存泄漏说明的是这样一种状况:堆中存在一些再也不使用的对象,但垃圾收集器没法将它们从内存中删除(垃圾收集器按期删除未引用的对象,但从不收集仍在引用的对象),所以对它们进行了没必要要的维护
这句话略显抽象,一张图你就能明白
一个HashCode问题的追问,差点让我陷入无底洞
若是有用的、但垃圾收集器又不能删除的对象增多,就像下图这样,那么就会逐渐致使内存溢出(OOM)了
一个HashCode问题的追问,差点让我陷入无底洞
因此也能够总结为,OOM 的缘由之一多是内存泄漏致使的
内存泄漏会带来哪些问题
内存泄漏,会致使真正可用内存变少,在没达到 OOM 的这个过程当中,就会出现奇奇怪怪的问题java

  1. 当应用程序长时间连续运行时,性能会严重降低,毕竟可用内存变小
  2. 自发的和奇怪的应用程序崩溃
  3. 应用程序偶尔会耗尽链接对象(这个常常据说吧)
  4. 最终的结果是 OOM
    因此也能够反过来推理,若是发生上述问题,有可能程序的某些地方发生了内存泄漏那常见的哪些情形可能会引发内存泄漏呢?又有哪些解决办法呢?
    会引发内存泄漏的常见情形与相应解决办法
    静态成员变量的乱用
    直接来看一个例子
  5. @Slf4j
  6. public class StaticTest {
  7. public static List<Double> list = new ArrayList<>();
  8. public void populateList() {
  9. for (int i = 0; i < 10000000; i++) {
  10. list.add(Math.random());
  11. }
  12. }
  13. public static void main(String[] args) {
  14. new StaticTest().populateList();
  15. }
  16. }
    populateList() 是一个 public 方法,可能被各类调用,致使 list 无限增大
    解决办法
    解决办法很简单,针对这种情形(也就是一般所说的长周期对象引用短周期对象),就是将 list 放到方法内部,方法栈帧执行完自动就会被回收了
  17. public void populateList() {
  18. List<Double> list = new ArrayList<>();
  19. for (int i = 0; i < 10000000; i++) {
  20. list.add(Math.random());
  21. }
  22. }
    有童鞋可能有疑问:
    看 Spring 源码时有好可能是 static 修饰的成员变量,难道它们也会致使内存泄漏?
    不是的,若是你仔细看逻辑,它们都是是在容器初始化的过程当中一次性加载的,因此不会像 populateList 随着调用次数的增长,无限撑大 List
    未关闭的流
    在学习流的时候老师就在耳边反复说:
    必定要关闭流... 闭流... 流... 㐬... 儿...
    由于每当咱们创建一个新的链接或打开一个流时(好比数据库链接、输入流和会话对象),JVM都会为这些资源分配内存,若是不关闭,这就是占用空间"有用"的对象, GC 就不会回收他们,当请求很大,来个请求就新建一个流,最终都还没关闭,结果可想而知
    解决办法
    流的解决办法很简单,其实主要遵循相应范式就能够避免此类问题
  23. 经过 try/catch/finally范式在 finally 关掉流
  24. 若是你用的 Java 7+ 的版本,也能够用 try-with-resources, 这样代码在编译后会自动帮你关闭流
  25. 也可使用 Lombok 的 @Cleanup 注解, 就像下面这样
  26. @Cleanup InputStream jobJarInputStream = new URL(jobJarUrl).openStream();
  27. @Cleanup OutputStream jobJarOutputStream = new FileOutputStream(jobJarFile);
  28. IOUtils.copy(jobJarInputStream, jobJarOutputStream);
    不正确的 equals 和 hashCode 实现
    又回到了这两个函数上,有很大一部分程序员不会主动重写 equals 和 hashCode,尤为是用 Lombok @Data 注解(该注解默认会帮助重写这两个函数)后,更会忽视这两个方法实现,一不当心的使就可能引发内存泄漏
    来看个很是简单的例子:
  29. public class MemLeakTest {
  30. public static void main(String[] args) throws InterruptedException {
  31. Map<Person, String> map = new HashMap<>();
  32. Person p1 = new Person("zhangsan", 1);
  33. Person p2 = new Person("zhangsan", 1);
  34. Person p3 = new Person("zhangsan", 1);
  35. map.put(p1, "zhangsan");
  36. map.put(p2, "zhangsan");
  37. map.put(p3, "zhangsan");
  38. System.out.println(map.entrySet().size()); // 运行结果:3
  39. }
  40. }
  41. @Getter
  42. @Setter
  43. class Person {
  44. private String name;
  45. private Integer id;
  46. public Person(String name, Integer id){
  47. this.name = name;
  48. this.id = id;
  49. }
  50. }
    Person 类没有重写 hashCode 方法,那 Map 的 put 方法就会调用 Object 默认的 hashCode 方法
  51. public V put(K key, V value) {
  52. return putVal(hash(key), key, value, false, true);
  53. }
  54. static final int hash(Object key) {
  55. int h;
  56. return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
  57. }
    p1, p2, p3 在【业务】属性上是彻底相同的三个对象,因为「对象地址」的不一样致使生成的 hashCode 不同,最终都被放到 Map 中,这就会致使业务重复对象占用空间,因此这也是内存泄漏的一种
    解决办法
    解决办法很简单,在 Person 上加一个 Lombok 的 @Data 注解自动帮你重写 hashCode 方法,或手动在 IDE 中 generate,再次运行,结果就为 1了,符合业务需求
    那重写了 hashCode 确实能够避免重复对象的加入,那这就完事大吉了吗, 再来看个例子
  58. public static void main(String[] args) throws InterruptedException {
  59. // 注意: HashSet 的底层也是 Map 结构
  60. Set<Person> set = new HashSet<Person>();
  61. Person p1 = new Person("zhangsan", 1);
  62. Person p2 = new Person("lisi", 2);
  63. Person p3 = new Person("wanger", 3);
  64. set.add(p1);
  65. set.add(p2);
  66. set.add(p3);
  67. System.out.println(set.size()); // 运行结果:3
  68. p3.setName("wangermao");
  69. set.remove(p3);
  70. System.out.println(set.size()); // 运行结果:3
  71. set.add(p3);
  72. System.out.println(set.size()); // 运行结果:4
  73. }
    从运行结果中来看,很显然 set.remove(p3) 没有删除成功,由于p3.setName("wangermao") 后,从新计算 p3 的 hashCode 会发生变化,因此 remove 的时候会找不到相应的 Node,这就又给了增长相同对象的“机会”,致使业务中无用的对象被引用着,因此能够说这也是内存泄漏的一种。运行结果来看:
    ![](https://s4.51cto.com/images/blog/202008/04/64bf8fd585b7d1daec77201a5443c118.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)
    因此诸如此类操做,最好是先 remove,而后更改属性,最后再从新 add 进去看到这,你应该发现了,要解决 hashCode 相关的问题,你要充分了解集合的特性,更要留意类是否重写了该方法以及它们的实现方式,避免出现内存泄漏状况
    ThreadLocal
    群消息中的最后,小姐姐 留下【ThreadLocal】几个字,深藏功与名的离开了,一看就是高手
    ThreadLocal 是面试多线程的高频考点,它的好处是能够快速方便的作到线程隔离,但你们也都知道他是一把双刃剑,由于使用很差就有可能致使内存泄漏了
    实际工做中咱们都是使用线程池来管理线程 「具体请参考 我会手动建立线程,为何要使用线程池」,这种方式可让线程获得反复利用(故意不让 GC 回收),
    如今,若是任何类建立了一个ThreadLocal变量,但没有显式地删除它,那么即便在web应用程序中止以后,该对象的副本仍将保留在工做线程中,从而阻止了该对象被垃圾收集,因此乱用也会致使内存泄漏
    解决办法
    解决办法依旧很简单,依旧是遵循标准
  74. 调用 ThreadLocal 的 remove() 方法,移除当前线程变量值
  75. 也能够将它看做一种 resource,使用 try/finally 范式,万一在运行过程当中出现异常,还能够在 finally 中 remove 掉
  76. try {
  77. threadLocal.set(System.nanoTime());
  78. // business code
  79. }
  80. finally {
  81. threadLocal.remove();
  82. }
    我以为小姐姐必定是高手
    总的来讲,引发内存泄漏的缘由很是多,好比还有引用外部类的内部类等问题,这里再也不展开说明,只是说明了几种很是常见的可能引起内存泄漏问题的几种场景
    内存泄漏问题不易察觉,因此有时须要借助工具来帮忙
    JVisualVM
    JVisualvm 【可视化JVM】,可分析JDK1.6及其以上版本的JVM运行时的JVM参数、系统参数、堆栈、CPU使用等信息。可分析本地应用及远程应用,在JDK1.6以上版本中自带,工具的使用暂不展开说明, 想快速使用此工具,只须要在 IDE 中安装个 VisualVM Launcher 插件
    ![](https://s4.51cto.com/images/blog/202008/04/30a8a7261a792ea8bca79d414edf3cba.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)
    而后在进行基本的配置
    ![](https://s4.51cto.com/images/blog/202008/04/08db45b3b4e2a4630044c08017ed5ebf.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)
    而后在IDE的右上角或当前类鼠标右键就能够点击运行查看了
    ![](https://s4.51cto.com/images/blog/202008/04/6a3d106bc303207027a47fd847b4c190.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)
    运行起 VisualVM 就是这样子了
    ![](https://s4.51cto.com/images/blog/202008/04/96a81429250c09ad0fa69e3c912de064.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)
    不要走,还没结束,在总结这篇文章的时候,我还发现了「新大陆」
    HashCode 真是根据对象内存地址生成的?
    脑海中的印象不知道为什么,很根深蒂固的接受了Object hashCode 是根据对象内存地址生成的,此次恰好想探求一下 hashCode 的本质,还着实打破了个人固有印象 (以 JDK1.8 为例)
    OpenJDK 定义 hashCode 的方法在下面两个文件中
    •   src/share/vm/prims/jvm.h
    •   src/share/vm/prims/jvm.cpp
    逐步看下去,最终会来到 get_next_hash 这个方法中,方便你们查看我先把方法截图至此:
    ![](https://s4.51cto.com/images/blog/202008/04/2f485ddc1425cb56b1992e58dac83bd0.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)
    总的来讲有 6 种生成 hashCode 的方式:
    •   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(else): Thread state combined with xor-shift[1]
    那在 JDK1.8 种用的哪种呢?
    ![](https://rgyb.sunluomeng.top/Screen Shot 2020-08-01 at 1.35.29 PM.png)
    能够看到在 JDK1.8 中生成 hashCode 的方式是 5, 也就是走程序的 else 路径,即便用 Xorshift,并非以前认为的对象内存地址「1」,觉得老版本是采用对象内存地址的方式,因此继续查看其余版本
    ![](https://s4.51cto.com/images/blog/202008/04/c39f3a5e0b02131601acad36c2222c49.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)
    从图中能够看出,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] 里面的图片加以文字说明整个冲突过程
    ![](https://s4.51cto.com/images/blog/202008/04/50ff8d9b46d8f7b08d87faeb41ed12ed.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)
    因此,调用 Object 的 hashCode() 方法或者 System.identityHashCode() 方法会让对象不能使用偏向锁。到这里你也就应该知道了,若是你还想使用偏向锁,那最好重写 hashCode() 方法,避免使偏向锁失效
    总结
    为了解决群的这个问题,发现新大陆的同时也差点让我掉入【追问无底洞】,不过经过本文你应该了解内存溢出和内存泄漏的差异,以及他们的解决方案,另外 hashCode[5] 生成方式还着实让人有些惊讶,若是你知道「hashCode的生成是根据对象内存地址生成的来源,还请留言赐教」。除此以外,小小的 hashCode 还有可能让偏向锁失效,全部的这些细节问题都有多是致使程序崩溃的坑,因此勿以「恶」小而为之,毋以「善」小而不为,良好的编程习惯能避免不少问题
    固然想要更好的理解内存泄漏,固然是要更好的理解 GC 机制,而想要更好的理解 GC,固然是更好的理解 JVM,我们后续慢慢分析吧
    灵魂追问
    为了清除 ThreadLocal 线程变量值,不用 ThreadLocal.remove() 方法,而是用 ThreadLocal.set(null) 会达到一样的效果吗?
    你曾经遇到哪些不易察觉的内存泄漏问题呢?
    参考
    [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]OpenJDK Wiki: https://wiki.openjdk.java.net/display/HotSpot/Synchronization
    [5]默认hashCode生成方式: https://srvaroa.github.io/jvm/java/openjdk/biased-locking/2017/01/30/hashCode.html
    本文转载自微信公众号「 日拱一兵」,能够经过如下二维码关注。转载本文请联系 日拱一兵公众号。
    ![](https://s4.51cto.com/images/blog/202008/04/947cb00cc60253709a64cffffcb86723.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)
  83. Java中hashCode和equals方法的正确使用
  84. 关于equals和hashCode,看这一篇真的够了!【责任编辑:武晓燕 TEL:(010)68476606】
相关文章
相关标签/搜索