来源:SpringForAll社区

1. 简介

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

虽然GC有效地处理了大部分内存,但它并不能成为保证内存泄漏的万无一失的解决方案。GC很聪明,但并不完美。即使在尽职尽责的开发人员的应用程序中,内存仍然可能会泄漏。

仍然可能存在应用程序生成大量多余对象的情况,从而耗尽关键内存资源,有时会导致整个应用程序失败。

内存泄漏是Java中的一个真实存在的问题。在本教程中,我们将了解内存泄漏的潜在原因是什么,如何在运行时识别它们,以及如何在我们的应用程序中处理它们

2. 什么是内存泄漏

内存泄漏是堆中存在不再使用的对象但垃圾收集器无法从内存中删除它们的情况,因此它们会被不必要地一直存在。

内存泄漏很糟糕,因为它会耗尽内存资源并降低系统性能。如果不处理,应用程序最终将耗尽其资源,最终以致命的java.lang.OutOfMemoryError终止。

堆内存中有两种不同类型的对象 - 被引用和未被引用。被引用的对象是在应用程序中仍具有活动引用的对象,而未被引用的对象没有任何的活动引用。

垃圾收集器会定期删除未引用的对象,但它永远不会收集仍在引用的对象。这是可能发生内存泄漏的地方:

内存泄漏的症状

  • 应用程序长时间连续运行时性能严重下降

  • 应用程序中的OutOfMemoryError堆错误

  • 自发且奇怪的应用程序崩溃

  • 应用程序偶尔会耗尽连接对象

让我们仔细看看其中一些场景以及如何处理它们。

3. Java中内存泄漏类型

在任何应用程序中,数不清的原因可能导致内存泄漏。在本节中,我们将讨论最常见的问题。

3.1 static字段引起的内存泄漏

可能导致潜在内存泄漏的第一种情况是大量使用static(静态)变量。

在Java中,静态字段通常拥有与整个应用程序相匹配的生命周期(除非 ClassLoader复合垃圾回收的条件)。

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

  1. public class StaticTest {

  2.    public static List<Double> list = new ArrayList<>();

  3.    public void populateList() {

  4.        for (int i = 0; i < 10000000; i++) {

  5.            list.add(Math.random());

  6.        }

  7.        Log.info("Debug Point 2");

  8.    }

  9.    public static void main(String[] args) {

  10.        Log.info("Debug Point 1");

  11.        new StaticTest().populateList();

  12.        Log.info("Debug Point 3");

  13.    }

  14. }

现在如果我们在程序中分析堆内存,我们会发现在调试点1和2之间,和预期中的一样,对内存增加了。

但当我们在调试点3,离开 populateList()方法时,堆内存并没有被垃圾回收,正如我们在 VisualVM响应中看到的一样:

但是,在上面的程序中,在第2行中,如果我们只删除关键字 static,那么它将对内存使用量带来巨大的变化,这个 VisualVM响应显示:

直到调试点的第一部分几乎与我们在 static情况下获得的部分相同 。但这次当我们离开 populateList()方法,列表中所有的内存都被垃圾回收掉了,因为我们没有任何对他的引用

因此,我们需要非常关注static(静态)变量的使用。如果集合或大对象被声明为static,那么它们将在应用程序的整个生命周期中保留在内存中,从而阻止可能在其他地方使用的重要内存。

如何预防呢?

  • 最大限度地减少静态变量的使用

  • 使用单例时,依赖于延迟加载对象而不是立即加载的方式

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

每当我们创建连接或打开一个流时,JVM都会为这些资源分配内存。例如数据库连接,输入流或者会话对象。

忘记关闭这些资源会导致持续占有内存,从而使他们无法GC。如果异常阻止程序执行到达处理关闭这些资源的代码,则甚至可能发生这种情况。

在任一种情况下,资源留下的开放连接都会消耗内存,如果我们不处理他们,他们可能会降低性能,甚至可能导致 OutOfMemoryError

如何预防呢?

  • 始终使用 finally块来关闭资源

  • 关闭资源的代码(甚至在 finally块中)本身不应该有任何异常

  • 使用Java 7+时,我们可以使用 try-with-resources

3.3 不正确的 equals()和 hashCode()实现

在定义新类时,一个非常常见的疏忽是不为 equals()hashCode()方法编写适当的重写方法。

