猪年第一篇译文,大家多多支持!

原文自工程师baeldung博客,传送门

1. 介绍

Java 的其中一个核心特点是经由内置的垃圾回收机制(GC)下的自动化内存管理。GC 默默地处理着内存分配和释放工作因此能够处理大部分内存泄漏问题。

虽然 GC 能够有效地理一大部分内存,但他不保证能处理所有内存泄漏情况。GC 十分智能,但并不完美。即使是在谨慎的程序员所开发的应用程序下内存泄漏依旧会悄悄地出现。

应用程序仍然会出现产生大量的多余的对象的情况,因此耗尽了所有关键的内存块资源,有时候还会导致应用程序崩坏。

内存泄漏是 Java 中的一个永恒的问题。在这篇文章中,我们将会讨论内存泄漏的潜在原因,怎么在运行时识别它们并且怎么在应用程序中解决它们

2. 什么是内存泄漏

内存泄漏是指这么一种情况,当存在对象在堆中不再被使用,但垃圾回收器无法从内存中移除它们并且因此变得不可被维护。

内存泄漏十分不好因为它锁住了部分内存资源并且逐渐降低系统的性能。并且如果无法处理它,应用程序最终会耗尽所有资源最终产生一个致命的错误 -- java.lang.OutOfMemoryError

这里有两种不同类型的对象存在于堆内存中,被引用的以及未被引用的。被引用的对象是指那些在应用程序中仍然被主动使用的而未被引用的对象是指那些不在被使用的。

垃圾回收器会定期清除未被引用对象,但从来都不收集那些仍然被引用的对象。这就是内存泄漏发生的其中一个原因:

内存泄漏的症状:

  • 当应用程序持续长时间运行导致服务器性能的严重下降
  • 应用程序中的堆异常 OutOfMemoryError
  • 自发以及奇怪的程序崩溃
  • 程序偶然耗尽连接对象

让我们关注下这些场景并且研究下它们为什么会发生。

3. Java 中内存泄漏的类型

在任何的程序当中,内存泄漏能由几种原因引起。在这节,我们来讨论下最常见的一种。

3.1. 静态字段导致的内存泄漏

第一种可能导致内存泄漏的情况是大量使用静态字段。

在 Java,静态字段的生命周期通常和运行的应用程序的整个生命周期相匹配(除非类加载器有资格进行垃圾回收)

让我们创建一个填充了静态 list 的简单 Java 程序:

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");}
}
复制代码

现在如果我们在程序运行过程中分析堆内存,可以看到在调试点1和2之间,正如预期所想的那样,堆内存的使用增加了。

但是当我们在调试点3跳出了 populateList() 方法,在 VisualVM 可以看到,堆内存仍然未被回收:

然而,在上述的程序当中,如果我们在第二行把关键字 static 去掉的话,内存使用将会发生一个剧烈的变化,在 VisualVM 可以看到:

调试点的第一部分和存在 static 的例子差不多一样。但这次在跳出 populateList() 之后,list 所使用的内存全部被回收了因为我们不再引用它了。

因此使用 static 变量时我们需要留意了。如果集合或者大对象被声明为 static,那么它们在应用程序的整个生命周期中都保留在内存中,因此锁住了那些原本可以用在其他重要地方的内存。

怎么预防这种情况发生呢?

  • 尽量减低 static 变量的使用
  • 使用单例模式时,使用延迟加载而非立即加载

3.2. 未关闭资源导致的内存泄漏

当我们产生新的连接或者开启流的时候,JVM 会为它们分配内存,像数据库连接、输入流或者会话对象等等。

忘记关闭流能导致内存被锁,从而它们也无法被回收。这甚至会出现在那些阻止程序执行关闭资源的语句的异常中。

不论哪种情况,资源产生的连接都会消耗掉内存,并且如果不处理它,会降低性能和导致 OutOfMemoryError

怎么预防这种情况发生呢?

  • 始终使用 finally 块来关闭资源
  • 关闭资源的代码块(包括 finally 块)自身不能带有异常
  • 当使用 Java 7或更高版本,可以使用 try-with-resources 语法

3.3. 不当的 equals()hashCode() 实现

当定义新类的时候,一种非常常见的疏忽是没有正确编写 equals()hashCode() 的重写实现方法。

HashSetHashMap 在许多操作当中使用这两个方法,如果我们没有合理地重写它们,会导致潜在的内存泄漏问题。

让我们以一个简单的 Person 类为例,并且将其作为一 HashMap 中的键:

public class Person {public String name;public Person(String name) {this.name = name;}
}
复制代码

现在我们在 Map 当中作为键插入相同的 Person 对象。

请记住 Map 并不能存在相同的键:

@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 作为键,由于 Map 不允许重复键,所以作为键插入的大量重复的 Person 应当不会增加内存的消耗。

但是由于我们没有正确地定义 equals() 方法,重复的对象会堆积起来并且增加内存消耗,这就是为什么在内存中能看到超过一个对象。VisualVM 的堆内存就像下图所示:

但是,如果我们正确地重写 equals()hashCode() 方法,那么 Map 中只会存在一个 Person 对象。

让我们看下 Person 类中正确的 equals()hashCode() 实现:

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

在这种情况下,下面的断言是正确的:

@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);
}
复制代码

在通过正确的 equals()hashCode() 方法后,相同程序的堆内存是这样的:

另外一个使用像 Hibernate 这样的 ORM 框架的例子中,它使用 equals()hashCode() 方法分析对象并将它们保存在缓存中。

如果这些方法不被重写发生内存泄漏的几率会变得非常大,因为 Hibernate 无法比较对象并且会将重复的对象填充到缓存当中。

怎么预防这种情况发生呢?

  • 根据经验,在定义新实体的时候,总是要重写 equals()hashCode() 方法
  • 仅仅重写还不够,还需要以最佳的方式来处理它们

3.4. 引用外部类的内部类

这种情况发生在非静态内部类(匿名类)当中。对于初始化,这些内部类总是需要一个封闭类的实例。

默认情况下,每个非静态内部类都有对其包含类的隐式引用。如果我们在程序当中使用这种内部类对象,即使包含类对象超出了作用域,它仍然不会被回收。

思考有一个类中包含大量大对象的引用以及一个非静态内部类。现在当我们创建一个内部类对象时,内存模型是这样的:

然而,如果我们定义这个内部类为静态,现在内存模型是这样的:

会发生这种情况的原因是内部类对象隐含着外部类对象的引用,从而它不能被垃圾回收所识别。匿名类同样如此。

怎么预防这种情况发生呢?

  • 如果内部类不需要访问包含的类的成员,考虑将它定义为静态类

3.5. finalize() 方法导致的内存泄漏

使用 finalizer 是另一个潜在内存泄漏问题的来源。每当类中的 finalize() 方法被重写,那么该类的对象不会马上被回收。相反,它们将会延后被 GC 放到队列当中序列化。

此外,如果用 finalize() 方法编写的代码不是最优的,并且 finalizer 队列跟不上 GC 的速度的话,那么,应用程序迟早会发生 OutOfMemoryError 异常。

为了演示这点,让我们假设我们已经有一个重写了 finalize() 方法的类并且这方法需要花费额外的一些时间来执行。当该类的大量对象被回收,VisualVM 是这样的:

然而,如果我们仅仅是移除 finalize() 方法,同一个程序给出以下的响应:

怎么预防这种情况发生呢?

  • 我们应该尽量避免序列化

3.6. 字符串

Java 字符串池发生了重大变化,当它在 Java7 中从 PermGen 转移到 HeapSpace 时所发生的。但是对于在版本6及以下运行的程序,我们在处理大字符串时应该更加注意。

如果我们读取一个巨大的字符串对象,并且调用 intern() 方法,它就会进入到位于 PermGen (永久内存)的字符串池中,而只要我们的应用程序运行,它就会一直呆在那里。

在 Java6 中本例子的 PermGen 在VisualVM 是这样的:

与此想法,在一个方法中,如果我们只是从文件中读取字符串,而不进行 intern,PermGen 是这样的:

怎么预防这种情况发生呢?

  • 预防的最简单的方法就是升级到最新的 Java 版本,因为字符串池是从 Java7 开始移动到 HeapSpace 的
  • 如果需要处理大字符串,增加 PermGen 空间的大小,以避免任何潜在的outofmemoryerror 异常
-XX:MaxPermSize=512m
复制代码

3.7. 使用 ThreadLocals

ThreadLocals 是一种结构,它使我们能够将状态隔离到特定的线程中,从而实现线程安全。

当使用这种结构,每个线程都会持有其 ThreadLocal 变量副本的隐式引用,并且维护它们自身的副本,而不是在活动状态的线程当中跨线程共享资源

尽管它有其优点,但是 ThreadLocal 的使用是受争议的。因为如果使用不恰当,它会导致内存泄漏。Joshua Bloch 曾经评论过 ThreadLocals*:

草率地使用线程池加上草率地使用线程局部变量,可能会导致意外的对象保留情况,这点在很多地方都被引起注意了,但把责任推给 ThreadLocal* 是没有依据的。

ThreadLocals 导致的内存泄漏

一旦持有的线程不再活动,ThreadLocals 应当被回收。当问题就出在当 ThreadLocals 被使用在现在流行的应用服务器上。

