Java最高

使用Spring 5和Spring Boot 2开始,通过学习的春天课程:

>>查看课程

1.介绍

Java的核心优点之一是在内置垃圾收集器(或GC短)。GC隐式地负责分配和释放内存,因此能够处理大部分内存泄漏问题。

虽然GC可以有效地处理相当一部分内存,但它不能保证提供一个解决内存泄漏的万无一失的解决方案。GC是相当聪明的,但不是完美无缺的。内存泄漏仍然可能偷偷摸摸地发生,即使是在一个谨慎的开发人员的应用程序中。

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

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

2.什么是内存泄漏

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

内存泄漏之所以不好是因为它阻止内存资源并随着时间的推移降低系统性能。如果不处理,应用程序将最终耗尽其资源,最终以致命的方式终止java.lang.OutOfMemoryError

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

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

内存泄漏的症状

  • 当应用程序连续运行很长时间时,严重的性能下降
  • OutOfMemoryError应用程序中的堆错误
  • 自发的和奇怪的应用程序崩溃
  • 应用程序有时会耗尽连接对象

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

3.Java中的内存泄漏类型

在任何应用程序中,有很多原因可能发生内存泄漏。在本节中,我们将讨论最常见的。

3.1。内存泄漏通过静态字段

可能导致潜在内存泄漏的第一个场景是大量使用静态变量。

在Java中,静态字段的生命周期通常与正在运行的应用程序的整个生命周期相匹配(除非类加载器有资格获得垃圾收集)。

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

public class static {public static List List = new ArrayList<>();public void populateList() {for (int i = 0;我< 10000000;我+ +){list.add (math . random ());} Log.info("调试点2");} public static void main(String[] args) {Log.info("调试点1");新的StaticTest () .populateList ();Log.info(“调试点3”);}}

现在,如果我们在这个程序执行期间分析堆内存,那么我们将看到,在调试点1到2之间,正如预期的那样,堆内存增加了。

但是当我们离开Populatelist()方法在调试点3处,堆内存尚未收集垃圾正如我们在VisualVM响应中看到的:

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

第一部分直到调试点几乎与我们在的情况下获得的相同静态的。但这次我们离开后Populatelist()方法,列表的所有内存都是收集的垃圾,因为我们没有任何引用

因此,我们需要非常密切地注意…的用法静态变量。如果集合或大型对象声明为静态,则它们在应用程序的整个生命周期中都保留在内存中,从而阻塞了可能在其他地方使用的重要内存。

如何预防?

  • 尽量减少使用静态变量
  • 当使用单例时,依赖于惰性加载对象的实现,而不是主动加载对象

3.2。通过打开资源

每当我们建立一个新的连接或打开一个流时,JVM都会为这些资源分配内存。一些例子包括数据库连接、输入流和会话对象。

忘记关闭这些资源会阻塞内存,从而使GC无法访问它们。这甚至可以发生在出现异常的情况下,该异常阻止程序执行到达正在处理关闭这些资源的代码的语句。

在这两种情况下,从资源留下的打开连接消耗内存,如果我们不处理它们,它们可能会恶化性能,甚至可能导致OutOfMemoryError

如何预防?

  • 总是使用最后块关闭资源
  • 代码(即使是在最后块)关闭资源本身不应该有任何例外情况
  • 使用Java 7+时,我们可以使用试一试资源块

3.3。不当equals ()hashcode()实现

在定义新课程时,一个非常常见的监督没有写适当的覆盖方法equals ()hashcode()方法。

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

让我们扮演一个微不足道的例子类并用它作为一个关键HashMap

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

现在我们将插入重复成一个对象地图用这把钥匙。

记住,一个地图不能包含重复的键:

@Test public void givenMap_whenEqualsAndHashCodeNotOverridden_thenMemoryLeak() {Map Map = new HashMap<>();for (int i = 0;我< 100;我+ +){地图。put(新人(jon), 1);} Assert.assertFalse(map.size() == 1);}

在这里我们使用作为一个关键。自从地图不允许重复键,众多重复我们作为密钥插入的对象不应该增加内存。

因为我们还没有定义properequals ()方法时,重复的对象会堆积起来,增加内存这就是为什么我们在记忆中看到不止一个物体。VisualVM中的堆内存是这样的:

