什么是内存泄露

什么是内存泄露,通俗的来说就是堆中的一些对象已经不会再被使用了,但垃圾收集器却无法将它们从内存中清除。

内存泄漏很严重的问题,因为它会阻塞内存资源并随着时间的推移降低系统性能。如果不进行有效的处理,最终的结果将会使应用程序耗尽内存资源,无法正常服务,导致程序崩溃,抛出java.lang.OutOfMemoryError异常。

堆内存中通常有两种类型的对象:被引用的对象和未被引用的对象。被引用的对象是应用程序中仍然具有活跃的引用,而未被引用的对象则没有任何活跃的引用。

垃圾收集器会回收那些未被引用的对象,但不会回收那些还在被引用的对象。这也是内存泄露发生的源头。
内存泄露往往有以下表象:

当应用程序长时间连续运行时,性能严重下降;
抛出OutOfMemoryError异常;
程序莫名其妙的自动崩溃;
应用程序耗尽链接对象;
当然,如果打印GC日志,有些场景下还会看到频繁执行full GC等状况。下面就具体分析一下这些场景和处理方案。

Java中内存泄露分类

在任何一个应用程序中,发生内存泄露往往由很多原因构成。下面我们就聊聊最常见的一些内存泄露场景。

静态属性导致内存泄露

会导致内存泄露的一种情况就是大量使用static静态变量。在Java中,静态属性的生命周期通常伴随着应用整个生命周期(除非ClassLoader符合垃圾回收的条件)。

下面来看一个具体的会导致内存泄露的实例:

public class StaticTest { public static List<Double> list = new ArrayList<>(); public void populateList() { for (int i = 0; i < 10000000; i++) { list.add(Math.random()); } Log.info("Debug Point 2"); } public static void main(String[] args) { Log.info("Debug Point 1"); new StaticTest().populateList(); Log.info("Debug Point 3"); }
}

如果监控内存堆内存的变化,会发现在打印Point1和Point2之间,堆内存会有一个明显的增长趋势图。

但当执行完populateList方法之后,对堆内存并没有被垃圾回收器进行回收。
因此,我们要十分留意static的变量,如果集合或大量的对象定义为static的,它们会停留在整个应用程序的生命周期当中。而它们所占用的内存空间,本可以用于其他地方。

那么如何优化呢?第一,进来减少静态变量;第二,如果使用单例,尽量采用懒加载。

未关闭的资源
无论什么时候当我们创建一个连接或打开一个流,JVM都会分配内存给这些资源。比如,数据库链接、输入流和session对象。

忘记关闭这些资源,会阻塞内存,从而导致GC无法进行清理。特别是当程序发生异常时,没有在finally中进行资源关闭的情况。

这些未正常关闭的连接,如果不进行处理,轻则影响程序性能,重则导致OutOfMemoryError异常发生。

如果进行处理呢?第一,始终记得在finally中进行资源的关闭;第二,关闭连接的自身代码不能发生异常;第三,Java7以上版本可使用try-with-resources代码方式进行资源关闭。

不当的equals方法和hashCode方法实现

当我们定义个新的类时,往往需要重写equals方法和hashCode方法。在HashSet和HashMap中的很多操作都用到了这两个方法。如果重写不得当,会造成内存泄露的问题。

下面来看一个具体的实例:

ublic class Person { public String name; public Person(String name) { this.name = name; }
}

现在将重复的Person对象插入到Map当中。我们知道Map的key是不能重复的。

@Test
public void givenMap_whenEqualsAndHashCodeNotOverridden_thenMemoryLeak() { Map<Person, Integer> map = new HashMap<>(); for(int i=0; i<100; i++) { map.put(new Person("jon"), 1); } Assert.assertFalse(map.size() == 1);
}

上述代码中将Person对象作为key,存入Map当中。理论上当重复的key存入Map时,会进行对象的覆盖,不会导致内存的增长。

但由于上述代码的Person类并没有重写equals方法,因此在执行put操作时,Map会认为每次创建的对象都是新的对象,从而导致内存不断的增长。
当重写equals方法和hashCode方法之后,Map当中便只会存储一个对象了。方法的实现如下:

public class Person { public String name; public Person(String name) { this.name = name; } @Override public boolean equals(Object o) { if (o == this) return true; if (!(o instanceof Person)) { return false; } Person person = (Person) o; return person.name.equals(name); } @Override public int hashCode() { int result = 17; result = 31 * result + name.hashCode(); return result; }
}

经过上述修改之后,Assert中判断Map的size便会返回true。

@Test
public void givenMap_whenEqualsAndHashCodeNotOverridden_thenMemoryLeak() { Map<Person, Integer> map = new HashMap<>(); for(int i=0; i<2; i++) { map.put(new Person("jon"), 1); } Assert.assertTrue(map.size() == 1);
}