现在的应用服务器是使用线程池去处理请求而并非创建新的线程来处理(例如 Apache Tomcat 的 Executor)此外,它们还使用单独的类加载器。

由于应用服务器二弟线程池使用线程重用的概念来工作,因此它们从来都不会被回收 — 相反,它们被重用来服务于另一个新的请求。

现在,如果任何类创建了一个 ThreadLocals 而并没有显式地删除掉它,那么即使在web应用程序停止后,对象的副本仍然保留在工作线程当中,从而使得对象没有被回收。

怎么预防这种情况发生呢?

  • ThreadLocals 不再使用时,清理它们是一个很好的实践 — threadlocals 提供 remove() 方法,这个方法将删除该变量中的当前线程。
  • 千万不要使用 ThreadLocal.set(null) 来清除 — 它实际上并没有做清除工作,而是会查找与当前线程关联的 Map 映射,并将键-值对分别设置为当前线程和null
  • 最好将 ThreadLocal 视为一个需要在 finally 块中关闭的资源,以确保它始终处于关闭状态,即使在异常情况下也需要如此:
try {threadLocal.set(System.nanoTime());//... further processing
}
finally {threadLocal.remove();
}
复制代码

4. 处理内存泄漏的其他策略

虽然在处理内存泄漏时并没有一种万能的解决方法,但是还是有些可以将风险降到最低的做法。

4.1. 使用剖析工具

Java 分析工具是通过应用程序监视和诊断内存泄漏的工具。它分析应用程序内部发生的事情 — 例如内存是怎么分配的。

通过分析器,我们能够比较不同的方法和找到使用资源的最优方法。

在第三节中我们使用 VisualVM。除此之外还有 Mission Control,JProfiler,YourKit,Java VisualVM,Netbeans Profiler 等等。

4.2. Verbose Garbage Collection

通过使用 Verbose Garbage Collection,我们可以跟踪 GC 的详细轨迹,为了开启它,我们需要在 JVM 配置中添加如下内容:

-verbose:gc
复制代码

通过添加这个参数,我们可以看到 GC 内部的细节:

4.3. 使用引用对象避免内存泄漏

我们也可以使用 java.lang.ref 包中内置的引用对象来处理内存泄漏。使用 java.lang.ref 包,而并不会直接引用对象,使用对对象的特殊引用使得它们容易被回收。设计出的引用队列也让我们了解到垃圾回收的执行操作。

4.4. Eclipse 的内存泄漏警告

对于 JDK1.5 或以上的项目,当遇到明显的内存泄漏情况时,Eclipse 都会显示警告和错误。因此使用 Eclipse 开发时,我们可以通过查看 Problems 标签栏,来提防内存泄漏的警告了(如果有的话):

4.5. Benchmarking

我们通过 Benchmarking 来度量和分析 Java 代码的性能。通过这种方法,我们可以比较对同一个任务的不同种做法之间的性能。这可以帮助我们选择更好的方法去运行,也可以节约内存消耗。

4.6. 代码 review

最后,还是以我们最经典,老式的代码遍历方法来处理啦。

在某些情况下,即使是一个看起来微不足道的方法,也可以帮助我们消除一些常见的内存泄漏问题。

5. 总结

用外行的话来说,我们可以把内存泄漏当作一种疾病,它通过阻塞重要的内存资源来降低应用程序的性能。和其他所有疾病一样,如果没有痊愈,随着时间推移,它可能导致致命的程序崩溃。

内存泄漏难以解决,找到它们需要对 Java 本身有很高的掌握以及知识。在处理内存泄漏时,没有适用于所有情况的解决方法,因为泄漏本身可以通过各种各样的事件发生。

然而,如果我们采用最佳的代码方式实践并且定期做代码的回顾和严格的代码分析,那么我们可以将应用程序中的内存泄漏风险降至最低。

像往常那样,用于生成本文章中 VisualVM 的响应的代码段在我们的 Github 上可以获取到。

6. 译者总结

这篇文章很详细的讲述了各种发生内存泄漏的情形以及一些简单的解决方法,其中详细的解决方法在作者的其他文章中有提及,本人因为翻译的原因并没有放到上面,有需要的读者可以自行到文章本体去阅读。

而且本人因为时(TOU)间(LAN)原因,并没有把图片中的描述翻译过来,望各位读者见谅。

最后祝大家新春快乐。


小喇叭

广州芦苇科技Java开发团队

芦苇科技-广州专业互联网软件服务公司

抓住每一处细节 ,创造每一个美好

关注我们的公众号,了解更多

想和我们一起奋斗吗?lagou搜索“ 芦苇科技 ”或者投放简历到 server@talkmoney.cn 加入我们吧

关注我们,你的评论和点赞对我们最大的支持

