容易发生内存泄漏的八个场景,你都知道吗?
内存泄漏与内存溢出
JVM在运行时会存在大量的对象,一部分对象是长久使用的,一部分对象只会短暂使用
JVM会通过可达性分析算法和一些条件判断对象是否再使用,当对象不再使用时,通过GC将这些对象进行回收,避免资源被用尽
内存泄漏:当不再需要使用的对象,因为不正确使用时,可能导致GC无法回收这些对象
当不正确的使用导致对象生命周期变成也是宽泛意义上的内存泄漏
内存溢出:当大量内存泄漏时,可能没有资源为新对象分配
举例内存泄漏
接下来将从对象生命周期变长、不关闭资源、改变对象哈希值、缓存等多个场景举例内存泄漏
对象生命周期变长引发内存泄漏
静态集合类
public class StaticClass { private static final List<Object> list = new ArrayList<>(); /** * 尽管这个局部变量Object生命周期非常短 * 但是它被生命周期非常长的静态列表引用 * 所以不会被GC回收 发生内存溢出 */ public void addObject(){ Object o = new Object(); list.add(o); } }
类卸载的条件非常苛刻,这个静态列表生命周期基本与JVM一样长
静态集合引用局部对象,使得局部对象生命周期变长,发生内存泄漏
饿汉式单例模式
public class Singleton { private static final Singleton INSTANCE = new Singleton(); private Singleton(){ if (INSTANCE!=null){ throw new RuntimeException("not create instance"); } } public static Singleton getInstance(){ return INSTANCE; } }
饿汉式的单例模式也是被静态变量引用,即时不需要使用这个单例对象,GC也不会回收
非静态内部类
非静态内部类会有一个指针指向外部类
public class InnerClassTest { class InnerClass { } public InnerClass getInnerInstance() { return this.new InnerClass(); } public static void main(String[] args) { InnerClass innerInstance = null; { InnerClassTest innerClassTest = new InnerClassTest(); innerInstance = innerClassTest.getInnerInstance(); System.out.println("===================外部实例对象内存布局=========================="); System.out.println(ClassLayout.parseInstance(innerClassTest).toPrintable()); System.out.println("===================内部实例对象内存布局==========================="); System.out.println(ClassLayout.parseInstance(innerInstance).toPrintable()); } //省略很多代码..... } }
当调用外部类实例方法通过外部实例对象返回一个内部实例对象时(调用代码中的getInnerInstance方法)
外部实例对象不需要使用了,但内部实例对象被长期使用,会导致这个外部实例对象生命周期变长
因为内部实例对象隐藏了一个指针指向(引用)创建它的外部实例对象
实例变量作用域不合理
如果只需要一个变量作为局部变量,在方法结束就不使用它了,但是把他设置为实例变量,此时如果该类的实例对象生命周期很长也会导致该变量无法回收发生内存泄漏(因为实例对象引用了它)
变量作用域设置的不合理会导致内存泄漏
隐式内存泄漏
动态数组ArrayList中remove操作会改变size的同时将删除位置置空,从而不再引用元素,避免内存泄漏
不置空要删除的元素对数组的添加删除查询等操作毫无影响(看起来是正常的),只是会带来隐式内存泄漏
不关闭资源引发内存泄漏
各种连接: 数据库连接、网络连接、IO连接在使用后忘记关闭,GC无法回收它们,会发生内存泄漏
所以使用连接时要使用 try-with-resource
自动关闭连接
改变对象哈希值引发内存泄漏
一般认为对象逻辑相等,只要对象关键域相等即可
一个对象加入到散列表是通过计算该对象的哈希值,通过哈希算法得到放入到散列表哪个索引中
如果将对象存入散列表后,修改了该对象的关键域,就会改变对象哈希值,导致后续要在散列表中删除该对象,会找错索引从而找不到该对象导致删除失败(极小概率找得到)
public class HashCodeTest { /** * 假设该对象实例变量a,d是关键域 * a,d分别相等的对象逻辑相等 */ private int a; private double d; @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; HashCodeTest that = (HashCodeTest) o; return a == that.a && Double.compare(that.d, d) == 0; } @Override public int hashCode() { return Objects.hash(a, d); } public HashCodeTest(int a, double d) { this.a = a; this.d = d; } public HashCodeTest() { } @Override public String toString() { return "HashCodeTest{" + "a=" + a + ", d=" + d + '}'; } public static void main(String[] args) { HashMap<HashCodeTest, Integer> map = new HashMap<>(); HashCodeTest h1 = new HashCodeTest(1, 1.5); map.put(h1, 100); map.put(new HashCodeTest(2, 2.5), 200); //修改关键域 导致改变哈希值 h1.a=100; System.out.println(map.remove(h1));//null Set<Map.Entry<HashCodeTest, Integer>> entrySet = map.entrySet(); for (Map.Entry<HashCodeTest, Integer> entry : entrySet) { System.out.println(entry); } //HashCodeTest{a=100, d=1.5}=100 //HashCodeTest{a=2, d=2.5}=200 } }
所以说对象当作Key存入散列表时,该对象最好是逻辑不可变对象,不能在外界改变它的关键域,从而无法改变哈希值
将关键域设置为final,只能在实例代码块中初始化或构造器中
如果关键域是引用类型,可以用final修饰后,对外不提供改变该引用关键域的方法,从而让外界无法修改引用关键域中的值 (如同String类型,所以String常常用来当作散列表的Key)
缓存引发内存泄漏
当缓存充当散列表的Key时,如果不再使用该缓存,就要手动在散列表中删除,否则会发生内存泄漏
如果使用的是WeakHashMap,它内部的Entry是弱引用,当它的Key不再使用时,下次垃圾回收就会回收掉,不会发生内存泄漏
public class CacheTest { private static Map<String, String> weakHashMap = new WeakHashMap<>(); private static Map<String, String> map = new HashMap<>(); public static void main(String[] args) { //模拟要缓存的对象 String s1 = new String("O1"); String s2 = new String("O2"); weakHashMap.put(s1,"S1"); map.put(s2,"S2"); //模拟不再使用缓存 s1=null; s2=null; //垃圾回收WeakHashMap中存的弱引用 System.gc(); try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } //遍历各个散列表 System.out.println("============HashMap==========="); traverseMaps(map); System.out.println(); System.out.println("============WeakHashMap==========="); traverseMaps(weakHashMap); } private static void traverseMaps(Map<String, String> map){ for (Map.Entry<String, String> entry : map.entrySet()) { System.out.println(entry); } } }
结果
注意: 监听器和回调 也应该像这样成为弱引用
总结
这篇文章介绍内存泄漏与内存溢出的区别,并从生命周期变长、不关闭资源、改变哈希值、缓存等多方面举例内存泄漏的场景
内存泄漏是指当对象不再使用,但是GC无法回收该对象
内存溢出是指当大量对象内存泄漏,没有资源再给新对象分配
静态集合、饿汉单例、不合理的设置变量作用域都会使对象生命周期变长,从而导致内存泄漏
非静态内部对象有隐式指向外部对象的指针、使用集合不删除元素等都会隐式导致内存泄漏
忘记关闭资源导致内存泄漏(try-with-resource自动关闭解决)
使用散列表时,充当Key 对象的哈希值被改变导致内存泄漏(key 使用逻辑不可变对象,关键域不能被修改)
缓存引发内存泄漏(使用弱引用解决)
最后(一键三连求求拉~)
本篇文章将被收入JVM专栏,觉得不错感兴趣的同学可以收藏专栏哟~
本篇文章笔记以及案例被收入 gitee-StudyJava、 github-StudyJava 感兴趣的同学可以stat下持续关注喔~
有什么问题可以在评论区交流,如果觉得菜菜写的不错,可以点赞、关注、收藏支持一下~
关注菜菜,分享更多干货,公众号:菜菜的后端私房菜
本文由博客一文多发平台 OpenWrite 发布!

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
前端monorepo大仓共享复杂业务组件最佳实践
一、背景 在 Monorepo 大仓模式中,我们把组件放在共享目录下,就能通过源码引入的方式实现组件共享。越来越多的应用愿意走进大仓,正是为了享受这种组件复用模式带来的开发便利。这种方式可以满足大部分代码复用的诉求,但对于复杂业务组件而言,无论是功能的完整性,还是质量的稳定性都有着更高的要求。源码引入的组件提供方一旦发生变更,其所有使用方都需要重新拉取 master 代码,然后构建发布才能使用新功能,这一特性对物料组件、工具组件以及那些对新功能敏感度较低的业务组件来说是可以接受的,但对于新功能敏感度高的复杂业务组件来说,功能更新的不及时会直接面临着资损风险。这类复杂组件也往往面临着频繁且快速的迭代发布,这样一来对于组件使用方而言不光需要订阅组件更新,而且需要做到及时发布升级才能规避风险,因此只用源码引入的方式来共享复杂业务组件是耗费精力且不合适的。 Webpack5 的 MF(Module Federation,模块联邦)有着动态集成多个构建的特性能够规避上述更新的问题。但同样也是把双刃剑,一旦远程组件提供方发挂了,其所有使用方也就不能正常使用,问题所造成的影响面也会被进一步放大。从分...
- 下一篇
基于 Zadig + Ingress 实现单应用灰度发布最佳实践
在当前激烈的软件开发竞争中,工程师们面临着众多挑战,其中最为关键的是如何在发布过程中确保稳定性、可靠性以及高效性。为了解决这一问题,企业通常会根据业务架构和应用场景综合选择适用的发布策略。在我们之前的文章「基于 Istio + Zadig,零负担实现云原生全链路灰度发布」中,Zadig 为微服务架构提供了通用的全链路灰度发布方案。然而,在实际场景中,仍有许多业务处于单体架构或单应用发布阶段。因此,Zadig 结合 Ingress 提供了专为单应用生产发布场景设计的安全保障方案。 本文将详细介绍如何结合 Zadig 和 Ingress 实现生产稳定发布的基本原理,并通过实际案例演示在 Zadig 中的具体操作。 基本原理 说明:生产版本和新版本(蓝环境)在 Zadig 同一个生产环境中管理。 工作原理: 1. 部署蓝环境:复制当前 workload,设置新镜像,并创建一个 blue service 指向它。 2. 切换部分生产流量到蓝环境:在原来 ingress 基础上创建一个相同 Host 的 ingress-blue ,service 指向 blue service,并且开启 n...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- SpringBoot2整合Thymeleaf,官方推荐html解决方案
- MySQL8.0.19开启GTID主从同步CentOS8
- SpringBoot2更换Tomcat为Jetty,小型站点的福音
- Red5直播服务器,属于Java语言的直播服务器
- CentOS7,8上快速安装Gitea,搭建Git服务器
- CentOS6,7,8上安装Nginx,支持https2.0的开启
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- SpringBoot2全家桶,快速入门学习开发网站教程