另外的例子就是当使用ORM框架,如Hibernate时,会使用equals方法和hashCode方法进行对象的的分析和缓存操作。

如果不重写这些方法,则发生内存泄漏的可能性非常高,因为Hibernate将无法比较对象(每次都是新对象),然后不停的更新缓存。

如何进行处理?第一,如果创建一个实体类,总是重写equals方法和hashCode方法;第二,不仅要覆盖默认的方法实现,而且还要考虑最优的实现方式;
外部类引用内部类
这种情况发生在非静态内部类(匿名类)中,在类初始化时,内部类总是需要外部类的一个实例。

每个非静态内部类默认都持有外部类的隐式引用。如果在应用程序中使用该内部类的对象,即使外部类使用完毕,也不会对其进行垃圾回收。

假设一个类,其中包含大量笨重对象的引用,并且具有一个非静态内部类。
此种情况,之所以发生内存泄露,是因为内部类对象隐含的持有外部类的引用,从而导致外部类成为垃圾对象时却无法被正常回收。使用匿名类的时候也会发生类似的情况。

如何避免此种情况?如果内部类不需要访问外部类的成员信息,可以考虑将其转换为静态内部类。
finalize()方法
使用finalize()方法会存在潜在的内存泄露问题,每当一个类的finalize()方法被重写时,该类的对象就不会被GC立即回收。GC会将它们放入队列进行最终确定,在以后的某个时间点进行回收。

如果finalize()方法重写的不合理或finalizer队列无法跟上Java垃圾回收器的速度,那么迟早,应用程序会出现OutOfMemoryError异常。

假设某个类重写了finalize()方法,并且重写的方法在执行时需要一些时间。

String的intern方法
字符串常量池在Java7中从PermGen移动到了堆空间。在Java6及以前版本,我们使用字符串时要多加小心。

如果读取了一个大字符串对象,并且调用其intern方法,intern()会将String放在JVM的内存池中(PermGen),而JVM的内存池是不会被GC的。同样会造成程序性能降低和内存溢出问题。

使用ThreadLocal
ThreadLocal提供了线程本地变量,它可以保证访问到的变量属于当前线程,每个线程都保存有一个变量副本,每个线程的变量都不同。ThreadLocal相当于提供了一种线程隔离,将变量与线程相绑定,从而实现线程安全的特性。
ThreadLocal的实现中,每个Thread维护一个ThreadLocalMap映射表,key是ThreadLocal实例本身,value是真正需要存储的Object。

ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统GC时,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value。

如果当前线程迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。

如何解决此问题?

第一,使用ThreadLocal提供的remove方法,可对当前线程中的value值进行移除;

第二,不要使用ThreadLocal.set(null) 的方式清除value,它实际上并没有清除值,而是查找与当前线程关联的Map并将键值对分别设置为当前线程和null。

第三,最好将ThreadLocal视为需要在finally块中关闭的资源,以确保即使在发生异常的情况下也始终关闭该资源。

try { threadLocal.set(System.nanoTime()); //... further processing
} finally { threadLocal.remove();
}

处理内存泄漏的其他策略
尽管在处理内存泄漏时没有万能的解决方案,但是有一些方法可以使内存泄漏最小化。

启用分析
我们可通过一些工具,用来对应用应用程序的内存使用情况等进行监控和诊断,从而找到最佳的利用系统资源的方案。

类似的工具有前面我们提到的VisualVM,还有Mission Control,JProfiler,YourKit,Java VisualVM和Netbeans Profiler等。

显示垃圾回收详情
通过启用垃圾收集详情日志,可以对GC的详细进行跟踪。通过以下命令进行启动:

-verbose:gc

通过添加此参数,我们可以看到GC内部发生的情况的详细信息:

使用引用对象避免内存泄漏
在Java中,我们还可以使用java.lang.ref包内置引用对象来处理内存泄漏。使用java.lang.ref包,而不是直接引用对象,我们对对象使用特殊的引用,从而确保它们可以轻松地被垃圾回收。

IDE警告
无论是Eclipse还是IDEA,如果安装对应的插件(比如阿里巴巴开发手册插件等),当写代码中出现内存泄露风险代码时,IDE会进行警告提醒,从而从源头上避免内存泄露的代码出现在生产环境。

基准测试
通过执行基准测试来衡量和分析Java代码的性能,从而选择更合理的解决方案。

Code Review
这也是最古老,最有效的方式之一,通过经验丰富的开发人员对代码的Review或多人进行Review,从而达到查漏补缺的效果,排除一些常见的内存泄露问题。