然而,如果我们覆盖了equals ()hashcode()方法正确,那么就只存在一个对象在这方面地图

让我们来看看正确的实现equals ()hashcode()对于我们的类:

public class Person {public String name;public Person(String name) {this.name = name;} @Override public boolean equals(Object o){如果(o == this)返回true;如果(!(o instanceof Person)){返回false;} Person Person = (Person) o;返回person.name.equals(名称);} @Override public int hashCode() {int result = 17;result = 31 * result + name.hashCode();返回结果; } }

在这种情况下,以下断言是真的:

@Test public void givenMap_whenEqualsAndHashCodeNotOverridden_thenMemoryLeak() {Map Map = new HashMap<>();for (int i = 0;我< 2;我+ +){地图。put(新人(jon), 1);} Assert.assertTrue(map.size() == 1);}

在妥善覆盖后equals ()hashcode(),同一程序的堆内存看起来如:

另一个例子是使用ORM工具,如Hibernate,它使用equals ()hashcode()方法分析对象并保存在缓存中。

如果这些方法没有被覆盖,内存泄漏的几率会很高因为这样Hibernate就不能比较对象,只能用重复的对象填充缓存。

如何预防?

  • 作为拇指的规则,在定义新实体时,总是覆盖equals ()hashcode()方法
  • 不仅要覆盖这些方法,还必须以最优的方式覆盖这些方法

欲了解更多信息,请访问我们的教程产生equals ()hashcode()与Eclipse指南hashcode()在Java.

3.4。引用外部类的内部类

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

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

考虑这样一个类,它持有对大量庞大对象的引用,并且有一个非静态的内部类。现在,当我们只创建内部类的对象时,内存模型如下:

但是,如果我们只是将内部类声明为静态,那么相同的内存模型看起来像这样:

这是因为内部类对象隐式地持有对外部类对象的引用,从而使其成为无效的垃圾收集候选对象。匿名类的情况也是如此。

如何预防?

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

3.5。通过finalize()方法

使用终结器是潜在内存泄漏问题的另一个来源。当一个类的finalize()方法将被重写该类的对象不会立即被垃圾收集。相反,GC将它们排在队列中以便在稍后的时间结束。

此外,如果代码写入finalize()方法不是最佳的,如果终结器队列无法跟上Java垃圾收集器,那么我们的应用程序才会达到迎接OutOfMemoryError

为了演示这一点,让我们考虑我们有一个类,我们已经覆盖了finalize()方法,并且该方法需要一点时间来执行。当这个类的大量对象得到垃圾收集时,然后在VisualVM中,它看起来像:

但是,如果我们只是删除被覆盖的finalize()方法,然后同一个程序给出如下响应:

如何预防?

  • 我们应该避免使用终结器

了解更多关于金宝搏官网188befinalize(),阅读第三部分(避免终结者)在我们的Java中的finalize方法指南

3.6。实习过字符串

Java细绳当pool从PermGen转移到堆空间时,它在Java 7中经历了一个重大的变化。但是对于在版本6及其以下运行的应用程序,在处理大型应用程序时应该更加注意字符串

如果我们读过一个巨大的巨大细绳对象,并调用实习生()在该对象上,它将转到字符串池,该字符串池位于PermGen(永久内存)中,并且在应用程序运行期间将一直待在那里。这将阻塞内存,并在我们的应用程序中造成严重的内存泄漏。

在这种情况下,JVM 1.6中的PermGen在VisualVM中是这样的:

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

如何预防?

  • 解决这个问题的最简单方法是升级到最新的Java版本,因为字符串池从Java version 7开始移动到HeapSpace
  • 如果大规模工作字符串,增加烫发空间的大小,以避免任何潜力OutofMemoryErrors.
    - xx: MaxPermSize = 512

3.7。使用Threadlocal.S.