HashSetHashMap 在许多操作中使用这些方法,如果它们没有被正确覆盖,那么它们可能成为潜在的内存泄漏问题的来源。

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

  1. public class Person {

  2.    public String name;

  3.    public Person(String name) {

  4.        this.name = name;

  5.    }

  6. }

现在我们将重复的Person对象插入到使用此键的Map中。

请记住,Map不能包含重复的键:

  1. @Test

  2. public void givenMap_whenEqualsAndHashCodeNotOverridden_thenMemoryLeak() {

  3.    Map<Person, Integer> map = new HashMap<>();

  4.    for(int i=0; i<100; i++) {

  5.        map.put(new Person("jon"), 1);

  6.    }

  7.    Assert.assertFalse(map.size() == 1);

  8. }

这里我们使用Person作为关键。由于 Map不允许重复键,因此我们作为键插入的众多重复 Person对象不应增加内存。

但是由于我们没有定义正确的equals()方法,重复的对象会堆积并增加内存,这就是我们在内存中看到多个对象的原因。VisualVM中的堆内存如下所示:

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

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

  1. public class Person {

  2.    public String name;

  3.    public Person(String name) {

  4.        this.name = name;

  5.    }

  6.    @Override

  7.    public boolean equals(Object o) {

  8.        if (o == this) return true;

  9.        if (!(o instanceof Person)) {

  10.            return false;

  11.        }

  12.        Person person = (Person) o;

  13.        return person.name.equals(name);

  14.    }

  15.    @Override

  16.    public int hashCode() {

  17.        int result = 17;

  18.        result = 31 * result + name.hashCode();

  19.        return result;

  20.    }

  21. }

在这种情况下,下面的断言将会是true:

  1. @Test

  2. public void givenMap_whenEqualsAndHashCodeNotOverridden_thenMemoryLeak() {

  3.    Map<Person, Integer> map = new HashMap<>();

  4.    for(int i=0; i<2; i++) {

  5.        map.put(new Person("jon"), 1);

  6.    }

  7.    Assert.assertTrue(map.size() == 1);

  8. }

在适当的重写 equals()hashCode()之后,堆内存在同一程序中如下所示:

另一个例子是当使用像 hibernate这样的ORM框架,他们使用 equals()hashCode()方法去分析对象然后将他们保存在缓存中。

如何预防呢?

  • 根据经验,定义新的实体时,总要重写 equals()和 hashCode()方法。

  • 只是重写他们是不够的,这些方法必须以最佳的方式被重写。

有关更多信息,请访问我们的 Generate equals() and hashCode() with Eclipse 和Guide to hashCode() in Java。

3.4 引用了外部类的内部类

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

默认情况下,每个非静态内部类都包含对其包含类的隐式引用。如果我们在应用程序中使用这个内部类'对象,那么即使在我们的包含类'对象超出范围之后,它也不会被垃圾收集

考虑一个类,它包含对大量庞大对象的引用,并具有非静态内部类。现在,当我们创建一个内部类的对象时,内存模型如下所示:

但是,如果我们只是将内部类声明为static,那么相同的内存模型如下所示:

发生这种情况是因为内部类对象隐式地保存对外部类对象的引用,从而使其成为垃圾收集的无效候选者。在匿名类的情况下也是如此。

如何预防呢?

  • 如果内部类不需要访问包含的类成员,请考虑将其转换为静态类

3.5 finalize()方法造成的内存泄漏

使用 finalizers是潜在的内存泄漏问题的另一个来源。每当重写类的 finalize()方法时,该类的对象不会立即被垃圾收集。相反,GC将它们排队等待最终确定,这将在稍后的时间点发生。

另外,如果用 finalize()方法编写的代码不是最佳的,并且终结器队列无法跟上Java垃圾收集器,那么迟早,我们的应用程序注定要遇到 OutOfMemoryError

为了证明这一点,让我们考虑一下我们已经覆盖了 finalize()方法的类,并且该方法需要一些时间来执行。当这个类的大量对象被垃圾收集时,那么在VisualVM中,它看起来像:

但是,如果我们只删除重写的finalize()方法,那么同一程序会给出以下响应:

如何预防呢?

  • 我们应该总是避免 finalizers

有关finalize()的更多详细信息,请阅读我们的 Guide to the finalize Method in Java 第3节(避免终结器) 。

常量字符串造成的内存泄漏

