hashCode竟然不是根据对象内存地址生成的?还对内存泄漏与偏向锁有影响?(上)

简介: hashCode竟然不是根据对象内存地址生成的?还对内存泄漏与偏向锁有影响?(上)

起因


起因是群里的一位童鞋突然问了这么问题:


如果重写 equals 不重写 hashcode 会有什么影响?


这个问题从上午10:45 开始陆续讨论,到下午15:39 接近尾声 (忽略这形同虚设的马赛克)


微信图片_20220511130240.jpg

这是一个好问题,更是一个高频基础面试题,我还曾经专门写过一篇文章 Java equals 和 hashCode 的这几个问题可以说明白吗, 主要说明了以下内容

随着讨论的进行,问题慢慢集中在内存溢出和内存泄漏的问题上


内存溢出 VS 内存泄漏



这两个词在中文解释上有些相似,至少给我的第一感觉,他们的差别是这样的(有人和我一样吗?)


微信图片_20220511130359.png


内存溢出:Out of Memory (OOM) ,这个大家都很熟悉了,理解起来也很简单,就是内存不够用了(啤酒【对象】太多,杯子【内存】装不下了)


那啥是内存泄漏呢?


内存泄漏:Memory Leak


特意查了一下 Leak 的字典含义,解释1的直白翻译是【通常是由于错误失误,从一个开口 进入或逃脱】


微信图片_20220511130432.png


所以程序中的内存泄漏我的理解更多是:由于程序的编写错误暴漏出一些 开口,导致一些对象进入这写开口,最终导致相关问题,进一步说白了,程序有漏洞,不当的调用就会出问题


所以接下来我们主要来看看 Java 内存泄漏,以及问题的起因 hashCode 和内存泄漏到底有哪些关系


内存泄漏


咱也是一个有身份证的人,不能总讲大白话,相对官方的内存泄漏解释是这样滴:


内存泄漏说明的是这样一种情况:堆中存在一些不再使用的对象,但垃圾收集器无法将它们从内存中删除(垃圾收集器定期删除未引用的对象,但从不收集仍在引用的对象),因此对它们进行了不必要的维护


这句话略显抽象,一张图你就能明白


微信图片_20220511130508.png


如果有用的、但垃圾收集器又不能删除的对象增多,就像下图这样,那么就会逐渐导致内存溢出(OOM)了


微信图片_20220511130533.png


所以也可以总结为,OOM 的原因之一可能是内存泄漏导致的


内存泄漏会带来哪些问题


内存泄漏,会导致真正可用内存变少,在没达到 OOM 的这个过程中,就会出现奇奇怪怪的问题


  1. 当应用程序长时间连续运行时,性能会严重下降,毕竟可用内存变小


  1. 自发的和奇怪的应用程序崩溃


  1. 应用程序偶尔会耗尽连接对象(这个经常听说吧)


  1. 最终的结果是 OOM


所以也可以反过来推理,如果发生上述问题,有可能程序的某些地方发生了内存泄漏


那常见的哪些情形可能会引起内存泄漏呢?又有哪些解决办法呢?


会引起内存泄漏的常见情形与相应解决办法


静态成员变量的乱用


直接来看一个例子


@Slf4j
public class StaticTest {
    public static List<Double> list = new ArrayList<>();

    public void populateList() {
        for (int i = 0; i < 10000000; i++) {
            list.add(Math.random());
        }
    }

    public static void main(String[] args) {
        new StaticTest().populateList();
    }
}


populateList() 是一个 public 方法,可能被各种调用,导致 list 无限增大


解决办法


解决办法很简单,针对这种情形(也就是通常所说的长周期对象引用短周期对象),就是将 list 放到方法内部,方法栈帧执行完自动就会被回收了


public void populateList() {
   List<Double> list = new ArrayList<>();
   for (int i = 0; i < 10000000; i++) {
      list.add(Math.random());
   }
}


有童鞋可能有疑问:


看 Spring 源码时有好多是 static 修饰的成员变量,难道它们也会导致内存泄漏?


不是的,如果你仔细看逻辑,它们都是是在容器初始化的过程中一次性加载的,所以不会像 populateList 随着调用次数的增加,无限撑大 List