Threadlocal.(详细讨论介绍Threadlocal.在Java.是一个构造,它给了我们将状态隔离到特定线程的能力,从而允许我们实现线程安全。

使用此构造时,每个线程都拥有一个对它的副本的隐式引用Threadlocal.变量并将维护自己的副本,而不是在多个线程上共享资源,只要线程就活着即可。

尽管有它的优点,使用Threadlocal.变量是有争议的,因为如果没有正确使用,它们是臭名昭着的臭名昭着的臭名昭着。约书亚布洛赫一次评论线程本地使用

“草率地使用线程池和草率地使用线程局部变量可能会导致无意识的对象保留,这在很多地方已经被注意到了。但将责任推到线程本地用户身上是毫无根据的。”

内存泄漏Threadlocals.

Threadlocals.一旦保持线程不再活跃,就应该是收集的垃圾。但问题出现了Threadlocals.与现代应用服务器一起使用。

现代的应用服务器使用线程池来处理请求,而不是创建新的请求(例如遗嘱执行人如果是Apache Tomcat)。此外,它们还使用一个单独的类加载器。

自从线程池在应用程序服务器中,在线程重用的概念上工作,它们永远不会收集垃圾 - 相反,它们已重复使用以服务其他请求。

现在,如果任何类创建了Threadlocal.变量但没有明确删除它,然后该对象的副本将留在工作者身边线程即使在Web应用程序停止之后,也可以防止对象被收集的垃圾。

如何预防?

  • 清理是一个很好的做法Threadlocals.当它们不再使用时Threadlocals.提供remove ()方法,删除此变量的当前线程的值
  • 不使用ThreadLocal.set(空)清除价值-它实际上不会清除值,而是会查找地图与当前线程关联,并将键值对设置为当前线程和分别
  • 考虑一下就更好了Threadlocal.作为需要关闭的资源最后块只是为了确保它始终关闭,即使在异常情况下:
    尝试{threadlocal.set(system.nanotime());// ......进一步处理}最后{threadlocal.remove();}

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

尽管在处理内存泄漏时没有一种适用于所有情况的解决方案,但是有一些方法可以使这些泄漏最小化。

4.1。启用貌相

Java分析器是监视和诊断应用程序内存泄漏的工具。它们分析应用程序内部发生的事情——例如,内存是如何分配的。

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

我们使用Java VisualVM整个教程的第3部分。请查看我们的Java分析器指南了解不同类型的分析金宝搏官网188be器,如任务控制、JProfiler、YourKit、Java VisualVM和Netbeans Profiler。

4.2。详细的垃圾收集

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

- verbose: gc

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

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

我们也可以求助于Java中内置的引用对象java.lang.ref包处理内存泄漏。使用java.lang.ref包,而不是直接引用对象,我们使用特殊的对象引用,使它们易于垃圾收集。

引用队列是为让我们知道垃圾收集器执行的操作而设计的。欲了解更多信息,请阅读Java中的软引用金宝搏188体育Baeldung教程,特别是第4节。

4.4。Eclipse内存泄漏警告

对于JDK 1.5及以上版本的项目,每当遇到明显的内存泄漏时,Eclipse都会显示警告和错误。因此,在Eclipse中开发时,我们可以定期访问“Problems”选项卡,对内存泄漏警告(如果有的话)更加警惕:金宝搏官网188be

4.5。基准测试

我们可以通过执行基准测试来度量和分析Java代码的性能。这样,我们就可以比较完成相同任务的不同方法的性能。这可以帮助我们选择一个更好的方法,也可以帮助我们保存内存。

更多关于基准测试的信息,请访问我们的金宝搏官网188be微基准测试与Java教程。

4.6。代码评审

最后,我们总是使用经典的、老式的方法来完成简单的代码演练。

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

5.结论

用外行人的话说,我们可以把内存泄漏看作是一种疾病,它会阻塞重要的内存资源,从而降低应用程序的性能。与所有其他疾病一样,如果不治愈,它可能随着时间的推移导致致命的应用程序崩溃。

内存泄漏是很难解决的,查找内存泄漏需要对Java语言的复杂掌握和控制。在处理内存泄漏时,没有一种适用于所有问题的解决方案,因为泄漏可以通过各种各样的事件发生。

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

与始终一样,用于生成本教程中描述的VisualVM响应的代码片段可用GitHub上

Java底部

使用Spring 5和Spring Boot 2开始,通过学习的春天课程:

>>查看课程
本文评论关闭!