Java String池Java 7时经历了在从永生代(PermGen)转移到堆空间(HeapSpace)的重大变化。但是对于在版本6及更低版本上运行的应用程序,在使用大型字符串时我们应该更加专心。

如果我们读取一个庞大的大量String对象,并在该对象上调用intern(),那么它将转到字符串池,它位于PermGen(永生代)中,并且只要我们的应用程序运行就会保留在那里。这会占用内存并在我们的应用程序中造成重大内存泄漏。

JVM 1.6中这种情况的PermGen在VisualVM中看起来像这样:

与此相反,在一个方法中,如果我们只是从文件中读取一个字符串而不是 intern(),那么PermGen看起来像:

如何预防呢?

  • 解决此问题的最简单方法是升级到最新的Java版本,因为String池从Java版本7开始转移到HeapSpace

  • 如果处理大型字符串,请增加PermGen空间的大小以避免任何潜在的OutOfMemoryErrors:

-XX:MaxPermSize=512m

3.7 使用 ThreadLocal造成的内存泄漏

ThreadLocal (在Introduction to ThreadLocal in Java 中详细介绍),是一种能将状态隔离到特定线程,从而保证我们实现线程安全的结构。

使用此结构时,每个线程只要处于存活状态即可将保留对其ThreadLocal变量副本的隐式引用,并且将保留其自己的副本,而不是跨多个线程共享资源。

尽管有其优点,ThreadLocal 变量的使用仍存在争议,因为如果使用不当,它们会因引入内存泄漏而臭名昭着。 Joshua Bloch once commented on thread local usage:

“Sloppy use of thread pools in combination with sloppy use of thread locals can cause unintended object retention, as has been noted in many places. But placing the blame on thread locals is unwarranted.”

"随意的在线程池中使用 ThreadLocal会保留很多意外的对象。但把责任归咎于 ThreadLocal是没有根据的 "

ThreadLocal中的内存泄漏

一旦保持线程不再存在, ThreadLocals应该被垃圾收集。但是当 ThreadLocals与现代应用程序服务器一起使用时,问题就出现了。

现代应用程序服务器使用线程池来处理请求而不是创建新请求(例如在Apache Tomcat的情况下为Executor)。此外,他们还使用单独的类加载器。

由于应用程序服务器中的线程池在线程重用的概念上工作,因此它们永远不会被垃圾收集 - 相反,它们会被重用来处理另一个请求。

现在,如果任何类创建 ThreadLocal 变量但未显式删除它,则即使在Web应用程序停止后,该对象的副本仍将保留在工作线程中,从而防止对象被垃圾回收。

如何预防呢?

  • 在不再使用 ThreadLocals时清理 ThreadLocals是一个很好的做法- ThreadLocals提供了 remove()方法,该方法删除了此变量的当前线程值

  • 不要使用 ThreadLocal.set(null) 来清除该值 - 它实际上不会清除该值,而是查找与当前线程关联的 Map并将键值对设置为当前线程并分别为null

最好将 ThreadLocal 视为需要在 finally块中关闭的资源,以 确保它始终关闭,即使在异常的情况下:

  1. try {

  2. threadLocal.set(System.nanoTime());

  3. //... further processing

  4. }

  5. finally {

  6. threadLocal.remove();

  7. }

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

虽然在处理内存泄漏时没有一个通用的解决方案,但有一些方法可以最大限度地减少这些泄漏。

4.1 使用 Profiling工具

Java分析器是通过应用程序监视和诊断内存泄漏的工具。他们分析我们的应用程序内部发生了什么 - 例如,如何分配内存。

使用分析器,我们可以比较不同的方法,并找到我们可以最佳地使用我们的资源的领域。

我们在本教程的第3部分中使用了Java VisualVM。请查看我们的 Java Profilers指南, 了解不同类型的分析器,如Mission Control,JProfiler,YourKit,Java VisualVM和Netbeans Profiler。

4.2 详细垃圾回收

通过启用详细垃圾收集,我们将跟踪GC的详细跟踪。要启用此功能,我们需要将以下内容添加到JVM配置中:

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

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

我们还可以使用java中的引用对象来构建 java.lang.ref包来处理内存泄漏。使用 java.lang.ref包,我们使用对象的特殊引用,而不是直接引用对象,这些对象可以很容易地进行垃圾回收。

引用队列旨在让我们了解垃圾收集器执行的操作。有关更多信息,请阅读Baeldung的 Soft References in Java ,特别是第4节。