未关闭的流


在学习流的时候老师就在耳边反复说:


一定要 关闭流... 闭流... ... ... ...


因为每当我们建立一个新的连接或打开一个流时(比如数据库连接、输入流和会话对象),JVM都会为这些资源分配内存,如果不关闭,这就是占用空间"有用"的对象, GC 就不会回收他们,当请求很大,来个请求就新建一个流,最终都还没关闭,结果可想而知


解决办法


流的解决办法很简单,其实主要遵循相应范式就可以避免此类问题


  1. 通过 try/catch/finally范式在 finally 关掉流


  1. 如果你用的 Java 7+ 的版本,也可以用 try-with-resources, 这样代码在编译后会自动帮你关闭流


  1. 也可以使用 Lombok 的 @Cleanup 注解, 就像下面这样


@Cleanup InputStream jobJarInputStream = new URL(jobJarUrl).openStream();
@Cleanup OutputStream jobJarOutputStream = new FileOutputStream(jobJarFile);
IOUtils.copy(jobJarInputStream, jobJarOutputStream);


不正确的 equals 和 hashCode 实现


又回到了这两个函数上,有很大一部分程序员不会主动重写 equals 和 hashCode,尤其是用 Lombok @Data 注解(该注解默认会帮助重写这两个函数)后,更会忽视这两个方法实现,一不小心的使就可能引起内存泄漏


来看个非常简单的例子:


public class MemLeakTest {

   public static void main(String[] args) throws InterruptedException {
      Map<Person, String> map = new HashMap<>();
      Person p1 = new Person("zhangsan", 1);
      Person p2 = new Person("zhangsan", 1);
      Person p3 = new Person("zhangsan", 1);

      map.put(p1, "zhangsan");
      map.put(p2, "zhangsan");
      map.put(p3, "zhangsan");

      System.out.println(map.entrySet().size()); // 运行结果:3
   }
}  

@Getter
@Setter
class Person {
    private String name;
    private Integer id;

    public Person(String name, Integer id){
        this.name = name;
        this.id = id;
    }
}


Person 类没有重写 hashCode 方法,那 Map 的 put 方法就会调用 Object 默认的 hashCode 方法


public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

static final int hash(Object key) {
  int h;
  return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}


p1, p2, p3 在【业务】属性上是完全相同的三个对象,由于「对象地址」的不同导致生成的 hashCode 不一样,最终都被放到 Map 中,这就会导致业务重复对象占用空间,所以这也是内存泄漏的一种


解决办法


解决办法很简单,在 Person 上加一个 Lombok 的 @Data 注解自动帮你重写 hashCode 方法,或手动在 IDE 中 generate,再次运行,结果就为 1了,符合业务需求


那重写了 hashCode 确实可以避免重复对象的加入,那这就完事大吉了吗, 再来看个例子


public static void main(String[] args) throws InterruptedException {
  // 注意: HashSet 的底层也是 Map 结构 
  Set<Person> set = new HashSet<Person>();

   Person p1 = new Person("zhangsan", 1);
   Person p2 = new Person("lisi", 2);
   Person p3 = new Person("wanger", 3);

   set.add(p1);
   set.add(p2);
   set.add(p3);
   
   System.out.println(set.size()); // 运行结果:3
   p3.setName("wangermao");
   set.remove(p3);
   System.out.println(set.size()); // 运行结果:3
   set.add(p3);
   System.out.println(set.size()); // 运行结果:4
}


从运行结果中来看,很显然 set.remove(p3) 没有删除成功,因为 p3.setName("wangermao") 后,重新计算 p3 的 hashCode 会发生变化,所以 remove 的时候会找不到相应的 Node,这就又给了增加相同对象的“机会”,导致业务中无用的对象被引用着,所以可以说这也是内存泄漏的一种。运行结果来看:


微信图片_20220511130934.png

所以诸如此类操作,最好是先 remove,然后更改属性,最后再重新 add 进去


看到这,你应该发现了,要解决 hashCode 相关的问题,你要充分了解集合的特性,更要留意类是否重写了该方法以及它们的实现方式,避免出现内存泄漏情况