7种内存泄露场景和13种解决方案相关推荐

  1. Android Handler的内存泄露场景分析

    在前面一篇博客<Android全面解析Handler>一文中,我们认识了Handler的异步通信机制,同时也提到过Handler如果使用不慎将会导致内存泄露.今天主要来讲述一下Handle ...

  2. 几种内存泄露检测工具的比较

    概述 内存泄漏(memory leak)指由于疏忽或错误造成程序未能释放已经不再使用的内存的情况,在大型的.复杂的应用程序中,内存泄漏是常见的问题.当以前分配的一片内存不再需要使用或无法访问时,但是却 ...

  3. linux 内存泄漏 定位,一种内存泄露检查和定位的方法

    一个系统后台服务进程,可能包括多个线程,在生成环境下要求系统程序能够稳定长时间稳定运行而不宕机.其中一个基本的前提就是需要保证系统程序不存在内存泄露.那么,该如何判读系统程序是否存在内存泄露呢?如果存 ...

  4. 一种内存泄露检查和定位的方法

    一个系统后台服务进程,可能包括多个线程,在生成环境下要求系统程序能够稳定长时间稳定运行而不宕机.其中一个基本的前提就是需要保证系统程序不存在内存泄露.那么,该如何判读系统程序是否存在内存泄露呢?如果存 ...

  5. linux内存硬件检测工具,13种在 Linux 系统上检测 CPU 信息的工具

    1. /proc/cpuinfo 最简单的方法就是查看 /proc/cpuinfo ,这个虚拟文件展示的是可用CPU硬件的配置.$ more/proc/cpuinfo 通过查看这个文件,你能识别出物理 ...

  6. 14种16阶群、13种60阶群的结构与表示(附GAP软件的使用)

    2019独角兽企业重金招聘Python工程师标准>>> 共有14个不同的16阶群,其中交换群有5个,其余9个为非交换群. gap> L:=Factors(16); [ 2, 2 ...

  7. 会不会导致内存泄漏_可能会导致.NET内存泄露的8种行为

    原文连接:https://michaelscodingspot.com/ways-to-cause-memory-leaks-in-dotnet/ 作者 Michael Shpilt.授权翻译,转载请 ...

  8. 世界500强坚决不用的13种人

    世界500强坚决不用的13种人 美国著名人力资源专家詹姆斯.希尔曼多年潜心研究世界500强企业的用人之道,惊讶地发现有13种人是世界500强企业最讨厌也是坚决不用之人.于是他写成了<世界500强 ...

  9. 一个Vue页面的内存泄露分析

    什么是内存泄露?内存泄露是指new了一块内存,但无法被释放或者被垃圾回收.new了一个对象之后,它申请占用了一块堆内存,当把这个对象指针置为null时或者离开作用域导致被销毁,那么这块内存没有人引用它 ...

最新文章

  1. 网络工程计算机文科可以报考么,文科生可报考自考本科网络工程吗?
  2. Struts2之环境配置
  3. 记录一次uni-app页面跳转无效 来回跳转问题
  4. oracle12c多个pdb,ProxmoxVE 之 oracle12C 多CDB和PDB
  5. 怎么样升级成为鸿蒙系统,手机升级成为鸿蒙系统第一手体验怎么样?-电脑自学网...
  6. mc服务器tps优化,LaggRemover——降低延迟/优化TPS/内存
  7. convert mysql_MySQL的CONVERT()
  8. SQL Server Management Studio中SQL代码段
  9. Python 之 游戏飞机大战项目实现
  10. Python max函数中key的用法
  11. 盘点下玩过的解谜游戏
  12. java条形码解析_Java 生成、识别条形码
  13. MySQL 数据库备份与还原
  14. 软件促进两化深度融合 ——记2016中国软件和信息技术服务业发展高峰论坛
  15. 基于FPGA的AD9854并行接口驱动(VerilogHDL语言)
  16. c#计算标准偏差实现跟excel中一样的STDEVP()
  17. 7-3 奥运排行榜(25 分)
  18. 值班c语言程序,5.值班安排C语言程序报告.doc
  19. 腾讯云ubuntu20.04,免密登录,安装anaconda,并安装远程jupyter notebook,TensorFlow
  20. 商女不知亡国恨,隔江犹吃炒腰花

热门文章

  1. 【人工智能 Open AI】设计一个SQL Where DSL模型,使用 golang 代码来实现DSL的翻译成SQL。
  2. Flutter 不可错过的学习资源
  3. Makefile depend规则
  4. 未来简史,从智人到智神
  5. 禁止公司内网电脑安装QQ电脑管家和360安全卫士
  6. Socket心跳包编程
  7. jqgrid使用分析
  8. 4826: [Hnoi2017]影魔
  9. 春季出游,学会这些功能,让你旅途更舒心
  10. macd的python代码同花顺_同花顺定量结构MACD指标公式(图文)