Eclipse的内存泄漏警告

对于JDK 1.5及更高版本的项目,Eclipse会在遇到明显的内存泄漏情况时显示警告和错误。因此,在Eclipse中开发时,我们可以定期访问“问题”选项卡,并对内存泄漏警告(如果有)更加警惕:

4.5 基准分析

我们可以通过执行基准来测量和分析Java代码的性能。这样,我们可以比较替代方法的性能来完成相同的任务。这可以帮助我们选择更好的方法,并可以帮助我们节约内存。

4.6 代码审核

最后,我们总是采用经典怀旧方式进行简单的代码审核。

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

5 结论

通俗地说,我们可以将内存泄漏视为一种通过阻止重要内存资源来降低应用程序性能的疾病。和所有其他疾病一样,如果不治愈,它可能导致致命的应用程序崩溃随着时间的推移。

内存泄漏很难解决,找到它们需要通过Java语言进行复杂的掌握和命令。在处理内存泄漏时,没有一个通用的解决方案,因为泄漏可能通过各种各样的事件发生。

但是,如果我们采用最佳实践并定期执行严格的代码演练和分析,那么我们可以最大程度地降低应用程序中内存泄漏的风险。

与往常一样,GitHub提供了用于生成本教程中描述的VisualVM响应的代码片段 。

原文链接:https://www.baeldung.com/java-memory-leaks

作者:baeldung

译者:thornhill

·END·

 近期热文:

  • Spring Cloud Stream 学习小清单

  • Spring Cloud Stream 使用延迟消息实现定时任务(RabbitMQ)

  • Git 常用命令清单,掌握这些,轻松驾驭版本管理

  • 优先级队列(头条面试题)

  • 来谈下高并发和分布式中的幂等处理

  • 你应该知道的7个写出更好的 Java 代码的技巧

  • 百亿数据量下,掌握这些Redis技巧你就能Hold全场

  • 深入聊一聊 Spring AOP 实现机制

  • 我说分布式事务之最大努力通知型事务

  • 我说分布式事务之TCC

  • 不可错过的CMS学习笔记

  • 可能是最全面的G1学习笔记

看完,赶紧点个“好看”鸭

点鸭点鸭

↓↓↓↓

了解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中的内存泄漏

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

  7. 翻译 | 理解Java中的内存泄漏

    猪年第一篇译文,大家多多支持! 原文自工程师baeldung博客,传送门 1. 介绍 Java 的其中一个核心特点是经由内置的垃圾回收机制(GC)下的自动化内存管理.GC 默默地处理着内存分配和释放工 ...

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

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

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

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

最新文章

  1. 获取安卓应用APK包名的方法
  2. 剑指-从尾到头打印链表
  3. 2021泗阳致远中学高考成绩查询,2021高考成绩什么时候发布?可通过哪些途径查询?...
  4. 鼠标方式自动弹出内容html,html 在一个超链接上面,鼠标移动上去时,也显示一串文字,如何实现...
  5. is not a function_libcxx 的 std::function 源码分析
  6. ipv6 neutron应用(一)
  7. 完全自定义TabBar(八)
  8. 如何用wps自动生成目录,你学会了吗?
  9. ctf中压缩包隐写经验总结
  10. AndroidQQ登录
  11. C++函数的定义与使用
  12. win7系统如何查看自己电脑IP
  13. php 数组的长度函数,php数组长度函数的例子
  14. 4k显示器用html好还是dp,2K、4K显示器的高清线你造怎么选吗?
  15. C++ 关键字 typeid, typename
  16. Flask、sqlite3、pipenv实现用户注册和登录(HandBook,菜鸟都会的)
  17. iOS XPC connection interrupted
  18. mysql优化手段——潭州学院
  19. 日常服务器巡检时,free -m命令的说明
  20. PP(3) MRP的控制参数

热门文章

  1. couchdb 垂直权限绕过漏洞(cve-2017-12635)
  2. linux ip_conntrack 连接满导致网络丢包
  3. python3 错误string indices must be integers 的解决方法
  4. linux chkconfig 添加服务 开机启动
  5. 安装phpMyAdmin图文教程
  6. 查看临界区等待线程数量
  7. IDT系列:(一)初探IDT,Interrupt Descriptor Table,中断描述符表
  8. linux kernel makefile analysis
  9. 红黑树:自平衡的二叉查找树
  10. icmp 报文中的进程号