ThreadLocal


群消息中的最后,小姐姐 留下【ThreadLocal】几个字,深藏功与名的离开了,一看就是高手


ThreadLocal 是面试多线程的高频考点,它的好处是可以快速方便的做到线程隔离,但大家也都知道他是一把双刃剑,因为使用不好就有可能导致内存泄漏了


实际工作中我们都是使用线程池来管理线程 「具体请参考 我会手动创建线程,为什么要使用线程池」,这种方式可以让线程得到反复利用(故意不让 GC 回收),

现在,如果任何类创建了一个ThreadLocal变量,但没有显式地删除它,那么即使在web应用程序停止之后,该对象的副本仍将保留在工作线程中,从而阻止了该对象被垃圾收集,所以乱用也会导致内存泄漏


解决办法


解决办法依旧很简单,依旧是遵循标准


  1. 调用 ThreadLocal 的 remove() 方法,移除当前线程变量值


  1. 也可以将它看作一种 resource,使用 try/finally 范式,万一在运行过程中出现异常,还可以在 finally 中 remove 掉


try {
    threadLocal.set(System.nanoTime());
    // business code
}
finally {
    threadLocal.remove();
}


我觉得小姐姐一定是高手


总的来说,引起内存泄漏的原因非常多,比如还有引用外部类的内部类等问题,这里不再展开说明,只是说明了几种非常常见的可能引发内存泄漏问题的几种场景


内存泄漏问题不易察觉,所以有时需要借助工具来帮忙




相关文章
|
1月前
|
存储 Java 程序员
Java中对象几种类型的内存分配(JVM对象储存机制)
Java中对象几种类型的内存分配(JVM对象储存机制)
60 5
Java中对象几种类型的内存分配(JVM对象储存机制)
|
12天前
|
存储 程序员 Python
Python类的定义_类和对象的关系_对象的内存模型
通过类的定义来创建对象,我们可以应用面向对象编程(OOP)的原则,例如封装、继承和多态,这些原则帮助程序员构建可复用的代码和模块化的系统。Python语言支持这样的OOP特性,使其成为强大而灵活的编程语言,适用于各种软件开发项目。
14 1
|
19天前
|
Linux 测试技术 C++
内存管理优化:内存泄漏检测与预防。
内存管理优化:内存泄漏检测与预防。
30 2
|
2月前
|
存储 缓存 算法
(五)JVM成神路之对象内存布局、分配过程、从生至死历程、强弱软虚引用全面剖析
在上篇文章中曾详细谈到了JVM的内存区域,其中也曾提及了:Java程序运行过程中,绝大部分创建的对象都会被分配在堆空间内。而本篇文章则会站在对象实例的角度,阐述一个Java对象从生到死的历程、Java对象在内存中的布局以及对象引用类型。
|
2月前
|
Arthas 存储 监控
JVM内存问题之JNI内存泄漏没有关联的异常类型吗
JVM内存问题之JNI内存泄漏没有关联的异常类型吗
|
2月前
|
NoSQL Redis C++
c++开发redis module问题之在复杂的Redis模块中,特别是使用第三方库或C++开发时,接管内存统计有哪些困难
c++开发redis module问题之在复杂的Redis模块中,特别是使用第三方库或C++开发时,接管内存统计有哪些困难
|
27天前
|
搜索推荐 Java API
Electron V8排查问题之分析 node-memwatch 提供的堆内存差异信息来定位内存泄漏对象如何解决
Electron V8排查问题之分析 node-memwatch 提供的堆内存差异信息来定位内存泄漏对象如何解决
28 0
|
23天前
|
存储 编译器 C语言
【C语言篇】数据在内存中的存储(超详细)
浮点数就采⽤下⾯的规则表⽰,即指数E的真实值加上127(或1023),再将有效数字M去掉整数部分的1。
|
2月前
|
存储 分布式计算 Hadoop
HadoopCPU、内存、存储限制
【7月更文挑战第13天】
173 14
|
13天前
|
存储 监控 Docker
如何限制docker使用的cpu,内存,存储
如何限制docker使用的cpu,内存,存储

热门文章

最新文章