原文连接:https://www.baeldung.com/java-memory-leakshtml
做者:baeldungjava
译者:thornhillgit
Java的核心优点之一是在内置垃圾收集器(简称GC)的帮助下实现自动内存管理。GC隐含地负责分配和释放内存,所以可以处理大多数内存泄漏问题。github
虽然GC有效地处理了大部份内存,但它并不能成为保证内存泄漏的万无一失的解决方案。GC很聪明,但并不完美。即便在尽职尽责的开发人员的应用程序中,内存仍然可能会泄漏。数据库
仍然可能存在应用程序生成大量多余对象的状况,从而耗尽关键内存资源,有时会致使整个应用程序失败。apache
内存泄漏是Java中的一个真实存在的问题。在本教程中,咱们将了解内存泄漏的潜在缘由是什么,如何在运行时识别它们,以及如何在咱们的应用程序中处理它们。api
内存泄漏是堆中存在再也不使用的对象但垃圾收集器没法从内存中删除它们的状况,所以它们会被没必要要地一直存在。缓存
内存泄漏很糟糕,由于它会耗尽内存资源并下降系统性能。若是不处理,应用程序最终将耗尽其资源,最终以至命的java.lang.OutOfMemoryError终止。tomcat
堆内存中有两种不一样类型的对象 - 被引用和未被引用。被引用的对象是在应用程序中仍具备活动引用的对象,而未被引用的对象没有任何的活动引用。安全
垃圾收集器会按期删除未引用的对象,但它永远不会收集仍在引用的对象。这是可能发生内存泄漏的地方:
内存泄漏的症状
让咱们仔细看看其中一些场景以及如何处理它们。
在任何应用程序中,数不清的缘由可能致使内存泄漏。在本节中,咱们将讨论最多见的问题。
static
字段引发的内存泄漏可能致使潜在内存泄漏的第一种状况是大量使用static(静态)变量。
在Java中,静态字段一般拥有与整个应用程序相匹配的生命周期(除非ClassLoader
复合垃圾回收的条件)。
让咱们建立一个填充静态列表的简单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
响应中看到的同样:
可是,在上面的程序中,在第2行中,若是咱们只删除关键字 static
,那么它将对内存使用量带来巨大的变化,这个Visual VM
响应显示:
直到调试点的第一部分几乎与咱们在static
状况下得到的部分相同 。但此次当咱们离开populateList()
方法,列表中全部的内存都被垃圾回收掉了,由于咱们没有任何对他的引用。
所以,咱们须要很是关注static(静态)变量的使用。若是集合或大对象被声明为static,那么它们将在应用程序的整个生命周期中保留在内存中,从而阻止可能在其余地方使用的重要内存。
如何预防呢?
每当咱们建立链接或打开一个流时,JVM都会为这些资源分配内存。例如数据库链接,输入流或者会话对象。
忘记关闭这些资源会致使持续占有内存,从而使他们没法GC。若是异常阻止程序执行到达处理关闭这些资源的代码,则甚至可能发生这种状况。
在任一种状况下,资源留下的开放链接都会消耗内存,若是咱们不处理他们,他们可能会下降性能,甚至可能致使OutOfMemoryError
。
如何预防呢?
finally
块来关闭资源finally
块中)自己不该该有任何异常try -with-resources
块equals()
和hashCode()
实现在定义新类时,一个很是常见的疏忽是不为equals()
和hashCode()
方法编写适当的重写方法。
HashSet
和 HashMap
在许多操做中使用这些方法,若是它们没有被正确覆盖,那么它们可能成为潜在的内存泄漏问题的来源。
让咱们以一个简单的Person
类为例, 并将其用做HashMap中
的键 :
public class Person { public String name; public Person(String name) { this.name = name; } }
如今咱们将重复的Person对象插入到使用此键的Map中。
请记住,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
对象。
让咱们看一下正确的实现了equals()
和hashCode()
的Person
类:
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; } }
在这种状况下,下面的断言将会是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); }
在适当的重写equals()
和hashCode()
以后,堆内存在同一程序中以下所示:
另外一个例子是当使用像hibernate
这样的ORM框架,他们使用equals()
和hashCode()
方法去分析对象而后将他们保存在缓存中。
如何预防呢?
equals()
和hashCode()
方法。有关更多信息,请访问咱们的 Generate equals() and hashCode() with Eclipse 和Guide to hashCode() in Java。
这种状况发生在非静态内部类(匿名类)的状况下。对于初始化,这些内部类老是须要外部类的实例。
默认状况下,每一个非静态内部类都包含对其包含类的隐式引用。若是咱们在应用程序中使用这个内部类'对象,那么即便在咱们的包含类'对象超出范围以后,它也不会被垃圾收集。
考虑一个类,它包含对大量庞大对象的引用,并具备非静态内部类。如今,当咱们建立一个内部类的对象时,内存模型以下所示:
可是,若是咱们只是将内部类声明为static,那么相同的内存模型以下所示:
发生这种状况是由于内部类对象隐式地保存对外部类对象的引用,从而使其成为垃圾收集的无效候选者。在匿名类的状况下也是如此。
如何预防呢?
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
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
块中关闭的资源,以 确保它始终关闭,即便在异常的状况下:
try { threadLocal.set(System.nanoTime()); //... further processing } finally { threadLocal.remove(); }
虽然在处理内存泄漏时没有一个通用的解决方案,但有一些方法能够最大限度地减小这些泄漏。
Profiling
工具Java分析器是经过应用程序监视和诊断内存泄漏的工具。他们分析咱们的应用程序内部发生了什么 - 例如,如何分配内存。
使用分析器,咱们能够比较不一样的方法,并找到咱们能够最佳地使用咱们的资源的领域。
咱们在本教程的第3部分中使用了Java VisualVM。请查看咱们的 Java Profilers指南, 了解不一样类型的分析器,如Mission Control,JProfiler,YourKit,Java VisualVM和Netbeans Profiler。
经过启用详细垃圾收集,咱们将跟踪GC的详细跟踪。要启用此功能,咱们须要将如下内容添加到JVM配置中:
经过添加此参数,咱们能够看到GC内部发生的详细信息:
咱们还可使用java中的引用对象来构建java.lang.ref
包来处理内存泄漏。使用java.lang.ref
包,咱们使用对象的特殊引用,而不是直接引用对象,这些对象能够很容易地进行垃圾回收。
引用队列旨在让咱们了解垃圾收集器执行的操做。有关更多信息,请阅读Baeldung的 Soft References in Java ,特别是第4节。
对于JDK 1.5及更高版本的项目,Eclipse会在遇到明显的内存泄漏状况时显示警告和错误。所以,在Eclipse中开发时,咱们能够按期访问“问题”选项卡,并对内存泄漏警告(若是有)更加警戒:
咱们能够经过执行基准来测量和分析Java代码的性能。这样,咱们能够比较替代方法的性能来完成相同的任务。这能够帮助咱们选择更好的方法,并能够帮助咱们节约内存。
有关基准测试的更多信息,请访问咱们的 Microbenchmarking with Java 教程。
最后,咱们老是采用经典怀旧方式进行简单的代码审核。
在某些状况下,即便是这种微不足道的方法也能够帮助消除一些常见的内存泄漏问题。
通俗地说,咱们能够将内存泄漏视为一种经过阻止重要内存资源来下降应用程序性能的疾病。和全部其余疾病同样,若是不治愈,它可能致使致命的应用程序崩溃随着时间的推移。
内存泄漏很难解决,找到它们须要经过Java语言进行复杂的掌握和命令。在处理内存泄漏时,没有一个通用的解决方案,由于泄漏可能经过各类各样的事件发生。
可是,若是咱们采用最佳实践并按期执行严格的代码演练和分析,那么咱们能够最大程度地下降应用程序中内存泄漏的风险。