翻译 | 理解Java中的内存泄漏相关推荐

  1. 深入理解Java中的内存泄漏

    理解Java中的内存泄漏,我们首先要清楚Java中的内存区域分配问题和内存回收的问题本文将分为三大部分介绍这些内容. Java中的内存分配 Java中的内存区域主要分为线程共享的和线程私有的两大区域: ...

  2. 介绍Java中的内存泄漏

    转载自  介绍Java中的内存泄漏 Java语言的一个关键的优势就是它的内存管理机制.你只管创建对象,Java的垃圾回收器帮你分配以及回收内存.然而,实际的情况并没有那么简单,因为内存泄漏在Java应 ...

  3. Java中关于内存泄漏分析和解决方案,都在这里了!

    作者:李序锴 www.jianshu.com/p/54b5da7c6816 最近正在熟悉Java内存泄漏的相关知识,上网查阅了一些资料,在此做个整理算是对收获的一些总结,希望能对各位有所帮助,有问题可 ...

  4. Java中关于内存泄漏出现的原因以及如何避免内存泄漏

    转账自:http://blog.csdn.net/wtt945482445/article/details/52483944 Java 内存分配策略 Java 程序运行时的内存分配策略有三种,分别是静 ...

  5. (转载)Java中关于内存泄漏出现的原因以及如何避免内存泄漏

    原文链接 Android 内存泄漏总结 内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题.内存泄漏大家都不陌生了,简单粗俗的讲,就是该被释放的对象没有释放,一直被某个或某些实 ...

  6. 了解Java中的内存泄漏

    来源:SpringForAll社区 1. 简介 Java的核心优势之一是在内置垃圾收集器(简称GC)的帮助下实现自动内存管理.GC隐含地负责分配和释放内存,因此能够处理大多数内存泄漏问题. 虽然GC有 ...

  7. java中的内存泄漏

    ● 请问java中内存泄漏是什么意思?什么场景下会出现内存泄漏的情况? 考察点:内存泄漏 参考回答: Java中的内存泄露,广义并通俗的说,就是:不再会被使用的对象的内存不能被回收,就是内存泄露.如果 ...

  8. 如何在Java中创建内存泄漏?

    我刚刚接受采访,并被要求使用Java造成内存泄漏. 不用说,我对如何开始创建它一无所知. 一个例子是什么? 解决方案: 这是在纯Java中创建真正的内存泄漏(运行代码无法访问但仍存储在内存中的对象)的 ...

  9. Java 中发生内存泄漏 5 个场景以及解决方法

    前言 说到垃圾回收(Garbage Collection,GC),很多人就会自然而然地把它和 Java 联系起来.在 Java 中,程序员不需要去关心内存动态分配和垃圾回收的问题,这一切都交给了JVM ...

最新文章

  1. Java Web中的Filter和Interceptor的区别
  2. Linux内核探讨-- 第七章
  3. [译]解密 Uber 数据科学团队路径选择算法的优化之路
  4. linux kernel中cache代码解读
  5. bzoj1715[Usaco2006 Dec]Wormholes 虫洞
  6. java实现账号单一ip登录,使用Java实现简单后台访问并获取IP示例
  7. gin 生成api文档_gin-swagger 生成RESTful风格OpenAPI文档
  8. FFmpeg 是如何实现多态的?
  9. MTK 驱动(60)---Audio驱动开发之音频链路
  10. 华为麦芒8首发开箱图赏:年轻潮流美学就看它
  11. (转)Maven仓库——私服介绍
  12. 屏幕演示讲解必备工具:虚拟教棒、屏幕画笔、ZoomIt放大屏幕、鼠标探照灯(图)
  13. neutron使用数据库
  14. Evernote 印象笔记离线使用方法
  15. 2020-11-7YTUOJ3545: 输出最长单词
  16. 根据文件MD5删除重复文件
  17. python学习笔记 os.scandir遍历目录
  18. 9.3 开发经验和屁股的关系——《逆袭大学》连载
  19. 语音群呼 售前营销售后服务一体化
  20. 那些人尽可夫的男人啊——黄金圣斗士对同人女的真情告白2

热门文章

  1. 苹果8p电池多少毫安的_苹果手机电池很不耐用,一般多久需要换一次?换一次的费用多少?...
  2. vuex 数据 mapState
  3. BugKuCTF-杂项-隐写 加对于图片宽度的理解
  4. PTA-穷举法搬运砖块问题
  5. java 网络 ppt,13Java网络编程.ppt
  6. Nginx——Nginx优化方案设计
  7. fastadmin 实现自动生成二维码
  8. 软件测试笔试题含答案
  9. Excel表格如何设置密码 Excel2003/2007/2010设置密码教程
  10. 读《墨菲定律》笔记—洛克定律:确定目标、专注行动